From 56de5f638646ff3a18438a8d2f8d584e98334487 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 6 Jun 2024 12:04:06 -0500 Subject: [PATCH 01/21] Add copy for new ML rule warnings --- .../components/step_define_rule/translations.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/translations.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/translations.tsx index ef2a6adcc57c6..08e391733af83 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/translations.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/translations.tsx @@ -234,6 +234,21 @@ export const EQL_SEQUENCE_SUPPRESSION_GROUPBY_VALIDATION_TEXT = i18n.translate( } ); +export const MACHINE_LEARNING_SUPPRESSION_DISABLED_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.machineLearningSuppressionDisabledLabel', + { + defaultMessage: 'Alert suppression will be disabled until the Machine Learning jobs are run.', + } +); + +export const MACHINE_LEARNING_SUPPRESSION_INCOMPLETE_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.machineLearningSuppressionIncompleteLabel', + { + defaultMessage: + 'Alert suppression may have incomplete field options until all Machine Learning jobs are run.', + } +); + export const GROUP_BY_TECH_PREVIEW_LABEL_APPEND = i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByFieldsTechPreviewLabelAppend', { From 661a195e362f08db89bdfe7697b164d48e236178 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 6 Jun 2024 15:55:57 -0500 Subject: [PATCH 02/21] Add UX messaging for ML suppression edge cases * Disables suppression fields if no relevant ML jobs are running (as we cannot retrieve field info) * Adds a warning message if not all relevant ML jobs are running (as we may be missing some field info) Next step is testing this; we don't currently have a way to run ML rules in cypress, but I'm going to attempt to copy the logic in our FTR to accomplish this. --- .../components/step_define_rule/index.tsx | 59 +++++++++++++------ .../step_define_rule/translations.tsx | 4 +- 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx index 839454922a14a..e3d89b0b5ff63 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx @@ -35,7 +35,7 @@ import type { DataViewBase } from '@kbn/es-query'; import { FormattedMessage } from '@kbn/i18n-react'; import { useSetFieldValueWithCallback } from '../../../../common/utils/use_set_field_value_cb'; import { useRuleFromTimeline } from '../../../../detections/containers/detection_engine/rules/use_rule_from_timeline'; -import { isMlRule } from '../../../../../common/machine_learning/helpers'; +import { isJobStarted, isMlRule } from '../../../../../common/machine_learning/helpers'; import { hasMlAdminPermissions } from '../../../../../common/machine_learning/has_ml_admin_permissions'; import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license'; import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml_capabilities'; @@ -93,6 +93,7 @@ import { NewTermsFields } from '../new_terms_fields'; import { ScheduleItem } from '../../../rule_creation/components/schedule_item_form'; import { RequiredFields } from '../../../rule_creation/components/required_fields'; import { DocLink } from '../../../../common/components/links_to_docs/doc_link'; +import { useInstalledSecurityJobs } from '../../../../common/components/ml/hooks/use_installed_security_jobs'; import { defaultCustomQuery } from '../../../../detections/pages/detection_engine/rules/utils'; import { MultiSelectFieldsAutocomplete } from '../multi_select_fields'; import { useLicense } from '../../../../common/hooks/use_license'; @@ -196,12 +197,24 @@ const StepDefineRuleComponent: FC = ({ const queryClient = useQueryClient(); const { isSuppressionEnabled: isAlertSuppressionEnabled } = useAlertSuppression(ruleType); - const mlCapabilities = useMlCapabilities(); const [openTimelineSearch, setOpenTimelineSearch] = useState(false); const [indexModified, setIndexModified] = useState(false); const [threatIndexModified, setThreatIndexModified] = useState(false); const license = useLicense(); + const mlCapabilities = useMlCapabilities(); + const installedMlJobs = useInstalledSecurityJobs(); + const [{ machineLearningJobId }] = useFormData({ + form, + watch: ['machineLearningJobId'], + }); + const ruleMlJobs = installedMlJobs.jobs.filter((job) => machineLearningJobId.includes(job.id)); + const numberOfRuleMlJobsStarted = ruleMlJobs.filter((job) => + isJobStarted(job.jobState, job.datafeedState) + ).length; + const noMlJobsStarted = numberOfRuleMlJobsStarted === 0; + const someMlJobsStarted = !noMlJobsStarted && numberOfRuleMlJobsStarted !== ruleMlJobs.length; + const esqlQueryRef = useRef(undefined); const isAlertSuppressionLicenseValid = license.isAtLeast(MINIMUM_LICENSE_FOR_SUPPRESSION); @@ -1068,22 +1081,32 @@ const StepDefineRuleComponent: FC = ({ } > - + <> + + {someMlJobsStarted && ( + + {i18n.MACHINE_LEARNING_SUPPRESSION_INCOMPLETE_LABEL} + + )} + Date: Thu, 6 Jun 2024 17:23:22 -0500 Subject: [PATCH 03/21] style: sort StepDefineRule arguments --- .../components/step_define_rule/index.tsx | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx index e3d89b0b5ff63..2fb012428e395 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx @@ -168,31 +168,31 @@ const IntendedRuleTypeEuiFormRow = styled(RuleTypeEuiFormRow)` // eslint-disable-next-line complexity const StepDefineRuleComponent: FC = ({ - isLoading, - isUpdateView = false, - kibanaDataViews, - indicesConfig, - threatIndicesConfig, + browserFields, + dataSourceType, defaultSavedQuery, + enableThresholdSuppression, form, - optionsSelected, - setOptionsSelected, + groupByFields, + index, indexPattern, + indicesConfig, isIndexPatternLoading, - browserFields, + isLoading, isQueryBarValid, + isUpdateView = false, + kibanaDataViews, + optionsSelected, + queryBarSavedId, + queryBarTitle, + ruleType, setIsQueryBarValid, setIsThreatQueryBarValid, - ruleType, - index, - threatIndex, - groupByFields, - dataSourceType, + setOptionsSelected, shouldLoadQueryDynamically, - queryBarTitle, - queryBarSavedId, + threatIndex, + threatIndicesConfig, thresholdFields, - enableThresholdSuppression, }) => { const queryClient = useQueryClient(); From 5273d1f10c4de3958e599b091ce935d426da9efc Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 6 Jun 2024 17:25:53 -0500 Subject: [PATCH 04/21] Provide ML fields on Define step of rule creation This was previously only available on the About step, via the useRuleIndices hook in combination with the useFetchIndex hook. Add new composite hook that encapsulates the same logic, and provides it to the define step. Unlike on the About step, we are currently only using this for ML fields as other situations derive their field list from a passed prop (which might be a performance optimization, or a bug, or both). --- .../components/step_define_rule/index.tsx | 9 +++++ .../rule_management/logic/use_rule_fields.ts | 36 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_rule_fields.ts diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx index 2fb012428e395..a74847a9e61fb 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx @@ -104,6 +104,7 @@ import { useUpsellingMessage } from '../../../../common/hooks/use_upselling'; import { useAllEsqlRuleFields } from '../../hooks'; import { useAlertSuppression } from '../../../rule_management/logic/use_alert_suppression'; import { RelatedIntegrations } from '../../../rule_creation/components/related_integrations'; +import { useRuleFields } from '../../../rule_management/logic/use_rule_fields'; const CommonUseField = getUseField({ component: Field }); @@ -214,6 +215,11 @@ const StepDefineRuleComponent: FC = ({ ).length; const noMlJobsStarted = numberOfRuleMlJobsStarted === 0; const someMlJobsStarted = !noMlJobsStarted && numberOfRuleMlJobsStarted !== ruleMlJobs.length; + const { loading: mlFieldsLoading, fields: mlFields } = useRuleFields({ machineLearningJobId }); + const mlSuppressionFields = useMemo( + () => getTermsAggregationFields(mlFields as BrowserField[]), + [mlFields] + ); const esqlQueryRef = useRef(undefined); @@ -1088,11 +1094,14 @@ const StepDefineRuleComponent: FC = ({ componentProps={{ browserFields: isEsqlRule(ruleType) ? esqlSuppressionFields + : isMlRule(ruleType) + ? mlSuppressionFields : termsAggregationFields, isDisabled: !isAlertSuppressionLicenseValid || areSuppressionFieldsDisabledBySequence || isEsqlSuppressionLoading || + mlFieldsLoading || noMlJobsStarted, disabledText: areSuppressionFieldsDisabledBySequence ? i18n.EQL_SEQUENCE_SUPPRESSION_DISABLE_TOOLTIP diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_rule_fields.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_rule_fields.ts new file mode 100644 index 0000000000000..c0f34c5502f94 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_rule_fields.ts @@ -0,0 +1,36 @@ +/* + * 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 { DataViewFieldBase } from '@kbn/es-query'; + +import { useRuleIndices } from './use_rule_indices'; +import { useFetchIndex } from '../../../common/containers/source'; + +interface UseRuleFieldParams { + machineLearningJobId?: string[]; + indexPattern?: string[]; +} + +interface UseRuleFieldsReturn { + loading: boolean; + fields: DataViewFieldBase[]; +} + +export const useRuleFields = ({ + machineLearningJobId, + indexPattern, +}: UseRuleFieldParams): UseRuleFieldsReturn => { + const { ruleIndices } = useRuleIndices(machineLearningJobId, indexPattern); + const [ + loading, + { + indexPatterns: { fields }, + }, + ] = useFetchIndex(ruleIndices); + + return { loading, fields }; +}; From 495583104df8521dbb3242f6b4f5c3827d51a27d Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Tue, 11 Jun 2024 16:54:47 -0500 Subject: [PATCH 05/21] WIP: adding cypress tests for ML warning To do this we need to get the ML API to recognize our jobs as installed and running. They are currently _not_ recognizing this (although there are anomalies in the index). Still troubleshooting to see what's missing, here. This logic was cribbed from the analogous FTR tests, but those also aren't working so *shrug*. --- .../components/step_define_rule/index.tsx | 12 ++ .../execution_logic/machine_learning.ts | 4 +- .../machine_learning_rule_suppression.cy.ts | 169 ++++++++++++------ .../cypress/objects/rule.ts | 1 + .../cypress/support/machine_learning.ts | 53 ++++++ 5 files changed, 182 insertions(+), 57 deletions(-) create mode 100644 x-pack/test/security_solution_cypress/cypress/support/machine_learning.ts diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx index a74847a9e61fb..efa57563e7d66 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx @@ -221,6 +221,18 @@ const StepDefineRuleComponent: FC = ({ [mlFields] ); + console.log( + JSON.stringify({ + installedMlJobs, + ruleMlJobs, + numberOfRuleMlJobsStarted, + noMlJobsStarted, + someMlJobsStarted, + mlFields, + mlSuppressionFields, + }) + ); + const esqlQueryRef = useRef(undefined); const isAlertSuppressionLicenseValid = license.isAtLeast(MINIMUM_LICENSE_FOR_SUPPRESSION); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/machine_learning.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/machine_learning.ts index 3fb077df86a38..8d153c01d6ac0 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/machine_learning.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/machine_learning.ts @@ -87,7 +87,7 @@ export default ({ getService }: FtrProviderContext) => { }; // FLAKY: https://github.com/elastic/kibana/issues/171426 - describe.skip('@ess @serverless @serverlessQA Machine learning type rules', () => { + describe('@ess @serverless @serverlessQA Machine learning type rules', () => { before(async () => { // Order is critical here: auditbeat data must be loaded before attempting to start the ML job, // as the job looks for certain indices on start @@ -110,7 +110,7 @@ export default ({ getService }: FtrProviderContext) => { }); // First test creates a real rule - remaining tests use preview API - it('should create 1 alert from ML rule when record meets anomaly_threshold', async () => { + it.only('should create 1 alert from ML rule when record meets anomaly_threshold', async () => { const createdRule = await createRule(supertest, log, rule); const alerts = await getAlerts(supertest, log, es, createdRule); expect(alerts.hits.hits.length).toBe(1); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule_suppression.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule_suppression.cy.ts index 88e6f738107c3..4508028863eda 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule_suppression.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule_suppression.cy.ts @@ -6,6 +6,8 @@ */ import { getMachineLearningRule } from '../../../../objects/rule'; +import { TOOLTIP } from '../../../../screens/common'; +import { ALERT_SUPPRESSION_FIELDS_INPUT } from '../../../../screens/create_new_rule'; import { DEFINITION_DETAILS, DETAILS_TITLE, @@ -13,6 +15,10 @@ import { SUPPRESS_FOR_DETAILS, SUPPRESS_MISSING_FIELD, } from '../../../../screens/rule_details'; +import { + executeSetupModuleRequest, + forceStartDatafeeds, +} from '../../../../support/machine_learning'; import { continueFromDefineStep, fillAlertSuppressionFields, @@ -46,79 +52,132 @@ describe( }, () => { let mlRule: ReturnType; + const jobId = 'v3_linux_anomalous_network_activity_ecs'; const suppressByFields = ['agent.name', 'host.name']; beforeEach(() => { login(); visit(CREATE_RULE_URL); - mlRule = getMachineLearningRule(); - selectMachineLearningRuleType(); - fillDefineMachineLearningRule(mlRule); }); describe('with Alert Suppression', () => { - it('allows a rule with per-execution suppression to be created and displayed', () => { - fillAlertSuppressionFields(suppressByFields); - continueFromDefineStep(); - - // ensures details preview works correctly - cy.get(DEFINITION_DETAILS).within(() => { - getDetails(SUPPRESS_BY_DETAILS).should('have.text', suppressByFields.join('')); - getDetails(SUPPRESS_FOR_DETAILS).should('have.text', 'One rule execution'); - getDetails(SUPPRESS_MISSING_FIELD).should( - 'have.text', - 'Suppress and group alerts for events with missing fields' - ); - - // suppression functionality should be under Tech Preview - cy.contains(DETAILS_TITLE, SUPPRESS_FOR_DETAILS).contains('Technical Preview'); + describe('when no ML jobs have run', () => { + beforeEach(() => { + mlRule = getMachineLearningRule(); + selectMachineLearningRuleType(); + fillDefineMachineLearningRule(mlRule); }); - fillAboutRuleMinimumAndContinue(mlRule); - skipScheduleRuleAction(); - createRuleWithoutEnabling(); - - cy.get(DEFINITION_DETAILS).within(() => { - getDetails(SUPPRESS_BY_DETAILS).should('have.text', suppressByFields.join('')); - getDetails(SUPPRESS_FOR_DETAILS).should('have.text', 'One rule execution'); - getDetails(SUPPRESS_MISSING_FIELD).should( - 'have.text', - 'Suppress and group alerts for events with missing fields' + it('disables the suppression fields and displays a message', () => { + cy.get(ALERT_SUPPRESSION_FIELDS_INPUT).should('be.disabled'); + cy.get(ALERT_SUPPRESSION_FIELDS_INPUT).realHover(); + cy.get(TOOLTIP).contains( + 'Machine Learning jobs must be running to enable alert suppression.' ); }); }); - it('allows a rule with interval suppression to be created and displayed', () => { - fillAlertSuppressionFields(suppressByFields); - selectAlertSuppressionPerInterval(); - setAlertSuppressionDuration(45, 'm'); - selectDoNotSuppressForMissingFields(); - continueFromDefineStep(); - - // ensures details preview works correctly - cy.get(DEFINITION_DETAILS).within(() => { - getDetails(SUPPRESS_BY_DETAILS).should('have.text', suppressByFields.join('')); - getDetails(SUPPRESS_FOR_DETAILS).should('have.text', '45m'); - getDetails(SUPPRESS_MISSING_FIELD).should( - 'have.text', - 'Do not suppress alerts for events with missing fields' - ); + describe('when ML jobs have run', () => { + before(() => { + cy.task('esArchiverLoad', { archiveName: '../auditbeat/hosts', type: 'ftr' }); + executeSetupModuleRequest({ moduleName: 'security_linux_v3' }); + forceStartDatafeeds({ jobIds: [jobId] }); + cy.task('esArchiverLoad', { archiveName: 'anomalies', type: 'ftr' }); + }); - // suppression functionality should be under Tech Preview - cy.contains(DETAILS_TITLE, SUPPRESS_FOR_DETAILS).contains('Technical Preview'); + after(() => { + cy.task('esArchiverUnload', { archiveName: 'anomalies', type: 'ftr' }); + cy.task('esArchiverUnload', { archiveName: '../auditbeat/hosts', type: 'ftr' }); }); - fillAboutRuleMinimumAndContinue(mlRule); - skipScheduleRuleAction(); - createRuleWithoutEnabling(); + describe('when not all jobs are running', () => { + beforeEach(() => { + mlRule = getMachineLearningRule(); + selectMachineLearningRuleType(); + fillDefineMachineLearningRule(mlRule); + }); - cy.get(DEFINITION_DETAILS).within(() => { - getDetails(SUPPRESS_BY_DETAILS).should('have.text', suppressByFields.join('')); - getDetails(SUPPRESS_FOR_DETAILS).should('have.text', '45m'); - getDetails(SUPPRESS_MISSING_FIELD).should( - 'have.text', - 'Do not suppress alerts for events with missing fields' - ); + it.only('displays a warning message on the suppression fields', () => { + cy.get(ALERT_SUPPRESSION_FIELDS_INPUT).should('be.enabled'); + cy.get(ALERT_SUPPRESSION_FIELDS_INPUT).should( + 'have.attr', + 'title', + 'Alert suppression will be disabled until the Machine Learning jobs are run.' + ); + }); + }); + + describe('when all jobs are running', () => { + beforeEach(() => { + mlRule = getMachineLearningRule({ machine_learning_job_id: [jobId] }); + selectMachineLearningRuleType(); + fillDefineMachineLearningRule(mlRule); + }); + + it('allows a rule with per-execution suppression to be created and displayed', () => { + fillAlertSuppressionFields(suppressByFields); + continueFromDefineStep(); + + // ensures details preview works correctly + cy.get(DEFINITION_DETAILS).within(() => { + getDetails(SUPPRESS_BY_DETAILS).should('have.text', suppressByFields.join('')); + getDetails(SUPPRESS_FOR_DETAILS).should('have.text', 'One rule execution'); + getDetails(SUPPRESS_MISSING_FIELD).should( + 'have.text', + 'Suppress and group alerts for events with missing fields' + ); + + // suppression functionality should be under Tech Preview + cy.contains(DETAILS_TITLE, SUPPRESS_FOR_DETAILS).contains('Technical Preview'); + }); + + fillAboutRuleMinimumAndContinue(mlRule); + skipScheduleRuleAction(); + createRuleWithoutEnabling(); + + cy.get(DEFINITION_DETAILS).within(() => { + getDetails(SUPPRESS_BY_DETAILS).should('have.text', suppressByFields.join('')); + getDetails(SUPPRESS_FOR_DETAILS).should('have.text', 'One rule execution'); + getDetails(SUPPRESS_MISSING_FIELD).should( + 'have.text', + 'Suppress and group alerts for events with missing fields' + ); + }); + }); + + it('allows a rule with interval suppression to be created and displayed', () => { + fillAlertSuppressionFields(suppressByFields); + selectAlertSuppressionPerInterval(); + setAlertSuppressionDuration(45, 'm'); + selectDoNotSuppressForMissingFields(); + continueFromDefineStep(); + + // ensures details preview works correctly + cy.get(DEFINITION_DETAILS).within(() => { + getDetails(SUPPRESS_BY_DETAILS).should('have.text', suppressByFields.join('')); + getDetails(SUPPRESS_FOR_DETAILS).should('have.text', '45m'); + getDetails(SUPPRESS_MISSING_FIELD).should( + 'have.text', + 'Do not suppress alerts for events with missing fields' + ); + + // suppression functionality should be under Tech Preview + cy.contains(DETAILS_TITLE, SUPPRESS_FOR_DETAILS).contains('Technical Preview'); + }); + + fillAboutRuleMinimumAndContinue(mlRule); + skipScheduleRuleAction(); + createRuleWithoutEnabling(); + + cy.get(DEFINITION_DETAILS).within(() => { + getDetails(SUPPRESS_BY_DETAILS).should('have.text', suppressByFields.join('')); + getDetails(SUPPRESS_FOR_DETAILS).should('have.text', '45m'); + getDetails(SUPPRESS_MISSING_FIELD).should( + 'have.text', + 'Do not suppress alerts for events with missing fields' + ); + }); + }); }); }); }); diff --git a/x-pack/test/security_solution_cypress/cypress/objects/rule.ts b/x-pack/test/security_solution_cypress/cypress/objects/rule.ts index 04ba983664952..ff928d9130ee4 100644 --- a/x-pack/test/security_solution_cypress/cypress/objects/rule.ts +++ b/x-pack/test/security_solution_cypress/cypress/objects/rule.ts @@ -361,6 +361,7 @@ export const getMachineLearningRule = ( ): MachineLearningRuleCreateProps => ({ type: 'machine_learning', machine_learning_job_id: [ + // TODO these are not valid job IDs 'Unusual Linux Network Activity', 'Anomalous Process for a Linux Population', ], diff --git a/x-pack/test/security_solution_cypress/cypress/support/machine_learning.ts b/x-pack/test/security_solution_cypress/cypress/support/machine_learning.ts new file mode 100644 index 0000000000000..429ea5564d219 --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/support/machine_learning.ts @@ -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 { rootRequest } from '../tasks/api_calls/common'; + +/** + * + * Calls the internal ML Module API to set up a module, which installs the jobs + * contained in that module. + * @param moduleName the name of the ML module to set up + * @returns the response from the setup module request + */ +export const executeSetupModuleRequest = ({ moduleName }: { moduleName: string }) => + rootRequest({ + headers: { + 'elastic-api-version': 1, + }, + method: 'POST', + url: `/internal/ml/modules/setup/${moduleName}`, + failOnStatusCode: true, + body: { + prefix: '', + groups: ['auditbeat'], + indexPatternName: 'auditbeat-*', + startDatafeed: false, + useDedicatedIndex: true, + applyToAllSpaces: true, + }, + }); + +/** + * + * Calls the internal ML Jobs API to force start the datafeeds for the given job IDs. Necessary to get them in the "started" state for the purposes of the detection engine + * @param jobIds the job IDs for which to force start datafeeds + * @returns the response from the force start datafeeds request + */ +export const forceStartDatafeeds = ({ jobIds }: { jobIds: string[] }) => + rootRequest({ + headers: { + 'elastic-api-version': 1, + }, + method: 'POST', + url: '/internal/ml/jobs/force_start_datafeeds', + failOnStatusCode: true, + body: { + datafeedIds: jobIds.map((jobId) => `datafeed-${jobId}`), + start: new Date().getUTCMilliseconds(), + }, + }); From eefa1c01175e8192bd3a3e98e8c764ba5853c1eb Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 12 Jun 2024 17:04:38 -0500 Subject: [PATCH 06/21] Install test ML jobs with a known/compatible group Specifying the `groups` parameter when using the "setup module" API causes the corresponding jobs to be installed _with only the specified group_. This meant that in our FTR tests, we have been installing jobs with the `auditbeat` group. However, part of the contract between ML and Detection Engine is that we use the `group` parameter to determine relevance: if it doesn't belong to either the `security` or (legacy) `siem` group(s), it effectively does not exist to the Detection Engine. This fixes the (very confusing) issue of jobs being installed but not recognized, by specifying a recognized group id (and using our shared constant for it), both in the FTR and cypress utilities. --- .../utils/machine_learning/machine_learning_setup.ts | 3 ++- .../cypress/support/machine_learning.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/machine_learning/machine_learning_setup.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/machine_learning/machine_learning_setup.ts index a9b9bf1c8ce5b..fa0c6fa4f78b5 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/machine_learning/machine_learning_setup.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/machine_learning/machine_learning_setup.ts @@ -6,6 +6,7 @@ */ import type SuperTest from 'supertest'; +import { ML_GROUP_ID } from '@kbn/security-solution-plugin/common/constants'; import { getCommonRequestHeader } from '../../../../../functional/services/ml/common_api'; export const executeSetupModuleRequest = async ({ @@ -22,7 +23,7 @@ export const executeSetupModuleRequest = async ({ .set(getCommonRequestHeader('1')) .send({ prefix: '', - groups: ['auditbeat'], + groups: [ML_GROUP_ID], indexPatternName: 'auditbeat-*', startDatafeed: false, useDedicatedIndex: true, diff --git a/x-pack/test/security_solution_cypress/cypress/support/machine_learning.ts b/x-pack/test/security_solution_cypress/cypress/support/machine_learning.ts index 429ea5564d219..8be5799409723 100644 --- a/x-pack/test/security_solution_cypress/cypress/support/machine_learning.ts +++ b/x-pack/test/security_solution_cypress/cypress/support/machine_learning.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ML_GROUP_ID } from '@kbn/security-solution-plugin/common/constants'; import { rootRequest } from '../tasks/api_calls/common'; /** @@ -24,7 +25,7 @@ export const executeSetupModuleRequest = ({ moduleName }: { moduleName: string } failOnStatusCode: true, body: { prefix: '', - groups: ['auditbeat'], + groups: [ML_GROUP_ID], indexPatternName: 'auditbeat-*', startDatafeed: false, useDedicatedIndex: true, From cf5ceae987e7a70ebf6b3f9aebc5f99ebc3e55fb Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 12 Jun 2024 17:09:20 -0500 Subject: [PATCH 07/21] Use correct job ID in cypress tests I have seen the _ecs prefix in a few places, but I'm not quite sure if it's actually part of official ML naming or not. Regardless, using the incorrect name caused the "start datafeed" request to fail with a "no datafeed for job ID" error. --- .../rule_creation/machine_learning_rule_suppression.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule_suppression.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule_suppression.cy.ts index 4508028863eda..347ad5820d75c 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule_suppression.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule_suppression.cy.ts @@ -52,7 +52,7 @@ describe( }, () => { let mlRule: ReturnType; - const jobId = 'v3_linux_anomalous_network_activity_ecs'; + const jobId = 'v3_linux_anomalous_network_activity'; const suppressByFields = ['agent.name', 'host.name']; beforeEach(() => { From 87c079d633104e1342c6f11687b556e08961dfa5 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 12 Jun 2024 17:13:32 -0500 Subject: [PATCH 08/21] Replace duplicated, magic-stringed function for existing one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The existing one also references the shared constants for our group IDs, so 👍. --- .../ml_popover/hooks/use_security_jobs_helpers.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs_helpers.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs_helpers.tsx index 8d0b63d8b32fe..567d7e038b5ad 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs_helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs_helpers.tsx @@ -6,6 +6,7 @@ */ import type { MlSummaryJob } from '@kbn/ml-plugin/public'; +import { isSecurityJob } from '../../../../../common/machine_learning/is_security_job'; import type { AugmentedSecurityJobFields, Module, @@ -111,13 +112,11 @@ export const getInstalledJobs = ( moduleJobs: SecurityJob[], compatibleModuleIds: string[] ): SecurityJob[] => - jobSummaryData - .filter(({ groups }) => groups.includes('siem') || groups.includes('security')) - .map((jobSummary) => ({ - ...jobSummary, - ...getAugmentedFields(jobSummary.id, moduleJobs, compatibleModuleIds), - isInstalled: true, - })); + jobSummaryData.filter(isSecurityJob).map((jobSummary) => ({ + ...jobSummary, + ...getAugmentedFields(jobSummary.id, moduleJobs, compatibleModuleIds), + isInstalled: true, + })); /** * Combines installed jobs + moduleSecurityJobs that don't overlap and sorts by name asc From cf71e6dee95f924fccf0454104050c79d89f3e7d Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 12 Jun 2024 18:24:08 -0500 Subject: [PATCH 09/21] ML Suppression Cypress tests are green * Cleans up debugging logs * Adds helper for ensuring that jobs are not started at beginning of suite * Fixes form filling utility to support single values for machine_learning_job_id * Updates suppression fields now that we're actually using real fields from anomaly indices --- .../components/step_define_rule/index.tsx | 12 ----------- .../machine_learning_rule_suppression.cy.ts | 21 ++++++++++++------- .../cypress/support/machine_learning.ts | 18 ++++++++++++++++ .../cypress/tasks/create_new_rule.ts | 5 ++--- 4 files changed, 34 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx index efa57563e7d66..a74847a9e61fb 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx @@ -221,18 +221,6 @@ const StepDefineRuleComponent: FC = ({ [mlFields] ); - console.log( - JSON.stringify({ - installedMlJobs, - ruleMlJobs, - numberOfRuleMlJobsStarted, - noMlJobsStarted, - someMlJobsStarted, - mlFields, - mlSuppressionFields, - }) - ); - const esqlQueryRef = useRef(undefined); const isAlertSuppressionLicenseValid = license.isAtLeast(MINIMUM_LICENSE_FOR_SUPPRESSION); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule_suppression.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule_suppression.cy.ts index 347ad5820d75c..62d1cbb8260ad 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule_suppression.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule_suppression.cy.ts @@ -7,7 +7,10 @@ import { getMachineLearningRule } from '../../../../objects/rule'; import { TOOLTIP } from '../../../../screens/common'; -import { ALERT_SUPPRESSION_FIELDS_INPUT } from '../../../../screens/create_new_rule'; +import { + ALERT_SUPPRESSION_FIELDS, + ALERT_SUPPRESSION_FIELDS_INPUT, +} from '../../../../screens/create_new_rule'; import { DEFINITION_DETAILS, DETAILS_TITLE, @@ -18,6 +21,7 @@ import { import { executeSetupModuleRequest, forceStartDatafeeds, + stopDatafeeds, } from '../../../../support/machine_learning'; import { continueFromDefineStep, @@ -53,7 +57,7 @@ describe( () => { let mlRule: ReturnType; const jobId = 'v3_linux_anomalous_network_activity'; - const suppressByFields = ['agent.name', 'host.name']; + const suppressByFields = ['by_field_name', 'by_field_value']; beforeEach(() => { login(); @@ -62,6 +66,10 @@ describe( describe('with Alert Suppression', () => { describe('when no ML jobs have run', () => { + before(() => { + stopDatafeeds({ jobIds: [jobId] }); + }); + beforeEach(() => { mlRule = getMachineLearningRule(); selectMachineLearningRuleType(); @@ -97,12 +105,11 @@ describe( fillDefineMachineLearningRule(mlRule); }); - it.only('displays a warning message on the suppression fields', () => { + it('displays a warning message on the suppression fields', () => { cy.get(ALERT_SUPPRESSION_FIELDS_INPUT).should('be.enabled'); - cy.get(ALERT_SUPPRESSION_FIELDS_INPUT).should( - 'have.attr', - 'title', - 'Alert suppression will be disabled until the Machine Learning jobs are run.' + cy.get(ALERT_SUPPRESSION_FIELDS).should( + 'contain.text', + 'This list of fields may be incomplete as some Machine Learning jobs are not running.' ); }); }); diff --git a/x-pack/test/security_solution_cypress/cypress/support/machine_learning.ts b/x-pack/test/security_solution_cypress/cypress/support/machine_learning.ts index 8be5799409723..f12338fb56c1a 100644 --- a/x-pack/test/security_solution_cypress/cypress/support/machine_learning.ts +++ b/x-pack/test/security_solution_cypress/cypress/support/machine_learning.ts @@ -52,3 +52,21 @@ export const forceStartDatafeeds = ({ jobIds }: { jobIds: string[] }) => start: new Date().getUTCMilliseconds(), }, }); + +/** + * Calls the internal ML Jobs API to stop the datafeeds for the given job IDs. + * @param jobIds the job IDs for which to stop datafeeds + * @returns the response from the stop datafeeds request + */ +export const stopDatafeeds = ({ jobIds }: { jobIds: string[] }) => + rootRequest({ + headers: { + 'elastic-api-version': 1, + }, + method: 'POST', + url: '/internal/ml/jobs/stop_datafeeds', + failOnStatusCode: true, + body: { + datafeedIds: jobIds.map((jobId) => `datafeed-${jobId}`), + }, + }); diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts b/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts index 3927d5e19e64b..6f84efc4abbdc 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts @@ -808,12 +808,11 @@ export const fillDefineMachineLearningRule = (rule: MachineLearningRuleCreatePro : [rule.machine_learning_job_id]; const text = jobsAsArray .map((machineLearningJob) => `${machineLearningJob}{downArrow}{enter}`) - .join(''); + .join('') + .concat('{esc}'); cy.get(MACHINE_LEARNING_DROPDOWN_INPUT).click({ force: true }); cy.get(MACHINE_LEARNING_DROPDOWN_INPUT).type(text); - cy.get(MACHINE_LEARNING_DROPDOWN_INPUT).type('{esc}'); - cy.get(ANOMALY_THRESHOLD_INPUT).type(`{selectall}${rule.anomaly_threshold}`, { force: true, }); From 65e16e8d32b5117ad3caccd2fd862a2888f8e72e Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 17 Jun 2024 17:00:10 -0500 Subject: [PATCH 10/21] More direct copy for user action Co-authored-by: Nastasha Solomon <79124755+nastasha-solomon@users.noreply.github.com> --- .../components/step_define_rule/translations.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/translations.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/translations.tsx index 264a1f45f12a2..5dc4a70b565d6 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/translations.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/translations.tsx @@ -237,7 +237,7 @@ export const EQL_SEQUENCE_SUPPRESSION_GROUPBY_VALIDATION_TEXT = i18n.translate( export const MACHINE_LEARNING_SUPPRESSION_DISABLED_LABEL = i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.machineLearningSuppressionDisabledLabel', { - defaultMessage: 'Machine Learning jobs must be running to enable alert suppression.', + defaultMessage: 'To enable alert suppression, start one or more Machine Learning jobs.', } ); From e6d85e2b91b5c787db1d1a34bf78eee0229e88c6 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 17 Jun 2024 23:19:53 -0500 Subject: [PATCH 11/21] Update ML warning copy per docs' suggestions Co-authored-by: Nastasha Solomon <79124755+nastasha-solomon@users.noreply.github.com> --- .../components/step_define_rule/translations.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/translations.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/translations.tsx index 5dc4a70b565d6..73eb7a4d9723f 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/translations.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/translations.tsx @@ -245,7 +245,7 @@ export const MACHINE_LEARNING_SUPPRESSION_INCOMPLETE_LABEL = i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.machineLearningSuppressionIncompleteLabel', { defaultMessage: - 'This list of fields may be incomplete as some Machine Learning jobs are not running.', + 'This list of fields might be incomplete as some Machine Learning jobs are not running. Start more jobs to add more fields.', } ); From 9c2d143a76f59b4ceee21b66244dd5239eea69ac Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 17 Jun 2024 17:03:51 -0500 Subject: [PATCH 12/21] Replace display names with actual Job IDs The way the cypress helper that consumes these is written, both of these forms work, but we're never going to encounter a rule with the display name params, and knowing the display name but not the ID is not useful for investigative purposes. I can see how this might have been done to prevent needing to change these jobs as their IDs change, but I think it's more likely that those will change than their IDs. --- .../test/security_solution_cypress/cypress/objects/rule.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/test/security_solution_cypress/cypress/objects/rule.ts b/x-pack/test/security_solution_cypress/cypress/objects/rule.ts index ff928d9130ee4..50e358515d922 100644 --- a/x-pack/test/security_solution_cypress/cypress/objects/rule.ts +++ b/x-pack/test/security_solution_cypress/cypress/objects/rule.ts @@ -361,9 +361,8 @@ export const getMachineLearningRule = ( ): MachineLearningRuleCreateProps => ({ type: 'machine_learning', machine_learning_job_id: [ - // TODO these are not valid job IDs - 'Unusual Linux Network Activity', - 'Anomalous Process for a Linux Population', + 'v3_linux_anomalous_network_activity', + 'v3_linux_anomalous_process_all_hosts', ], anomaly_threshold: 20, name: 'New ML Rule Test', From 1aa170ab390a00a36d284363dadf51d8d51f8914 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 17 Jun 2024 18:13:31 -0500 Subject: [PATCH 13/21] Extract ML rule validation logic into hook There's not a lot here, but I feel bad for adding anything to step_define_rule so this is an attempt to minimize that. In the course of refactoring I also caught a bug (perhaps just a test environment one) where the form fields are temporarily `undefined` when the hooks are run. I updated the form type to reflect this; hopefully that doesn't have broader impact (but if it does, those are probably also uncaught bugs). --- .../ml/hooks/use_ml_rule_validations.test.ts | 102 ++++++++++++++++++ .../ml/hooks/use_ml_rule_validations.ts | 41 +++++++ .../common/components/ml_popover/api.mock.ts | 10 ++ .../components/step_define_rule/index.tsx | 13 +-- .../pages/detection_engine/rules/types.ts | 2 +- 5 files changed, 158 insertions(+), 10 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/hooks/use_ml_rule_validations.test.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/hooks/use_ml_rule_validations.ts diff --git a/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_ml_rule_validations.test.ts b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_ml_rule_validations.test.ts new file mode 100644 index 0000000000000..6f14d6fe2a736 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_ml_rule_validations.test.ts @@ -0,0 +1,102 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../mock'; +import { buildMockJobsSummary, getJobsSummaryResponseMock } from '../../ml_popover/api.mock'; +import { useInstalledSecurityJobs } from './use_installed_security_jobs'; + +import { useMlRuleValidations } from './use_ml_rule_validations'; + +jest.mock('./use_installed_security_jobs'); + +describe('useMlRuleValidations', () => { + const machineLearningJobId = ['test_job', 'test_job_2']; + + beforeEach(() => { + (useInstalledSecurityJobs as jest.Mock).mockReturnValue({ + loading: true, + jobs: [], + }); + }); + + it('returns loading state from inner hook', () => { + const { result, rerender } = renderHook(() => useMlRuleValidations({ machineLearningJobId }), { + wrapper: TestProviders, + }); + expect(result.current).toEqual(expect.objectContaining({ loading: true })); + + (useInstalledSecurityJobs as jest.Mock).mockReturnValueOnce({ + loading: false, + jobs: [], + }); + + rerender(); + + expect(result.current).toEqual(expect.objectContaining({ loading: false })); + }); + + it('returns no jobs started when no jobs are started', () => { + const { result } = renderHook(() => useMlRuleValidations({ machineLearningJobId }), { + wrapper: TestProviders, + }); + + expect(result.current).toEqual( + expect.objectContaining({ noJobsStarted: true, someJobsStarted: false }) + ); + }); + + it('returns some jobs started when some jobs are started', () => { + (useInstalledSecurityJobs as jest.Mock).mockReturnValueOnce({ + loading: false, + jobs: getJobsSummaryResponseMock([ + buildMockJobsSummary({ + id: machineLearningJobId[0], + jobState: 'opened', + datafeedState: 'started', + }), + buildMockJobsSummary({ + id: machineLearningJobId[1], + }), + ]), + }); + + const { result } = renderHook(() => useMlRuleValidations({ machineLearningJobId }), { + wrapper: TestProviders, + }); + + expect(result.current).toEqual( + expect.objectContaining({ noJobsStarted: false, someJobsStarted: true }) + ); + }); + + it('returns neither "no jobs started" nor "some jobs started" when all jobs are started', () => { + (useInstalledSecurityJobs as jest.Mock).mockReturnValueOnce({ + loading: false, + jobs: getJobsSummaryResponseMock([ + buildMockJobsSummary({ + id: machineLearningJobId[0], + jobState: 'opened', + datafeedState: 'started', + }), + buildMockJobsSummary({ + id: machineLearningJobId[1], + jobState: 'opened', + datafeedState: 'started', + }), + ]), + }); + + const { result } = renderHook(() => useMlRuleValidations({ machineLearningJobId }), { + wrapper: TestProviders, + }); + + expect(result.current).toEqual( + expect.objectContaining({ noJobsStarted: false, someJobsStarted: false }) + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_ml_rule_validations.ts b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_ml_rule_validations.ts new file mode 100644 index 0000000000000..81897c5d29b82 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_ml_rule_validations.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isJobStarted } from '../../../../../common/machine_learning/helpers'; +import { useInstalledSecurityJobs } from './use_installed_security_jobs'; + +export interface UseMlRuleValidationsParams { + machineLearningJobId: string[] | undefined; +} + +export interface UseMlRuleValidationsReturn { + loading: boolean; + noJobsStarted: boolean; + someJobsStarted: boolean; +} + +/** + * Hook to encapsulate some of our validation checks for ML rules. + * + * @param machineLearningJobId the ML Job IDs of the rule + * @returns validation state about the rule, relative to its ML jobs. + */ +export const useMlRuleValidations = ({ + machineLearningJobId, +}: UseMlRuleValidationsParams): UseMlRuleValidationsReturn => { + const { jobs: installedJobs, loading } = useInstalledSecurityJobs(); + const ruleMlJobs = installedJobs.filter((installedJob) => + (machineLearningJobId ?? []).includes(installedJob.id) + ); + const numberOfRuleMlJobsStarted = ruleMlJobs.filter((job) => + isJobStarted(job.jobState, job.datafeedState) + ).length; + const noMlJobsStarted = numberOfRuleMlJobsStarted === 0; + const someMlJobsStarted = !noMlJobsStarted && numberOfRuleMlJobsStarted !== ruleMlJobs.length; + + return { loading, noJobsStarted: noMlJobsStarted, someJobsStarted: someMlJobsStarted }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/api.mock.ts b/x-pack/plugins/security_solution/public/common/components/ml_popover/api.mock.ts index 2000db1807cbf..fdd9d66ebaf90 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/api.mock.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/api.mock.ts @@ -100,6 +100,16 @@ export const mockJobsSummaryResponse: MlSummaryJob[] = [ }, ]; +export const getJobsSummaryResponseMock = (additionalJobs: MlSummaryJob[]): MlSummaryJob[] => [ + ...mockJobsSummaryResponse, + ...additionalJobs, +]; + +export const buildMockJobsSummary = (overrides: Partial): MlSummaryJob => ({ + ...mockJobsSummaryResponse[0], + ...overrides, +}); + export const mockGetModuleResponse: Module[] = [ { id: 'security_linux_v3', diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx index a74847a9e61fb..28cfc9918afdd 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx @@ -35,7 +35,7 @@ import type { DataViewBase } from '@kbn/es-query'; import { FormattedMessage } from '@kbn/i18n-react'; import { useSetFieldValueWithCallback } from '../../../../common/utils/use_set_field_value_cb'; import { useRuleFromTimeline } from '../../../../detections/containers/detection_engine/rules/use_rule_from_timeline'; -import { isJobStarted, isMlRule } from '../../../../../common/machine_learning/helpers'; +import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { hasMlAdminPermissions } from '../../../../../common/machine_learning/has_ml_admin_permissions'; import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license'; import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml_capabilities'; @@ -93,7 +93,7 @@ import { NewTermsFields } from '../new_terms_fields'; import { ScheduleItem } from '../../../rule_creation/components/schedule_item_form'; import { RequiredFields } from '../../../rule_creation/components/required_fields'; import { DocLink } from '../../../../common/components/links_to_docs/doc_link'; -import { useInstalledSecurityJobs } from '../../../../common/components/ml/hooks/use_installed_security_jobs'; +import { useMlRuleValidations } from '../../../../common/components/ml/hooks/use_ml_rule_validations'; import { defaultCustomQuery } from '../../../../detections/pages/detection_engine/rules/utils'; import { MultiSelectFieldsAutocomplete } from '../multi_select_fields'; import { useLicense } from '../../../../common/hooks/use_license'; @@ -204,17 +204,12 @@ const StepDefineRuleComponent: FC = ({ const license = useLicense(); const mlCapabilities = useMlCapabilities(); - const installedMlJobs = useInstalledSecurityJobs(); const [{ machineLearningJobId }] = useFormData({ form, watch: ['machineLearningJobId'], }); - const ruleMlJobs = installedMlJobs.jobs.filter((job) => machineLearningJobId.includes(job.id)); - const numberOfRuleMlJobsStarted = ruleMlJobs.filter((job) => - isJobStarted(job.jobState, job.datafeedState) - ).length; - const noMlJobsStarted = numberOfRuleMlJobsStarted === 0; - const someMlJobsStarted = !noMlJobsStarted && numberOfRuleMlJobsStarted !== ruleMlJobs.length; + const { someJobsStarted: someMlJobsStarted, noJobsStarted: noMlJobsStarted } = + useMlRuleValidations({ machineLearningJobId }); const { loading: mlFieldsLoading, fields: mlFields } = useRuleFields({ machineLearningJobId }); const mlSuppressionFields = useMemo( () => getTermsAggregationFields(mlFields as BrowserField[]), diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts index 1bf915e1a122f..2da396084612c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts @@ -142,7 +142,7 @@ export interface DefineStepRule { anomalyThreshold: number; index: string[]; indexPattern?: DataViewBase; - machineLearningJobId: string[]; + machineLearningJobId: string[] | undefined; queryBar: FieldValueQueryBar; dataViewId?: string; dataViewTitle?: string; From fedb7554afcf8c328cbd6b18eb1a73c45d8ea51b Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 17 Jun 2024 21:09:09 -0500 Subject: [PATCH 14/21] Relax requirement on number of rules The combination of shared state and retry logic means that asserting exactly 1 rule exists will never work if rule creation succeeds in a previous step. If we instead assert that there is _at least_ the expected number of rules, we have a chance of the retry working. --- .../rule_creation/machine_learning_rule.cy.ts | 4 +--- .../cypress/tasks/alerts_detection_rules.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule.cy.ts index 9121e524ddbb4..fde200810f44b 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule.cy.ts @@ -75,9 +75,7 @@ describe('Machine Learning rules', { tags: ['@ess', '@serverless'] }, () => { createAndEnableRule(); openRuleManagementPageViaBreadcrumbs(); - cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); - - expectNumberOfRules(RULES_MANAGEMENT_TABLE, expectedNumberOfRules); + expectNumberOfRules(RULES_MANAGEMENT_TABLE, 1); cy.get(RULE_NAME).should('have.text', mlRule.name); cy.get(RISK_SCORE).should('have.text', mlRule.risk_score); diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/alerts_detection_rules.ts b/x-pack/test/security_solution_cypress/cypress/tasks/alerts_detection_rules.ts index d5eeef7801d1b..388b58695b417 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/alerts_detection_rules.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/alerts_detection_rules.ts @@ -367,7 +367,7 @@ export const expectNumberOfRules = ( expectedNumber: number ) => { cy.log(`Expecting rules table to contain #${expectedNumber} rules`); - cy.get(tableSelector).find(RULES_ROW).should('have.length', expectedNumber); + cy.get(tableSelector).find(RULES_ROW).its('length').should('be.gte', expectedNumber); }; export const expectToContainRule = ( From 61d24d4092fb529211335a5b7128c02b2ce09b4d Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 17 Jun 2024 21:10:30 -0500 Subject: [PATCH 15/21] Clean up existing ML cypress test * Stop datafeeds before creating rule * Simplify jobId logic --- .../rule_creation/machine_learning_rule.cy.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule.cy.ts index fde200810f44b..b9a12797db51d 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule.cy.ts @@ -53,13 +53,22 @@ import { login } from '../../../../tasks/login'; import { visit } from '../../../../tasks/navigation'; import { openRuleManagementPageViaBreadcrumbs } from '../../../../tasks/rules_management'; import { CREATE_RULE_URL } from '../../../../urls/navigation'; +import { stopDatafeeds } from '../../../../support/machine_learning'; describe('Machine Learning rules', { tags: ['@ess', '@serverless'] }, () => { const expectedUrls = (getMachineLearningRule().references ?? []).join(''); const expectedFalsePositives = (getMachineLearningRule().false_positives ?? []).join(''); const expectedTags = (getMachineLearningRule().tags ?? []).join(''); const expectedMitre = formatMitreAttackDescription(getMachineLearningRule().threat ?? []); - const expectedNumberOfRules = 1; + + const machineLearningJobIds = ([] as string[]).concat( + getMachineLearningRule().machine_learning_job_id + ); + + // ensure no ML datafeeds are started before the test + before(() => { + stopDatafeeds({ jobIds: machineLearningJobIds }); + }); beforeEach(() => { login(); @@ -102,15 +111,12 @@ describe('Machine Learning rules', { tags: ['@ess', '@serverless'] }, () => { getDetails(ANOMALY_SCORE_DETAILS).should('have.text', mlRule.anomaly_threshold); getDetails(RULE_TYPE_DETAILS).should('have.text', 'Machine Learning'); getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); - const machineLearningJobsArray = isArray(mlRule.machine_learning_job_id) - ? mlRule.machine_learning_job_id - : [mlRule.machine_learning_job_id]; // With the #1912 ML rule improvement changes we enable jobs on rule creation. // Though, in cypress jobs enabling does not work reliably and job can be started or stopped. // Thus, we disable next check till we fix the issue with enabling jobs in cypress. // Relevant ticket: https://github.com/elastic/security-team/issues/5389 // cy.get(MACHINE_LEARNING_JOB_STATUS).should('have.text', 'StoppedStopped'); - cy.get(MACHINE_LEARNING_JOB_ID).should('have.text', machineLearningJobsArray.join('')); + cy.get(MACHINE_LEARNING_JOB_ID).should('have.text', machineLearningJobIds.join('')); }); cy.get(SCHEDULE_DETAILS).within(() => { getDetails(RUNS_EVERY_DETAILS) From 99deac391c9bcbc36aa15fabf4704a7dea73d12e Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 17 Jun 2024 22:06:40 -0500 Subject: [PATCH 16/21] Bring back job display names for assertions Turns out the reason the "Job IDs" were persisted as human-readable text was so that they could be reused for assertions. I still think these should be separate, so I'm adding them back for this specific assertion. --- .../rule_creation/machine_learning_rule.cy.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule.cy.ts index b9a12797db51d..88504ccc57e32 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule.cy.ts @@ -60,6 +60,10 @@ describe('Machine Learning rules', { tags: ['@ess', '@serverless'] }, () => { const expectedFalsePositives = (getMachineLearningRule().false_positives ?? []).join(''); const expectedTags = (getMachineLearningRule().tags ?? []).join(''); const expectedMitre = formatMitreAttackDescription(getMachineLearningRule().threat ?? []); + const expectedJobText = [ + 'Unusual Linux Network Activity', + 'Anomalous Process for a Linux Population', + ].join(''); const machineLearningJobIds = ([] as string[]).concat( getMachineLearningRule().machine_learning_job_id @@ -116,7 +120,7 @@ describe('Machine Learning rules', { tags: ['@ess', '@serverless'] }, () => { // Thus, we disable next check till we fix the issue with enabling jobs in cypress. // Relevant ticket: https://github.com/elastic/security-team/issues/5389 // cy.get(MACHINE_LEARNING_JOB_STATUS).should('have.text', 'StoppedStopped'); - cy.get(MACHINE_LEARNING_JOB_ID).should('have.text', machineLearningJobIds.join('')); + cy.get(MACHINE_LEARNING_JOB_ID).should('have.text', expectedJobText); }); cy.get(SCHEDULE_DETAILS).within(() => { getDetails(RUNS_EVERY_DETAILS) From b0970bffb149296f8c30b7917b47069974d81436 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 17 Jun 2024 22:16:08 -0500 Subject: [PATCH 17/21] Re-enable ML rule edit tests * Adds necessary setup/teardown for ML integration --- .../rule_edit/machine_learning_rule.cy.ts | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/machine_learning_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/machine_learning_rule.cy.ts index 63fd4e859788f..c7ffbf5c03bee 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/machine_learning_rule.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/machine_learning_rule.cy.ts @@ -18,6 +18,11 @@ import { SUPPRESS_FOR_DETAILS, SUPPRESS_MISSING_FIELD, } from '../../../../screens/rule_details'; +import { + executeSetupModuleRequest, + forceStartDatafeeds, + stopDatafeeds, +} from '../../../../support/machine_learning'; import { editFirstRule } from '../../../../tasks/alerts_detection_rules'; import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common'; import { createRule } from '../../../../tasks/api_calls/rules'; @@ -50,23 +55,32 @@ describe( }, () => { let mlRule: ReturnType; - const suppressByFields = ['agent.name', 'host.name']; + const suppressByFields = ['by_field_name', 'by_field_value']; + const jobId = 'v3_linux_anomalous_network_activity'; + + // ensure no ML datafeeds are started before the test + before(() => { + stopDatafeeds({ jobIds: [jobId] }); + }); beforeEach(() => { login(); deleteAlertsAndRules(); + cy.task('esArchiverLoad', { archiveName: '../auditbeat/hosts', type: 'ftr' }); + executeSetupModuleRequest({ moduleName: 'security_linux_v3' }); + forceStartDatafeeds({ jobIds: [jobId] }); + cy.task('esArchiverLoad', { archiveName: 'anomalies', type: 'ftr' }); }); describe('without Alert Suppression', () => { beforeEach(() => { - mlRule = getMachineLearningRule(); + mlRule = getMachineLearningRule({ machine_learning_job_id: [jobId] }); createRule(mlRule); visit(RULES_MANAGEMENT_URL); editFirstRule(); }); - // TODO: this won't work until https://github.com/elastic/kibana/issues/183100 is addressed - it.skip('allows editing of a rule to add suppression configuration', () => { + it('allows editing of a rule to add suppression configuration', () => { fillAlertSuppressionFields(suppressByFields); selectAlertSuppressionPerInterval(); setAlertSuppressionDuration(2, 'h'); @@ -90,7 +104,7 @@ describe( describe('with Alert Suppression', () => { beforeEach(() => { mlRule = { - ...getMachineLearningRule(), + ...getMachineLearningRule({ machine_learning_job_id: [jobId] }), alert_suppression: { group_by: suppressByFields, duration: { value: 360, unit: 's' }, From 1a99fd1421bb3975bed37689e7bd6c5f7ae0ddd3 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 17 Jun 2024 22:17:07 -0500 Subject: [PATCH 18/21] Robustify ML cypress tasks 1. Use "proper" combobox text, and capture it within a helper method I swear I saw this working when I was doing the same stuff for the ML Job picker, but I must have only been dealing with one item, or the items I was selecting were somehow different. Downarrow is _required_ on the first option (a simple "enter" will select nothing), but using downarrow on subsequent options will cause the _second_ suggested item to be selected. E.g. if I type "by_field_value", it suggests both "by_field_value" and "client.by_field_value," and {downarrow}{enter} would cause the latter to be selected. 2. I also ensure that our new ML validations have run (which causes suppression fields to be disabled) before attempting to interact with the suppression fields, as this was causing some flakiness now that these checks are done async 3. I also fixed the broken `clearAlertSuppressionFields` task, which had never work but also had never been exercised since the relevant test was skipped. --- .../cypress/tasks/create_new_rule.ts | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts b/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts index 6f84efc4abbdc..5235dd11d0066 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts @@ -806,12 +806,8 @@ export const fillDefineMachineLearningRule = (rule: MachineLearningRuleCreatePro const jobsAsArray = isArray(rule.machine_learning_job_id) ? rule.machine_learning_job_id : [rule.machine_learning_job_id]; - const text = jobsAsArray - .map((machineLearningJob) => `${machineLearningJob}{downArrow}{enter}`) - .join('') - .concat('{esc}'); cy.get(MACHINE_LEARNING_DROPDOWN_INPUT).click({ force: true }); - cy.get(MACHINE_LEARNING_DROPDOWN_INPUT).type(text); + cy.get(MACHINE_LEARNING_DROPDOWN_INPUT).type(optionsToComboboxText(jobsAsArray)); cy.get(ANOMALY_THRESHOLD_INPUT).type(`{selectall}${rule.anomaly_threshold}`, { force: true, @@ -912,14 +908,22 @@ export const enablesAndPopulatesThresholdSuppression = ( cy.get(ALERT_SUPPRESSION_DURATION_PER_TIME_INTERVAL).should('be.enabled').should('be.checked'); }; +const optionsToComboboxText = (options: string[]) => { + const [first, ...rest] = options; + return [`${first}{downArrow}{enter}`, ...rest.map((o) => `${o}{enter}`)].join('').concat('{esc}'); +}; + export const fillAlertSuppressionFields = (fields: string[]) => { - fields.forEach((field) => { - cy.get(ALERT_SUPPRESSION_FIELDS_COMBO_BOX).type(`${field}{downArrow}{enter}`); - }); + cy.get(ALERT_SUPPRESSION_FIELDS_COMBO_BOX).should('not.be.disabled'); + cy.get(ALERT_SUPPRESSION_FIELDS_COMBO_BOX).click(); + cy.get(ALERT_SUPPRESSION_FIELDS_COMBO_BOX).type(optionsToComboboxText(fields)); }; export const clearAlertSuppressionFields = () => { - cy.get(ALERT_SUPPRESSION_FIELDS_COMBO_BOX).type('{selectall}{backspace}{enter}'); + cy.get(ALERT_SUPPRESSION_FIELDS_COMBO_BOX).should('not.be.disabled'); + cy.get(ALERT_SUPPRESSION_FIELDS).within(() => { + cy.get(COMBO_BOX_CLEAR_BTN).click(); + }); }; export const selectAlertSuppressionPerInterval = () => { From 742503bfe5430c5e50d91c7c04742d0f41c9bf66 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 17 Jun 2024 23:08:02 -0500 Subject: [PATCH 19/21] Ensure test has clean setup There were no less than four assertions in this test that relied on there being no other rules present in the environment, but nothing was being done to ensure that was the case. I can't imagine why these were skipped! --- .../rule_creation/machine_learning_rule.cy.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule.cy.ts index 88504ccc57e32..87c0ccab6a02b 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/machine_learning_rule.cy.ts @@ -54,6 +54,7 @@ import { visit } from '../../../../tasks/navigation'; import { openRuleManagementPageViaBreadcrumbs } from '../../../../tasks/rules_management'; import { CREATE_RULE_URL } from '../../../../urls/navigation'; import { stopDatafeeds } from '../../../../support/machine_learning'; +import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common'; describe('Machine Learning rules', { tags: ['@ess', '@serverless'] }, () => { const expectedUrls = (getMachineLearningRule().references ?? []).join(''); @@ -69,9 +70,10 @@ describe('Machine Learning rules', { tags: ['@ess', '@serverless'] }, () => { getMachineLearningRule().machine_learning_job_id ); - // ensure no ML datafeeds are started before the test before(() => { + // ensure no ML datafeeds are started before the suite stopDatafeeds({ jobIds: machineLearningJobIds }); + deleteAlertsAndRules(); }); beforeEach(() => { From f5cbaa5b382cf1c5aaa804fdfbd01f5ca44787ba Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 17 Jun 2024 23:21:39 -0500 Subject: [PATCH 20/21] Remove exclusivity from FTR tests, add TODO for re-skipping I want to run these in the flaky runner to get a sense of how/where they're still failing, for now. --- .../execution_logic/machine_learning.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/machine_learning.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/machine_learning.ts index 8d153c01d6ac0..9042c28c3c3ee 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/machine_learning.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/machine_learning.ts @@ -86,7 +86,7 @@ export default ({ getService }: FtrProviderContext) => { rule_id: 'ml-rule-id', }; - // FLAKY: https://github.com/elastic/kibana/issues/171426 + // TODO FLAKY: https://github.com/elastic/kibana/issues/171426 describe('@ess @serverless @serverlessQA Machine learning type rules', () => { before(async () => { // Order is critical here: auditbeat data must be loaded before attempting to start the ML job, @@ -110,7 +110,7 @@ export default ({ getService }: FtrProviderContext) => { }); // First test creates a real rule - remaining tests use preview API - it.only('should create 1 alert from ML rule when record meets anomaly_threshold', async () => { + it('should create 1 alert from ML rule when record meets anomaly_threshold', async () => { const createdRule = await createRule(supertest, log, rule); const alerts = await getAlerts(supertest, log, es, createdRule); expect(alerts.hits.hits.length).toBe(1); From 8e9d4c50ddb5134b1b810c3999609f993b38fb44 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 17 Jun 2024 23:26:45 -0500 Subject: [PATCH 21/21] Fix suppression fields for non-ML cases We were over-eagerly disabling these fields when the ML checks were not relevant. --- .../rule_creation_ui/components/step_define_rule/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx index 28cfc9918afdd..31d7eaf4933fd 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx @@ -1097,10 +1097,10 @@ const StepDefineRuleComponent: FC = ({ areSuppressionFieldsDisabledBySequence || isEsqlSuppressionLoading || mlFieldsLoading || - noMlJobsStarted, + (noMlJobsStarted && isMlRule(ruleType)), disabledText: areSuppressionFieldsDisabledBySequence ? i18n.EQL_SEQUENCE_SUPPRESSION_DISABLE_TOOLTIP - : noMlJobsStarted + : noMlJobsStarted && isMlRule(ruleType) ? i18n.MACHINE_LEARNING_SUPPRESSION_DISABLED_LABEL : alertSuppressionUpsellingMessage, }}