[Detection Engine] Cypress cleanup and simplification (#217276)

## Summary

This PR attempts to simplify our Cypress tests to focus in on what
exactly we want a test to be doing. Many of our rule creation cypress
tests were testing rule creation, rule edit, rule details and more. This
results in a lot of flake and us triaging tests that often test things
other than what we're ultimately trying to answer.

I tried to simplify it so the rule specific tests simply answer - can we
create this rule type in the UI? Then there's a single test for checking
the entire flow of create rule -> rule details and check for alerts. The
FTRs should be ensuring that the rules generate alerts as expected so we
don't need to check this for every rule type in cypress.

I also moved alert suppression into it's own folder as there is a lot of
specific logic to test around that.
This commit is contained in:
Yara Tercero 2025-04-14 11:46:47 -07:00 committed by GitHub
parent b8e78bcd55
commit 81c93ca5d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 1249 additions and 1961 deletions

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { getNewThreatIndicatorRule } from '../../../../objects/rule';
import { getEqlRule, getNewThreatIndicatorRule } from '../../../../objects/rule';
import {
SUPPRESS_FOR_DETAILS,
@ -13,6 +13,7 @@ import {
SUPPRESS_MISSING_FIELD,
DEFINITION_DETAILS,
ALERT_SUPPRESSION_INSUFFICIENT_LICENSING_ICON,
DETAILS_TITLE,
} from '../../../../screens/rule_details';
import { startBasicLicense } from '../../../../tasks/api_calls/licensing';
@ -28,19 +29,19 @@ import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
const SUPPRESS_BY_FIELDS = ['myhash.mysha256', 'source.ip.keyword'];
describe(
'Detection rules, Indicator Match, Alert Suppression',
'Alert Suppression license check - Rule Details',
{
tags: ['@ess'],
},
() => {
describe('Create rule form', () => {
beforeEach(() => {
deleteAlertsAndRules();
login();
visit(CREATE_RULE_URL);
startBasicLicense();
});
beforeEach(() => {
deleteAlertsAndRules();
login();
visit(CREATE_RULE_URL);
startBasicLicense();
});
describe('Indicator match', () => {
it('shows upselling message on rule details with suppression on basic license', () => {
const rule = getNewThreatIndicatorRule();
@ -71,5 +72,40 @@ describe(
});
});
});
describe('EQL rule', () => {
it('shows an upselling message on rule suppression details', () => {
const rule = getEqlRule();
createRule({
...rule,
alert_suppression: {
group_by: SUPPRESS_BY_FIELDS,
duration: { value: 360, unit: 's' },
missing_fields_strategy: 'doNotSuppress',
},
}).then((createdRule) => {
visit(ruleDetailsUrl(createdRule.body.id));
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(SUPPRESS_BY_DETAILS).should('have.text', SUPPRESS_BY_FIELDS.join(''));
getDetails(SUPPRESS_FOR_DETAILS).should('have.text', '360s');
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');
});
// Platinum license is required for configuration to apply
cy.get(ALERT_SUPPRESSION_INSUFFICIENT_LICENSING_ICON).eq(2).trigger('mouseover');
cy.get(TOOLTIP).contains(
'Alert suppression is configured but will not be applied due to insufficient licensing'
);
});
});
});
}
);

View file

@ -0,0 +1,79 @@
/*
* 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 {
THRESHOLD_ENABLE_SUPPRESSION_CHECKBOX,
ALERT_SUPPRESSION_DURATION_VALUE_INPUT,
MACHINE_LEARNING_TYPE,
ALERT_SUPPRESSION_DURATION_UNIT_INPUT,
ALERT_SUPPRESSION_FIELDS_INPUT,
ALERT_SUPPRESSION_FIELDS,
} from '../../../../screens/create_new_rule';
import {
selectIndicatorMatchType,
selectNewTermsRuleType,
selectThresholdRuleType,
selectEsqlRuleType,
openSuppressionFieldsTooltipAndCheckLicense,
selectEqlRuleType,
} from '../../../../tasks/create_new_rule';
import { startBasicLicense } from '../../../../tasks/api_calls/licensing';
import { login } from '../../../../tasks/login';
import { visit } from '../../../../tasks/navigation';
import { CREATE_RULE_URL } from '../../../../urls/navigation';
import { TOOLTIP } from '../../../../screens/common';
import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
describe(
'Alert Suppression basic license check - Rule Form',
{
tags: ['@ess'],
},
() => {
beforeEach(() => {
deleteAlertsAndRules();
login();
visit(CREATE_RULE_URL);
startBasicLicense();
});
it('cannot create rule with rule execution suppression on basic license for all rules with enabled suppression', () => {
// Default query rule
openSuppressionFieldsTooltipAndCheckLicense();
selectIndicatorMatchType();
openSuppressionFieldsTooltipAndCheckLicense();
selectNewTermsRuleType();
openSuppressionFieldsTooltipAndCheckLicense();
selectEsqlRuleType();
openSuppressionFieldsTooltipAndCheckLicense();
selectEqlRuleType();
cy.get(ALERT_SUPPRESSION_FIELDS_INPUT).should('be.disabled');
cy.get(ALERT_SUPPRESSION_FIELDS).trigger('mouseover');
// Platinum license is required, tooltip on disabled alert suppression checkbox should tell this
cy.get(TOOLTIP).contains('Platinum license');
// ML Rules require Platinum license
cy.get(MACHINE_LEARNING_TYPE).get('button').should('be.disabled');
selectThresholdRuleType();
cy.get(THRESHOLD_ENABLE_SUPPRESSION_CHECKBOX).should('be.disabled');
cy.get(THRESHOLD_ENABLE_SUPPRESSION_CHECKBOX).parent().trigger('mouseover');
// Platinum license is required, tooltip on disabled alert suppression checkbox should tell this
cy.get(TOOLTIP).contains('Platinum license');
cy.get(ALERT_SUPPRESSION_DURATION_VALUE_INPUT).should('be.disabled');
cy.get(ALERT_SUPPRESSION_DURATION_UNIT_INPUT).should('be.disabled');
});
}
);

View file

@ -0,0 +1,85 @@
/*
* 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 { getNewRule } from '../../../../objects/rule';
import {
DEFINITION_DETAILS,
SUPPRESS_FOR_DETAILS,
SUPPRESS_BY_DETAILS,
SUPPRESS_MISSING_FIELD,
DETAILS_TITLE,
} from '../../../../screens/rule_details';
import { ALERT_SUPPRESSION_FIELDS } from '../../../../screens/create_new_rule';
import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
import {
fillAboutRuleMinimumAndContinue,
createRuleWithoutEnabling,
fillAlertSuppressionFields,
skipScheduleRuleAction,
continueFromDefineStep,
fillCustomQueryInput,
} from '../../../../tasks/create_new_rule';
import { login } from '../../../../tasks/login';
import { visit } from '../../../../tasks/navigation';
import { getDetails } from '../../../../tasks/rule_details';
import { CREATE_RULE_URL } from '../../../../urls/navigation';
describe('Custom Query Rule - Alert suppression', { tags: ['@ess', '@serverless'] }, () => {
const rule = getNewRule();
beforeEach(() => {
deleteAlertsAndRules();
login();
visit(CREATE_RULE_URL);
});
const SUPPRESS_BY_FIELDS = ['source.ip'];
it('creates rule with suppression', () => {
fillCustomQueryInput('*');
fillAlertSuppressionFields(SUPPRESS_BY_FIELDS);
// alert suppression fields input should not have Technical Preview label
cy.get(ALERT_SUPPRESSION_FIELDS).should('not.contain.text', 'Technical Preview');
continueFromDefineStep();
// ensures details preview works correctly
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(SUPPRESS_BY_DETAILS).should('have.text', SUPPRESS_BY_FIELDS.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 in GA
cy.contains(DETAILS_TITLE, SUPPRESS_FOR_DETAILS).should(
'not.contain.text',
'Technical Preview'
);
});
fillAboutRuleMinimumAndContinue(rule);
skipScheduleRuleAction();
createRuleWithoutEnabling();
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(SUPPRESS_BY_DETAILS).should('have.text', SUPPRESS_BY_FIELDS.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 in GA
cy.contains(DETAILS_TITLE, SUPPRESS_FOR_DETAILS).should(
'not.contain.text',
'Technical Preview'
);
});
});
});

View file

@ -36,7 +36,7 @@ const SUPPRESS_BY_FIELDS = ['agent.type'];
// Skip in MKI due to flake
describe(
'Detection Rule Creation - EQL Rules - With Alert Suppression',
'Detection EQL Rules - Alert suppression',
{
tags: ['@ess', '@skipInServerlessMKI'],
},

View file

@ -35,11 +35,9 @@ import {
const SUPPRESS_BY_FIELDS = ['agent.type'];
describe(
'Detection Rule Creation - EQL Rules - With Alert Suppression',
'EQL Rules - Alert suppression',
{
// skipped in MKI as it depends on feature flag alertSuppressionForEsqlRuleEnabled
// alertSuppressionForEsqlRuleEnabled feature flag is also enabled in a global config
tags: ['@ess', '@skipInServerlessMKI'],
tags: ['@ess', '@serverless'],
env: {
kbnServerArgs: [
`--xpack.securitySolution.enableExperimental=${JSON.stringify([

View file

@ -0,0 +1,107 @@
/*
* 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 { getEsqlRule } from '../../../../objects/rule';
import {
DEFINITION_DETAILS,
SUPPRESS_BY_DETAILS,
SUPPRESS_FOR_DETAILS,
SUPPRESS_MISSING_FIELD,
} from '../../../../screens/rule_details';
import { getDetails } from '../../../../tasks/rule_details';
import {
selectEsqlRuleType,
fillEsqlQueryBar,
createRuleWithoutEnabling,
fillAlertSuppressionFields,
selectAlertSuppressionPerInterval,
setAlertSuppressionDuration,
selectDoNotSuppressForMissingFields,
continueFromDefineStep,
fillAboutRuleMinimumAndContinue,
skipScheduleRuleAction,
interceptEsqlQueryFieldsRequest,
} from '../../../../tasks/create_new_rule';
import { login } from '../../../../tasks/login';
import { visit } from '../../../../tasks/navigation';
import { CREATE_RULE_URL } from '../../../../urls/navigation';
// https://github.com/cypress-io/cypress/issues/22113
// issue is inside monaco editor, used in ES|QL query input
// calling it after visiting page in each tests, seems fixes the issue
// the only other alternative is patching ResizeObserver, which is something I would like to avoid
const workaroundForResizeObserver = () =>
cy.on('uncaught:exception', (err) => {
if (err.message.includes('ResizeObserver loop limit exceeded')) {
return false;
}
});
describe(
'Detection ES|QL - Alert suppression',
{
tags: ['@ess', '@serverless'],
},
() => {
const rule = getEsqlRule();
beforeEach(() => {
login();
visit(CREATE_RULE_URL);
});
it('shows custom ES|QL field in investigation fields autocomplete and saves it in rule', function () {
const CUSTOM_ESQL_FIELD = '_custom_agent_name';
const SUPPRESS_BY_FIELDS = [CUSTOM_ESQL_FIELD, 'agent.type'];
const queryWithCustomFields = [
`from auditbeat* metadata _id, _version, _index`,
`eval ${CUSTOM_ESQL_FIELD} = agent.name`,
`drop agent.*`,
].join(' | ');
workaroundForResizeObserver();
selectEsqlRuleType();
interceptEsqlQueryFieldsRequest(queryWithCustomFields, 'esqlSuppressionFieldsRequest');
fillEsqlQueryBar(queryWithCustomFields);
cy.wait('@esqlSuppressionFieldsRequest');
fillAlertSuppressionFields(SUPPRESS_BY_FIELDS);
selectAlertSuppressionPerInterval();
setAlertSuppressionDuration(2, 'h');
selectDoNotSuppressForMissingFields();
continueFromDefineStep();
// ensures details preview works correctly
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(SUPPRESS_BY_DETAILS).should('have.text', SUPPRESS_BY_FIELDS.join(''));
getDetails(SUPPRESS_FOR_DETAILS).should('have.text', '2h');
getDetails(SUPPRESS_MISSING_FIELD).should(
'have.text',
'Do not suppress alerts for events with missing fields'
);
});
fillAboutRuleMinimumAndContinue(rule);
skipScheduleRuleAction();
createRuleWithoutEnabling();
// ensures rule details displayed correctly after rule created
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(SUPPRESS_BY_DETAILS).should('have.text', SUPPRESS_BY_FIELDS.join(''));
getDetails(SUPPRESS_FOR_DETAILS).should('have.text', '2h');
getDetails(SUPPRESS_MISSING_FIELD).should(
'have.text',
'Do not suppress alerts for events with missing fields'
);
});
});
}
);

View file

@ -21,7 +21,7 @@ import {
import { CREATE_RULE_URL } from '../../../../urls/navigation';
describe(
'Detection rules, Alert Suppression for Essentials tier',
'Alert Suppression - Essentials tier license check',
{
tags: ['@serverless'],
env: {

View file

@ -36,7 +36,7 @@ import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
const SUPPRESS_BY_FIELDS = ['myhash.mysha256', 'source.ip.keyword'];
describe(
'Detection rules, Indicator Match, Alert Suppression',
'Indicator Match - Alert suppression',
{
tags: ['@ess', '@serverless'],
},

View file

@ -40,7 +40,7 @@ import { getDetails } from '../../../../tasks/rule_details';
import { CREATE_RULE_URL } from '../../../../urls/navigation';
describe(
'Machine Learning Detection Rules - Creation',
'Machine Learning Detection Rules - Alert suppression',
{
tags: ['@ess', '@serverless'],
},

View file

@ -0,0 +1,86 @@
/*
* 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 { getNewTermsRule } from '../../../../objects/rule';
import {
DEFINITION_DETAILS,
SUPPRESS_BY_DETAILS,
SUPPRESS_FOR_DETAILS,
SUPPRESS_MISSING_FIELD,
} from '../../../../screens/rule_details';
import { getDetails } from '../../../../tasks/rule_details';
import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
import {
fillDefineNewTermsRule,
selectNewTermsRuleType,
fillAlertSuppressionFields,
fillAboutRuleMinimumAndContinue,
createRuleWithoutEnabling,
skipScheduleRuleAction,
continueFromDefineStep,
selectAlertSuppressionPerInterval,
setAlertSuppressionDuration,
selectDoNotSuppressForMissingFields,
} from '../../../../tasks/create_new_rule';
import { login } from '../../../../tasks/login';
import { visit } from '../../../../tasks/navigation';
import { CREATE_RULE_URL } from '../../../../urls/navigation';
describe(
'New Terms Rule - Alert suppression',
{
tags: ['@ess', '@serverless'],
},
() => {
const rule = getNewTermsRule();
beforeEach(() => {
deleteAlertsAndRules();
login();
visit(CREATE_RULE_URL);
selectNewTermsRuleType();
});
it('With time interval suppression', () => {
const SUPPRESS_BY_FIELDS = ['agent.hostname', 'agent.type'];
fillDefineNewTermsRule(rule);
// fill suppress by fields and select non-default suppression options
fillAlertSuppressionFields(SUPPRESS_BY_FIELDS);
selectAlertSuppressionPerInterval();
setAlertSuppressionDuration(45, 'm');
selectDoNotSuppressForMissingFields();
continueFromDefineStep();
// ensures details preview works correctly
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(SUPPRESS_BY_DETAILS).should('have.text', SUPPRESS_BY_FIELDS.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'
);
});
fillAboutRuleMinimumAndContinue(rule);
skipScheduleRuleAction();
createRuleWithoutEnabling();
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(SUPPRESS_BY_DETAILS).should('have.text', SUPPRESS_BY_FIELDS.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'
);
});
});
}
);

View file

@ -0,0 +1,65 @@
/*
* 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 { getNewThresholdRule } from '../../../../objects/rule';
import { DEFINITION_DETAILS, SUPPRESS_FOR_DETAILS } from '../../../../screens/rule_details';
import { goToRuleDetailsOf } from '../../../../tasks/alerts_detection_rules';
import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
import {
createRuleWithoutEnabling,
fillAboutRuleMinimumAndContinue,
enablesAndPopulatesThresholdSuppression,
skipScheduleRuleAction,
selectThresholdRuleType,
fillDefineThresholdRule,
continueFromDefineStep,
} from '../../../../tasks/create_new_rule';
import { login } from '../../../../tasks/login';
import { visit } from '../../../../tasks/navigation';
import { getDetails } from '../../../../tasks/rule_details';
import { openRuleManagementPageViaBreadcrumbs } from '../../../../tasks/rules_management';
import { CREATE_RULE_URL } from '../../../../urls/navigation';
describe(
'Threshold Rule - Alert suppression',
{
tags: ['@ess', '@serverless'],
},
() => {
const rule = getNewThresholdRule();
beforeEach(() => {
deleteAlertsAndRules();
login();
visit(CREATE_RULE_URL);
});
it('Creates a new threshold rule with suppression enabled', () => {
selectThresholdRuleType();
fillDefineThresholdRule(rule);
enablesAndPopulatesThresholdSuppression(5, 'h');
continueFromDefineStep();
// ensures duration displayed on define step in preview mode
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(SUPPRESS_FOR_DETAILS).should('have.text', '5h');
});
fillAboutRuleMinimumAndContinue(rule);
skipScheduleRuleAction();
createRuleWithoutEnabling();
openRuleManagementPageViaBreadcrumbs();
goToRuleDetailsOf(rule.name);
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(SUPPRESS_FOR_DETAILS).should('have.text', '5h');
});
});
}
);

View file

@ -43,10 +43,13 @@ import {
fillThreatSubtechnique,
fillThreatTechnique,
importSavedQuery,
waitForAlertsToPopulate,
} from '../../../../tasks/create_new_rule';
import { login } from '../../../../tasks/login';
import { CREATE_RULE_URL } from '../../../../urls/navigation';
import { visit } from '../../../../tasks/navigation';
import { waitForTheRuleToBeExecuted } from '../../../../tasks/rule_details';
import { ALERTS_COUNT, ALERT_GRID_CELL } from '../../../../screens/alerts';
// This test is meant to test touching all the common various components in rule creation
// to ensure we don't miss any changes that maybe affect one of these more obscure UI components
@ -111,5 +114,13 @@ describe('Common rule creation flows', { tags: ['@ess', '@serverless'] }, () =>
cy.get(DESCRIPTION_SETUP_GUIDE_BUTTON).click();
cy.get(DESCRIPTION_SETUP_GUIDE_CONTENT).should('contain', 'test setup markdown'); // Markdown formatting should be removed
waitForTheRuleToBeExecuted();
waitForAlertsToPopulate();
cy.get(ALERTS_COUNT)
.invoke('text')
.should('match', /^[1-9].+$/);
cy.get(ALERT_GRID_CELL).contains(ruleFields.ruleName);
});
});

View file

@ -1,71 +0,0 @@
/*
* 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 {
THRESHOLD_ENABLE_SUPPRESSION_CHECKBOX,
ALERT_SUPPRESSION_DURATION_VALUE_INPUT,
MACHINE_LEARNING_TYPE,
ALERT_SUPPRESSION_DURATION_UNIT_INPUT,
} from '../../../../screens/create_new_rule';
import {
selectIndicatorMatchType,
selectNewTermsRuleType,
selectThresholdRuleType,
selectEsqlRuleType,
openSuppressionFieldsTooltipAndCheckLicense,
} from '../../../../tasks/create_new_rule';
import { startBasicLicense } from '../../../../tasks/api_calls/licensing';
import { login } from '../../../../tasks/login';
import { visit } from '../../../../tasks/navigation';
import { CREATE_RULE_URL } from '../../../../urls/navigation';
import { TOOLTIP } from '../../../../screens/common';
import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
describe(
'Detection rules, Common flows Alert Suppression',
{
tags: ['@ess'],
},
() => {
describe('Create rule form', () => {
beforeEach(() => {
deleteAlertsAndRules();
login();
visit(CREATE_RULE_URL);
startBasicLicense();
});
it('can not create rule with rule execution suppression on basic license for all rules with enabled suppression', () => {
// Default query rule
openSuppressionFieldsTooltipAndCheckLicense();
selectIndicatorMatchType();
openSuppressionFieldsTooltipAndCheckLicense();
selectNewTermsRuleType();
openSuppressionFieldsTooltipAndCheckLicense();
selectEsqlRuleType();
openSuppressionFieldsTooltipAndCheckLicense();
// ML Rules require Platinum license
cy.get(MACHINE_LEARNING_TYPE).get('button').should('be.disabled');
selectThresholdRuleType();
cy.get(THRESHOLD_ENABLE_SUPPRESSION_CHECKBOX).should('be.disabled');
cy.get(THRESHOLD_ENABLE_SUPPRESSION_CHECKBOX).parent().trigger('mouseover');
// Platinum license is required, tooltip on disabled alert suppression checkbox should tell this
cy.get(TOOLTIP).contains('Platinum license');
cy.get(ALERT_SUPPRESSION_DURATION_VALUE_INPUT).should('be.disabled');
cy.get(ALERT_SUPPRESSION_DURATION_UNIT_INPUT).should('be.disabled');
});
});
}
);

View file

@ -6,15 +6,7 @@
*/
import { getNewRule } from '../../../../objects/rule';
import {
RULE_NAME_HEADER,
DEFINITION_DETAILS,
SUPPRESS_FOR_DETAILS,
SUPPRESS_BY_DETAILS,
SUPPRESS_MISSING_FIELD,
DETAILS_TITLE,
} from '../../../../screens/rule_details';
import { ALERT_SUPPRESSION_FIELDS } from '../../../../screens/create_new_rule';
import { RULE_NAME_HEADER } from '../../../../screens/rule_details';
import { GLOBAL_SEARCH_BAR_FILTER_ITEM } from '../../../../screens/search_bar';
import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
@ -25,100 +17,41 @@ import {
createRuleWithoutEnabling,
fillDefineCustomRule,
openAddFilterPopover,
fillAlertSuppressionFields,
skipScheduleRuleAction,
continueFromDefineStep,
fillCustomQueryInput,
} from '../../../../tasks/create_new_rule';
import { login } from '../../../../tasks/login';
import { visit } from '../../../../tasks/navigation';
import { getDetails } from '../../../../tasks/rule_details';
import { fillAddFilterForm } from '../../../../tasks/search_bar';
import { CREATE_RULE_URL } from '../../../../urls/navigation';
describe('Create custom query rule', { tags: ['@ess', '@serverless'] }, () => {
describe('Custom query rule - Rule Creation', { tags: ['@ess', '@serverless'] }, () => {
const rule = getNewRule();
beforeEach(() => {
deleteAlertsAndRules();
login();
visit(CREATE_RULE_URL);
});
describe('Custom detection rules creation', () => {
beforeEach(() => {
deleteAlertsAndRules();
login();
visit(CREATE_RULE_URL);
});
it('Creates and enables a rule', function () {
fillDefineCustomRuleAndContinue(rule);
fillAboutRuleMinimumAndContinue(rule);
fillScheduleRuleAndContinue(rule);
createRuleWithoutEnabling();
cy.log('Asserting we have a new rule created');
cy.get(RULE_NAME_HEADER).should('contain', rule.name);
});
// FLAKEY - see https://github.com/elastic/kibana/issues/182891
it('Adds filter on define step', { tags: ['@skipInServerless'] }, () => {
visit(CREATE_RULE_URL);
fillDefineCustomRule(rule);
openAddFilterPopover();
fillAddFilterForm({
key: 'host.name',
operator: 'exists',
});
// Check that newly added filter exists
cy.get(GLOBAL_SEARCH_BAR_FILTER_ITEM).should('have.text', 'host.name: exists');
});
// https://github.com/elastic/kibana/issues/187277
describe('Alert suppression', { tags: ['@skipInServerlessMKI'] }, () => {
const SUPPRESS_BY_FIELDS = ['source.ip'];
it('creates rule with suppression', () => {
fillCustomQueryInput('*');
fillAlertSuppressionFields(SUPPRESS_BY_FIELDS);
// alert suppression fields input should not have Technical Preview label
cy.get(ALERT_SUPPRESSION_FIELDS).should('not.contain.text', 'Technical Preview');
continueFromDefineStep();
// ensures details preview works correctly
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(SUPPRESS_BY_DETAILS).should('have.text', SUPPRESS_BY_FIELDS.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 in GA
cy.contains(DETAILS_TITLE, SUPPRESS_FOR_DETAILS).should(
'not.contain.text',
'Technical Preview'
);
});
fillAboutRuleMinimumAndContinue(rule);
skipScheduleRuleAction();
createRuleWithoutEnabling();
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(SUPPRESS_BY_DETAILS).should('have.text', SUPPRESS_BY_FIELDS.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 in GA
cy.contains(DETAILS_TITLE, SUPPRESS_FOR_DETAILS).should(
'not.contain.text',
'Technical Preview'
);
});
});
it('Creates and enables a rule', function () {
fillDefineCustomRuleAndContinue(rule);
fillAboutRuleMinimumAndContinue(rule);
fillScheduleRuleAndContinue(rule);
createRuleWithoutEnabling();
cy.log('Asserting we have a new rule created');
cy.get(RULE_NAME_HEADER).should('contain', rule.name);
});
// FLAKEY - see https://github.com/elastic/kibana/issues/182891
it('Adds filter on define step', { tags: ['@skipInServerless'] }, () => {
visit(CREATE_RULE_URL);
fillDefineCustomRule(rule);
openAddFilterPopover();
fillAddFilterForm({
key: 'host.name',
operator: 'exists',
});
// Check that newly added filter exists
cy.get(GLOBAL_SEARCH_BAR_FILTER_ITEM).should('have.text', 'host.name: exists');
});
});

View file

@ -5,89 +5,36 @@
* 2.0.
*/
import { formatMitreAttackDescription, getHumanizedDuration } from '../../../../helpers/rules';
import { getDataViewRule } from '../../../../objects/rule';
import { ALERTS_COUNT, ALERT_GRID_CELL } from '../../../../screens/alerts';
import {
CUSTOM_RULES_BTN,
RISK_SCORE,
RULE_NAME,
RULE_SWITCH,
SEVERITY,
} from '../../../../screens/alerts_detection_rules';
import {
ABOUT_CONTINUE_BTN,
RULE_DESCRIPTION_INPUT,
RULE_NAME_INPUT,
} from '../../../../screens/create_new_rule';
import {
ADDITIONAL_LOOK_BACK_DETAILS,
ABOUT_DETAILS,
ABOUT_INVESTIGATION_NOTES,
ABOUT_RULE_DESCRIPTION,
CUSTOM_QUERY_DETAILS,
DEFINITION_DETAILS,
FALSE_POSITIVES_DETAILS,
removeExternalLinkText,
INDEX_PATTERNS_DETAILS,
INVESTIGATION_NOTES_MARKDOWN,
INVESTIGATION_NOTES_TOGGLE,
MITRE_ATTACK_DETAILS,
REFERENCE_URLS_DETAILS,
RISK_SCORE_DETAILS,
RULE_NAME_HEADER,
RULE_TYPE_DETAILS,
RUNS_EVERY_DETAILS,
SCHEDULE_DETAILS,
SEVERITY_DETAILS,
TAGS_DETAILS,
TIMELINE_TEMPLATE_DETAILS,
DATA_VIEW_DETAILS,
EDIT_RULE_SETTINGS_LINK,
INTERVAL_ABBR_VALUE,
} from '../../../../screens/rule_details';
import { RULE_NAME_HEADER } from '../../../../screens/rule_details';
import { GLOBAL_SEARCH_BAR_FILTER_ITEM } from '../../../../screens/search_bar';
import {
getRulesManagementTableRows,
goToRuleDetailsOf,
} from '../../../../tasks/alerts_detection_rules';
import {
deleteAlertsAndRules,
deleteDataView,
postDataView,
} from '../../../../tasks/api_calls/common';
import {
createAndEnableRule,
createRuleWithoutEnabling,
fillAboutRuleAndContinue,
fillDefineCustomRule,
fillDefineCustomRuleAndContinue,
fillScheduleRuleAndContinue,
openAddFilterPopover,
waitForAlertsToPopulate,
} from '../../../../tasks/create_new_rule';
import { login } from '../../../../tasks/login';
import { visit } from '../../../../tasks/navigation';
import { openRuleManagementPageViaBreadcrumbs } from '../../../../tasks/rules_management';
import { getDetails, waitForTheRuleToBeExecuted } from '../../../../tasks/rule_details';
import { fillAddFilterForm } from '../../../../tasks/search_bar';
import { CREATE_RULE_URL } from '../../../../urls/navigation';
// Skipping in MKI due to flake
describe('Custom query rules', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, () => {
describe('Custom detection rules creation with data views', () => {
describe(
'Custom query rules with data views - Rule Creation',
{ tags: ['@ess', '@serverless'] },
() => {
const rule = getDataViewRule();
const expectedUrls = rule.references?.join('');
const expectedFalsePositives = rule.false_positives?.join('');
const expectedTags = rule.tags?.join('');
const mitreAttack = rule.threat;
const expectedMitre = formatMitreAttackDescription(mitreAttack ?? []);
const expectedNumberOfRules = 1;
beforeEach(() => {
if (rule.data_view_id != null) {
@ -95,6 +42,7 @@ describe('Custom query rules', { tags: ['@ess', '@serverless', '@skipInServerles
}
deleteAlertsAndRules();
login();
visit(CREATE_RULE_URL);
});
afterEach(() => {
@ -104,91 +52,16 @@ describe('Custom query rules', { tags: ['@ess', '@serverless', '@skipInServerles
});
it('Creates and enables a new rule', function () {
visit(CREATE_RULE_URL);
fillDefineCustomRuleAndContinue(rule);
fillAboutRuleAndContinue(rule);
fillScheduleRuleAndContinue(rule);
createAndEnableRule();
openRuleManagementPageViaBreadcrumbs();
cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)');
getRulesManagementTableRows().should('have.length', expectedNumberOfRules);
cy.get(RULE_NAME).should('have.text', rule.name);
cy.get(RISK_SCORE).should('have.text', rule.risk_score);
cy.get(SEVERITY).should('have.text', 'High');
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
goToRuleDetailsOf(rule.name);
cy.get(RULE_NAME_HEADER).should('contain', `${rule.name}`);
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', rule.description);
cy.get(ABOUT_DETAILS).within(() => {
getDetails(SEVERITY_DETAILS).should('have.text', 'High');
getDetails(RISK_SCORE_DETAILS).should('have.text', rule.risk_score);
getDetails(REFERENCE_URLS_DETAILS).should((details) => {
expect(removeExternalLinkText(details.text())).equal(expectedUrls);
});
getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives);
getDetails(MITRE_ATTACK_DETAILS).should((mitre) => {
expect(removeExternalLinkText(mitre.text())).equal(expectedMitre);
});
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
});
cy.get(INVESTIGATION_NOTES_TOGGLE).click();
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN);
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(DATA_VIEW_DETAILS).should('have.text', rule.data_view_id);
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', rule.query);
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query');
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
});
cy.get(DEFINITION_DETAILS).should('not.contain', INDEX_PATTERNS_DETAILS);
cy.get(SCHEDULE_DETAILS).within(() => {
getDetails(RUNS_EVERY_DETAILS)
.find(INTERVAL_ABBR_VALUE)
.should('have.text', `${rule.interval}`);
const humanizedDuration = getHumanizedDuration(
rule.from ?? 'now-6m',
rule.interval ?? '5m'
);
getDetails(ADDITIONAL_LOOK_BACK_DETAILS)
.find(INTERVAL_ABBR_VALUE)
.should('have.text', `${humanizedDuration}`);
});
waitForTheRuleToBeExecuted();
waitForAlertsToPopulate();
cy.get(ALERTS_COUNT)
.invoke('text')
.should('match', /^[1-9].+$/);
cy.get(ALERT_GRID_CELL).contains(rule.name);
});
it('Creates and edits a new rule with a data view', function () {
visit(CREATE_RULE_URL);
fillDefineCustomRuleAndContinue(rule);
cy.get(RULE_NAME_INPUT).clear();
cy.get(RULE_NAME_INPUT).type(rule.name);
cy.get(RULE_DESCRIPTION_INPUT).clear();
cy.get(RULE_DESCRIPTION_INPUT).type(rule.description);
cy.get(ABOUT_CONTINUE_BTN).should('exist').click();
fillScheduleRuleAndContinue(rule);
createRuleWithoutEnabling();
openRuleManagementPageViaBreadcrumbs();
goToRuleDetailsOf(rule.name);
cy.get(EDIT_RULE_SETTINGS_LINK).click();
cy.get(RULE_NAME_HEADER).should('contain', 'Edit rule settings');
cy.log('Asserting we have a new rule created');
cy.get(RULE_NAME_HEADER).should('contain', rule.name);
});
it('Adds filter on define step', () => {
visit(CREATE_RULE_URL);
fillDefineCustomRule(rule);
openAddFilterPopover();
fillAddFilterForm({
@ -198,5 +71,5 @@ describe('Custom query rules', { tags: ['@ess', '@serverless', '@skipInServerles
// Check that newly added filter exists
cy.get(GLOBAL_SEARCH_BAR_FILTER_ITEM).should('have.text', 'host.name: exists');
});
});
});
}
);

View file

@ -5,259 +5,81 @@
* 2.0.
*/
import { getNewRule, getSavedQueryRule } from '../../../../objects/rule';
import { getSavedQueryRule } from '../../../../objects/rule';
import {
DEFINE_CONTINUE_BUTTON,
LOAD_QUERY_DYNAMICALLY_CHECKBOX,
QUERY_BAR,
} from '../../../../screens/create_new_rule';
import { DEFINE_CONTINUE_BUTTON, QUERY_BAR } from '../../../../screens/create_new_rule';
import { TOASTER } from '../../../../screens/alerts_detection_rules';
import {
RULE_NAME_HEADER,
SAVED_QUERY_NAME_DETAILS,
SAVED_QUERY_DETAILS,
SAVED_QUERY_FILTERS_DETAILS,
DEFINE_RULE_PANEL_PROGRESS,
CUSTOM_QUERY_DETAILS,
} from '../../../../screens/rule_details';
import { RULE_NAME_HEADER } from '../../../../screens/rule_details';
import { editFirstRule, goToRuleDetailsOf } from '../../../../tasks/alerts_detection_rules';
import { createSavedQuery, deleteSavedQueries } from '../../../../tasks/api_calls/saved_queries';
import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
import {
createAndEnableRule,
fillAboutRuleAndContinue,
fillScheduleRuleAndContinue,
selectAndLoadSavedQuery,
getCustomQueryInput,
checkLoadQueryDynamically,
uncheckLoadQueryDynamically,
createRuleWithoutEnabling,
} from '../../../../tasks/create_new_rule';
import { saveEditedRule, visitEditRulePage } from '../../../../tasks/edit_rule';
import { login } from '../../../../tasks/login';
import { visit } from '../../../../tasks/navigation';
import {
assertDetailsNotExist,
getDetails,
visitRuleDetailsPage,
} from '../../../../tasks/rule_details';
import { visitRuleDetailsPage } from '../../../../tasks/rule_details';
import { createRule } from '../../../../tasks/api_calls/rules';
import { CREATE_RULE_URL } from '../../../../urls/navigation';
import { RULES_MANAGEMENT_URL } from '../../../../urls/rules_management';
import { openRuleManagementPageViaBreadcrumbs } from '../../../../tasks/rules_management';
const savedQueryName = 'custom saved query';
const savedQueryQuery = 'process.name: test';
const savedQueryFilterKey = 'testAgent.value';
describe('Saved query rules', { tags: ['@ess', '@serverless'] }, () => {
describe('Custom saved_query detection rule creation', () => {
beforeEach(() => {
login();
deleteAlertsAndRules();
deleteSavedQueries();
});
describe('Saved query rules - Rule Creation', { tags: ['@ess', '@serverless'] }, () => {
beforeEach(() => {
login();
deleteAlertsAndRules();
deleteSavedQueries();
});
it('Creates saved query rule', function () {
const rule = getSavedQueryRule();
createSavedQuery(savedQueryName, savedQueryQuery, savedQueryFilterKey);
visit(CREATE_RULE_URL);
it('Creates saved query rule', function () {
const rule = getSavedQueryRule();
createSavedQuery(savedQueryName, savedQueryQuery, savedQueryFilterKey);
visit(CREATE_RULE_URL);
selectAndLoadSavedQuery(savedQueryName, savedQueryQuery);
selectAndLoadSavedQuery(savedQueryName, savedQueryQuery);
// edit loaded saved query
getCustomQueryInput()
.type(' AND random query')
.should('have.value', [savedQueryQuery, ' AND random query'].join(''));
// edit loaded saved query
getCustomQueryInput()
.type(' AND random query')
.should('have.value', [savedQueryQuery, ' AND random query'].join(''));
// when clicking load query dynamically checkbox, saved query should be shown in query input and input should be disabled
checkLoadQueryDynamically();
getCustomQueryInput().should('have.value', savedQueryQuery).should('be.disabled');
cy.get(QUERY_BAR).should('contain', savedQueryFilterKey);
// when clicking load query dynamically checkbox, saved query should be shown in query input and input should be disabled
checkLoadQueryDynamically();
getCustomQueryInput().should('have.value', savedQueryQuery).should('be.disabled');
cy.get(QUERY_BAR).should('contain', savedQueryFilterKey);
cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click();
cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click();
fillAboutRuleAndContinue(rule);
fillScheduleRuleAndContinue(rule);
cy.intercept('POST', '/api/detection_engine/rules').as('savedQueryRule');
createAndEnableRule();
openRuleManagementPageViaBreadcrumbs();
fillAboutRuleAndContinue(rule);
fillScheduleRuleAndContinue(rule);
createRuleWithoutEnabling();
cy.wait('@savedQueryRule').then(({ response }) => {
// created rule should have saved_query type
cy.wrap(response?.body.type).should('equal', 'saved_query');
cy.log('Asserting we have a new rule created');
cy.get(RULE_NAME_HEADER).should('contain', rule.name);
});
context('Non existent saved query', () => {
const FAILED_TO_LOAD_ERROR = 'Failed to load the saved query';
describe('on rule details page', () => {
beforeEach(() => {
createRule(
getSavedQueryRule({
saved_id: 'non-existent',
query: undefined,
})
).then((rule) => visitRuleDetailsPage(rule.body.id));
});
goToRuleDetailsOf(rule.name);
cy.get(RULE_NAME_HEADER).should('contain', `${rule.name}`);
cy.get(DEFINE_RULE_PANEL_PROGRESS).should('not.exist');
getDetails(SAVED_QUERY_NAME_DETAILS).should('contain', savedQueryName);
getDetails(SAVED_QUERY_DETAILS).should('contain', savedQueryQuery);
getDetails(SAVED_QUERY_FILTERS_DETAILS).should('contain', savedQueryFilterKey);
});
context('Non existent saved query', () => {
const FAILED_TO_LOAD_ERROR = 'Failed to load the saved query';
describe('on rule details page', () => {
beforeEach(() => {
createRule(
getSavedQueryRule({
saved_id: 'non-existent',
query: undefined,
})
).then((rule) => visitRuleDetailsPage(rule.body.id));
});
it('Shows error toast on details page when saved query can not be loaded', function () {
cy.get(TOASTER).should('contain', FAILED_TO_LOAD_ERROR);
});
});
describe('on rule editing page', () => {
beforeEach(() => {
createRule(
getSavedQueryRule({
saved_id: 'non-existent',
query: undefined,
})
).then((rule) => visitEditRulePage(rule.body.id));
});
it('Shows validation error on rule edit when saved query can not be loaded', function () {
cy.get(TOASTER).should('contain', FAILED_TO_LOAD_ERROR);
});
// https://github.com/elastic/kibana/issues/187623
it(
'Allows to update saved_query rule with non-existent query',
{ tags: ['@skipInServerlessMKI'] },
() => {
cy.get(LOAD_QUERY_DYNAMICALLY_CHECKBOX).should('exist');
cy.intercept('PUT', '/api/detection_engine/rules').as('editedRule');
saveEditedRule();
cy.wait('@editedRule').then(({ response }) => {
// updated rule type shouldn't change
cy.wrap(response?.body.type).should('equal', 'saved_query');
});
cy.get(DEFINE_RULE_PANEL_PROGRESS).should('not.exist');
assertDetailsNotExist(SAVED_QUERY_NAME_DETAILS);
assertDetailsNotExist(SAVED_QUERY_DETAILS);
}
);
});
});
context('Editing', () => {
it('Allows to update query rule as saved_query rule type', () => {
createSavedQuery(savedQueryName, savedQueryQuery);
createRule(getNewRule()).then((rule) => visitEditRulePage(rule.body.id));
selectAndLoadSavedQuery(savedQueryName, savedQueryQuery);
checkLoadQueryDynamically();
cy.intercept('PUT', '/api/detection_engine/rules').as('editedRule');
saveEditedRule();
cy.wait('@editedRule').then(({ response }) => {
// updated rule should be saved as saved_query type once Load query dynamically checkbox was checked
cy.wrap(response?.body.type).should('equal', 'saved_query');
});
cy.get(DEFINE_RULE_PANEL_PROGRESS).should('not.exist');
getDetails(SAVED_QUERY_NAME_DETAILS).should('contain', savedQueryName);
getDetails(SAVED_QUERY_DETAILS).should('contain', savedQueryQuery);
});
it('Allows to update saved_query rule as query rule type', () => {
const expectedCustomTestQuery = 'random test query';
createSavedQuery(savedQueryName, savedQueryQuery).then((response) => {
cy.log(JSON.stringify(response.body, null, 2));
createRule(getSavedQueryRule({ saved_id: response.body.id, query: undefined })).then(
(rule) => visitEditRulePage(rule.body.id)
);
});
// query input should be disabled and has value of saved query
getCustomQueryInput().should('have.value', savedQueryQuery).should('be.disabled');
// after unchecking Load Query Dynamically checkbox, query input becomes enabled, type custom query
uncheckLoadQueryDynamically();
getCustomQueryInput().should('be.enabled').clear().type(expectedCustomTestQuery);
cy.intercept('PUT', '/api/detection_engine/rules').as('editedRule');
saveEditedRule();
cy.wait('@editedRule').then(({ response }) => {
// updated rule should be saved as query type once Load query dynamically checkbox was unchecked
cy.wrap(response?.body.type).should('equal', 'query');
});
getDetails(CUSTOM_QUERY_DETAILS).should('contain', expectedCustomTestQuery);
});
it('Allows to update saved_query rule with non-existent query by adding custom query', () => {
const expectedCustomTestQuery = 'random test query';
createRule(getSavedQueryRule({ saved_id: 'non-existent', query: undefined })).then((rule) =>
visitEditRulePage(rule.body.id)
);
uncheckLoadQueryDynamically();
// type custom query, ensure Load dynamically checkbox is absent, as rule can't be saved win non valid saved query
getCustomQueryInput().type(expectedCustomTestQuery);
cy.get(LOAD_QUERY_DYNAMICALLY_CHECKBOX).should('not.visible');
cy.intercept('PUT', '/api/detection_engine/rules').as('editedRule');
saveEditedRule();
cy.wait('@editedRule').then(({ response }) => {
// updated rule should be saved as query type once Load query dynamically checkbox was unchecked
cy.wrap(response?.body.type).should('equal', 'query');
});
getDetails(CUSTOM_QUERY_DETAILS).should('contain', expectedCustomTestQuery);
});
it('Allows to update saved_query rule with non-existent query by selecting another saved query', () => {
createSavedQuery(savedQueryName, savedQueryQuery);
createRule(getSavedQueryRule({ saved_id: 'non-existent', query: undefined })).then((rule) =>
visitEditRulePage(rule.body.id)
);
visit(RULES_MANAGEMENT_URL);
editFirstRule();
uncheckLoadQueryDynamically();
// select another saved query, edit query input, which later should be dismissed once Load query dynamically checkbox checked
selectAndLoadSavedQuery(savedQueryName, savedQueryQuery);
getCustomQueryInput().type('AND this part wil be dismissed');
checkLoadQueryDynamically();
getCustomQueryInput().should('have.value', savedQueryQuery);
cy.intercept('PUT', '/api/detection_engine/rules').as('editedRule');
saveEditedRule();
cy.wait('@editedRule').then(({ response }) => {
// updated rule type shouldn't change
cy.wrap(response?.body.type).should('equal', 'saved_query');
});
cy.get(DEFINE_RULE_PANEL_PROGRESS).should('not.exist');
getDetails(SAVED_QUERY_NAME_DETAILS).should('contain', savedQueryName);
getDetails(SAVED_QUERY_DETAILS).should('contain', savedQueryQuery);
it('Shows error toast on details page when saved query can not be loaded', function () {
cy.get(TOASTER).should('contain', FAILED_TO_LOAD_ERROR);
});
});
});

View file

@ -5,50 +5,14 @@
* 2.0.
*/
import { formatMitreAttackDescription, getHumanizedDuration } from '../../../../helpers/rules';
import { getEqlRule, getEqlSequenceRule, getIndexPatterns } from '../../../../objects/rule';
import { getEqlRule, getEqlSequenceRule } from '../../../../objects/rule';
import { ALERTS_COUNT, ALERT_DATA_GRID } from '../../../../screens/alerts';
import {
CUSTOM_RULES_BTN,
RISK_SCORE,
RULES_MANAGEMENT_TABLE,
RULE_NAME,
RULE_SWITCH,
SEVERITY,
} from '../../../../screens/alerts_detection_rules';
import {
ABOUT_DETAILS,
ABOUT_INVESTIGATION_NOTES,
ABOUT_RULE_DESCRIPTION,
ADDITIONAL_LOOK_BACK_DETAILS,
EQL_QUERY_DETAILS,
DEFINITION_DETAILS,
FALSE_POSITIVES_DETAILS,
removeExternalLinkText,
INDEX_PATTERNS_DETAILS,
INVESTIGATION_NOTES_MARKDOWN,
INVESTIGATION_NOTES_TOGGLE,
MITRE_ATTACK_DETAILS,
REFERENCE_URLS_DETAILS,
RISK_SCORE_DETAILS,
RULE_NAME_HEADER,
RULE_TYPE_DETAILS,
RUNS_EVERY_DETAILS,
SCHEDULE_DETAILS,
SEVERITY_DETAILS,
TAGS_DETAILS,
TIMELINE_TEMPLATE_DETAILS,
INTERVAL_ABBR_VALUE,
} from '../../../../screens/rule_details';
import { RULE_NAME_HEADER } from '../../../../screens/rule_details';
import { getDetails } from '../../../../tasks/rule_details';
import { expectNumberOfRules, goToRuleDetailsOf } from '../../../../tasks/alerts_detection_rules';
import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
import {
continueFromDefineStep,
createAndEnableRule,
createRuleWithNonBlockingErrors,
createRuleWithoutEnabling,
fillAboutRuleAndContinue,
fillDefineEqlRuleAndContinue,
fillScheduleRuleAndContinue,
@ -56,12 +20,9 @@ import {
getIndexPatternClearButton,
getRuleIndexInput,
selectEqlRuleType,
waitForAlertsToPopulate,
} from '../../../../tasks/create_new_rule';
import { login } from '../../../../tasks/login';
import { visit } from '../../../../tasks/navigation';
import { openRuleManagementPageViaBreadcrumbs } from '../../../../tasks/rules_management';
import { CREATE_RULE_URL } from '../../../../urls/navigation';
import {
EQL_OPTIONS_POPOVER_TRIGGER,
EQL_OPTIONS_TIMESTAMP_INPUT,
@ -70,132 +31,40 @@ import {
EQL_QUERY_VALIDATION_ERROR_CONTENT,
RULES_CREATION_FORM,
} from '../../../../screens/create_new_rule';
import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
import { CREATE_RULE_URL } from '../../../../urls/navigation';
// Skip in MKI due to flake
describe('EQL rules', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, () => {
describe('EQL Rule - Rule Creation', { tags: ['@ess', '@serverless'] }, () => {
beforeEach(() => {
login();
deleteAlertsAndRules();
login();
visit(CREATE_RULE_URL);
});
describe('Detection rules, EQL', () => {
it('Creates a new EQL rule', function () {
const rule = getEqlRule();
const expectedUrls = rule.references?.join('');
const expectedFalsePositives = rule.false_positives?.join('');
const expectedTags = rule.tags?.join('');
const mitreAttack = rule.threat;
const expectedMitre = formatMitreAttackDescription(mitreAttack ?? []);
const expectedNumberOfRules = 1;
const expectedNumberOfAlerts = '1 alert';
it('Creates and enables a new EQL rule', function () {
visit(CREATE_RULE_URL);
selectEqlRuleType();
fillDefineEqlRuleAndContinue(rule);
fillAboutRuleAndContinue(rule);
fillScheduleRuleAndContinue(rule);
createAndEnableRule();
openRuleManagementPageViaBreadcrumbs();
selectEqlRuleType();
fillDefineEqlRuleAndContinue(rule);
fillAboutRuleAndContinue(rule);
fillScheduleRuleAndContinue(rule);
createRuleWithoutEnabling();
cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)');
expectNumberOfRules(RULES_MANAGEMENT_TABLE, expectedNumberOfRules);
cy.get(RULE_NAME).should('have.text', rule.name);
cy.get(RISK_SCORE).should('have.text', rule.risk_score);
cy.get(SEVERITY).should('have.text', 'High');
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
goToRuleDetailsOf(rule.name);
cy.get(RULE_NAME_HEADER).should('contain', `${rule.name}`);
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', rule.description);
cy.get(ABOUT_DETAILS).within(() => {
getDetails(SEVERITY_DETAILS).should('have.text', 'High');
getDetails(RISK_SCORE_DETAILS).should('have.text', rule.risk_score);
getDetails(REFERENCE_URLS_DETAILS).should((details) => {
expect(removeExternalLinkText(details.text())).equal(expectedUrls);
});
getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives);
getDetails(MITRE_ATTACK_DETAILS).should((mitre) => {
expect(removeExternalLinkText(mitre.text())).equal(expectedMitre);
});
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
});
cy.get(INVESTIGATION_NOTES_TOGGLE).click();
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN);
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(INDEX_PATTERNS_DETAILS).should('have.text', getIndexPatterns().join(''));
getDetails(EQL_QUERY_DETAILS).should('have.text', rule.query);
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Event Correlation');
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
});
cy.get(SCHEDULE_DETAILS).within(() => {
getDetails(RUNS_EVERY_DETAILS)
.find(INTERVAL_ABBR_VALUE)
.should('have.text', `${rule.interval}`);
const humanizedDuration = getHumanizedDuration(
rule.from ?? 'now-6m',
rule.interval ?? '5m'
);
getDetails(ADDITIONAL_LOOK_BACK_DETAILS)
.find(INTERVAL_ABBR_VALUE)
.should('have.text', `${humanizedDuration}`);
});
waitForAlertsToPopulate();
cy.get(ALERTS_COUNT).should('have.text', expectedNumberOfAlerts);
cy.get(ALERT_DATA_GRID)
.invoke('text')
.then((text) => {
expect(text).contains(rule.name);
expect(text).contains(rule.severity);
expect(text).contains(rule.risk_score);
});
});
cy.log('Asserting we have a new rule created');
cy.get(RULE_NAME_HEADER).should('contain', rule.name);
});
describe('Detection rules, sequence EQL', () => {
const expectedNumberOfSequenceAlerts = '2 alerts';
it('Creates a new EQL rule with a sequence', () => {
const rule = getEqlSequenceRule();
before(() => {
cy.task('esArchiverLoad', { archiveName: 'auditbeat_multiple' });
});
selectEqlRuleType();
fillDefineEqlRuleAndContinue(rule);
fillAboutRuleAndContinue(rule);
fillScheduleRuleAndContinue(rule);
createRuleWithoutEnabling();
after(() => {
cy.task('esArchiverUnload', { archiveName: 'auditbeat_multiple' });
});
it(
'Creates and enables a new EQL rule with a sequence',
{
tags: ['@skipInServerlessMKI'],
},
function () {
login();
visit(CREATE_RULE_URL);
selectEqlRuleType();
fillDefineEqlRuleAndContinue(rule);
fillAboutRuleAndContinue(rule);
fillScheduleRuleAndContinue(rule);
createAndEnableRule();
openRuleManagementPageViaBreadcrumbs();
goToRuleDetailsOf(rule.name);
waitForAlertsToPopulate();
cy.get(ALERTS_COUNT).should('have.text', expectedNumberOfSequenceAlerts);
cy.get(ALERT_DATA_GRID)
.invoke('text')
.then((text) => {
cy.log('ALERT_DATA_GRID', text);
expect(text).contains(rule.name);
expect(text).contains(rule.severity);
});
}
);
cy.log('Asserting we have a new rule created');
cy.get(RULE_NAME_HEADER).should('contain', rule.name);
});
describe('with source data requiring EQL overrides', () => {

View file

@ -1,93 +0,0 @@
/*
* 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 { getEqlRule } from '../../../../objects/rule';
import { login } from '../../../../tasks/login';
import { visit } from '../../../../tasks/navigation';
import { getDetails } from '../../../../tasks/rule_details';
import { CREATE_RULE_URL } from '../../../../urls/navigation';
import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
import { selectEqlRuleType } from '../../../../tasks/create_new_rule';
import {
ALERT_SUPPRESSION_FIELDS_INPUT,
ALERT_SUPPRESSION_FIELDS,
} from '../../../../screens/create_new_rule';
import {
DEFINITION_DETAILS,
SUPPRESS_FOR_DETAILS,
SUPPRESS_BY_DETAILS,
SUPPRESS_MISSING_FIELD,
DETAILS_TITLE,
ALERT_SUPPRESSION_INSUFFICIENT_LICENSING_ICON,
} from '../../../../screens/rule_details';
import { startBasicLicense } from '../../../../tasks/api_calls/licensing';
import { createRule } from '../../../../tasks/api_calls/rules';
import { TOOLTIP } from '../../../../screens/common';
import { ruleDetailsUrl } from '../../../../urls/rule_details';
const SUPPRESS_BY_FIELDS = ['agent.type'];
describe(
'Detection Rule Creation - EQL Rules - With Alert Suppression - Basic License',
{
tags: ['@ess'],
},
() => {
beforeEach(() => {
deleteAlertsAndRules();
login();
visit(CREATE_RULE_URL);
startBasicLicense();
});
after(() => {
cy.task('esArchiverUnload', { archiveName: 'auditbeat_multiple' });
});
it('cannot create a rule with "per rule execution" suppression durations', () => {
selectEqlRuleType();
cy.get(ALERT_SUPPRESSION_FIELDS_INPUT).should('be.disabled');
cy.get(ALERT_SUPPRESSION_FIELDS).trigger('mouseover');
// Platinum license is required, tooltip on disabled alert suppression checkbox should tell this
cy.get(TOOLTIP).contains('Platinum license');
});
it('shows an upselling message on rule suppression details', () => {
const rule = getEqlRule();
createRule({
...rule,
alert_suppression: {
group_by: SUPPRESS_BY_FIELDS,
duration: { value: 360, unit: 's' },
missing_fields_strategy: 'doNotSuppress',
},
}).then((createdRule) => {
visit(ruleDetailsUrl(createdRule.body.id));
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(SUPPRESS_BY_DETAILS).should('have.text', SUPPRESS_BY_FIELDS.join(''));
getDetails(SUPPRESS_FOR_DETAILS).should('have.text', '360s');
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');
});
// Platinum license is required for configuration to apply
cy.get(ALERT_SUPPRESSION_INSUFFICIENT_LICENSING_ICON).eq(2).trigger('mouseover');
cy.get(TOOLTIP).contains(
'Alert suppression is configured but will not be applied due to insufficient licensing'
);
});
});
}
);

View file

@ -1,74 +0,0 @@
/*
* 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 { getEqlRule } from '../../../../objects/rule';
import { login } from '../../../../tasks/login';
import { visit } from '../../../../tasks/navigation';
import { getDetails } from '../../../../tasks/rule_details';
import { CREATE_RULE_URL } from '../../../../urls/navigation';
import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
import {
fillAlertSuppressionFields,
fillAboutRuleMinimumAndContinue,
createRuleWithoutEnabling,
skipScheduleRuleAction,
continueFromDefineStep,
fillDefineEqlRule,
selectEqlRuleType,
} from '../../../../tasks/create_new_rule';
import { DEFINITION_DETAILS, SUPPRESS_BY_DETAILS } from '../../../../screens/rule_details';
const SUPPRESS_BY_FIELDS = ['agent.type'];
describe(
'Detection Rule Creation - EQL Rules - With Alert Suppression - Serverless Essentials License',
{
tags: ['@serverless'],
env: {
ftrConfig: {
productTypes: [
{ product_line: 'security', product_tier: 'essentials' },
{ product_line: 'endpoint', product_tier: 'essentials' },
],
},
},
},
() => {
const rule = getEqlRule();
before(() => {
cy.task('esArchiverLoad', { archiveName: 'auditbeat_multiple' });
});
beforeEach(() => {
deleteAlertsAndRules();
login();
});
after(() => {
cy.task('esArchiverUnload', { archiveName: 'auditbeat_multiple' });
});
describe('with non-sequence queries', () => {
it('creates a rule with a "per rule execution" suppression duration', () => {
visit(CREATE_RULE_URL);
selectEqlRuleType();
fillDefineEqlRule(rule);
// selecting only suppression fields, the rest options would be default
fillAlertSuppressionFields(SUPPRESS_BY_FIELDS);
continueFromDefineStep();
fillAboutRuleMinimumAndContinue(rule);
skipScheduleRuleAction();
createRuleWithoutEnabling();
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(SUPPRESS_BY_DETAILS).should('have.text', SUPPRESS_BY_FIELDS.join(''));
});
});
});
}
);

View file

@ -16,10 +16,6 @@ import {
RULE_NAME_HEADER,
RULE_TYPE_DETAILS,
RULE_NAME_OVERRIDE_DETAILS,
DEFINITION_DETAILS,
SUPPRESS_BY_DETAILS,
SUPPRESS_FOR_DETAILS,
SUPPRESS_MISSING_FIELD,
} from '../../../../screens/rule_details';
import { ESQL_QUERY_BAR } from '../../../../screens/create_new_rule';
@ -41,14 +37,6 @@ import {
fillRuleName,
fillDescription,
getAboutContinueButton,
fillAlertSuppressionFields,
selectAlertSuppressionPerInterval,
setAlertSuppressionDuration,
selectDoNotSuppressForMissingFields,
continueFromDefineStep,
fillAboutRuleMinimumAndContinue,
skipScheduleRuleAction,
interceptEsqlQueryFieldsRequest,
createRuleWithNonBlockingErrors,
} from '../../../../tasks/create_new_rule';
import { login } from '../../../../tasks/login';
@ -68,7 +56,7 @@ const workaroundForResizeObserver = () =>
});
describe(
'Detection ES|QL rules, creation',
'Detection ES|QL rules - Rule Creation',
{
tags: ['@ess', '@serverless', '@skipInServerlessMKI'],
},
@ -241,60 +229,5 @@ describe(
cy.get(INVESTIGATION_FIELDS_VALUE_ITEM).should('have.text', CUSTOM_ESQL_FIELD);
});
});
describe('Alert suppression', () => {
beforeEach(() => {
login();
visit(CREATE_RULE_URL);
});
it('shows custom ES|QL field in investigation fields autocomplete and saves it in rule', function () {
const CUSTOM_ESQL_FIELD = '_custom_agent_name';
const SUPPRESS_BY_FIELDS = [CUSTOM_ESQL_FIELD, 'agent.type'];
const queryWithCustomFields = [
`from auditbeat* metadata _id, _version, _index`,
`eval ${CUSTOM_ESQL_FIELD} = agent.name`,
`drop agent.*`,
].join(' | ');
workaroundForResizeObserver();
selectEsqlRuleType();
interceptEsqlQueryFieldsRequest(queryWithCustomFields, 'esqlSuppressionFieldsRequest');
fillEsqlQueryBar(queryWithCustomFields);
cy.wait('@esqlSuppressionFieldsRequest');
fillAlertSuppressionFields(SUPPRESS_BY_FIELDS);
selectAlertSuppressionPerInterval();
setAlertSuppressionDuration(2, 'h');
selectDoNotSuppressForMissingFields();
continueFromDefineStep();
// ensures details preview works correctly
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(SUPPRESS_BY_DETAILS).should('have.text', SUPPRESS_BY_FIELDS.join(''));
getDetails(SUPPRESS_FOR_DETAILS).should('have.text', '2h');
getDetails(SUPPRESS_MISSING_FIELD).should(
'have.text',
'Do not suppress alerts for events with missing fields'
);
});
fillAboutRuleMinimumAndContinue(rule);
skipScheduleRuleAction();
createRuleWithoutEnabling();
// ensures rule details displayed correctly after rule created
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(SUPPRESS_BY_DETAILS).should('have.text', SUPPRESS_BY_FIELDS.join(''));
getDetails(SUPPRESS_FOR_DETAILS).should('have.text', '2h');
getDetails(SUPPRESS_MISSING_FIELD).should(
'have.text',
'Do not suppress alerts for events with missing fields'
);
});
});
});
}
);

View file

@ -118,461 +118,465 @@ import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
const DEFAULT_THREAT_MATCH_QUERY = '@timestamp >= "now-30d/d"';
// Skipping in MKI due to flake
describe('indicator match', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, () => {
describe('Detection rules, Indicator Match', () => {
const expectedUrls = getNewThreatIndicatorRule().references?.join('');
const expectedFalsePositives = getNewThreatIndicatorRule().false_positives?.join('');
const expectedTags = getNewThreatIndicatorRule().tags?.join('');
const mitreAttack = getNewThreatIndicatorRule().threat;
const expectedMitre = formatMitreAttackDescription(mitreAttack ?? []);
const expectedNumberOfRules = 1;
const expectedNumberOfAlerts = '1 alert';
describe(
'Indicator Match - Rule Creation',
{ tags: ['@ess', '@serverless', '@skipInServerlessMKI'] },
() => {
describe('Detection rules, Indicator Match', () => {
const expectedUrls = getNewThreatIndicatorRule().references?.join('');
const expectedFalsePositives = getNewThreatIndicatorRule().false_positives?.join('');
const expectedTags = getNewThreatIndicatorRule().tags?.join('');
const mitreAttack = getNewThreatIndicatorRule().threat;
const expectedMitre = formatMitreAttackDescription(mitreAttack ?? []);
const expectedNumberOfRules = 1;
const expectedNumberOfAlerts = '1 alert';
beforeEach(() => {
cy.task('esArchiverLoad', { archiveName: 'threat_indicator' });
cy.task('esArchiverLoad', { archiveName: 'suspicious_source_event' });
deleteAlertsAndRules();
login();
});
beforeEach(() => {
cy.task('esArchiverLoad', { archiveName: 'threat_indicator' });
cy.task('esArchiverLoad', { archiveName: 'suspicious_source_event' });
deleteAlertsAndRules();
login();
});
describe('Creating new indicator match rules', () => {
describe('Index patterns', () => {
beforeEach(() => {
visit(CREATE_RULE_URL);
selectIndicatorMatchType();
describe('Creating new indicator match rules', () => {
describe('Index patterns', () => {
beforeEach(() => {
visit(CREATE_RULE_URL);
selectIndicatorMatchType();
});
it('Contains a predefined index pattern', () => {
getRuleIndexInput().should('have.text', getIndexPatterns().join(''));
});
it('Does NOT show invalidation text on initial page load if indicator index pattern is filled out', () => {
getDefineContinueButton().click();
getIndexPatternInvalidationText().should('not.exist');
});
it('Shows invalidation text when you try to continue without filling it out', () => {
getIndexPatternClearButton().click();
getIndicatorIndicatorIndex().type(`{backspace}{enter}`);
getDefineContinueButton().click();
getIndexPatternInvalidationText().should('exist');
});
});
it('Contains a predefined index pattern', () => {
getRuleIndexInput().should('have.text', getIndexPatterns().join(''));
describe('Indicator index patterns', () => {
beforeEach(() => {
visit(CREATE_RULE_URL);
selectIndicatorMatchType();
});
it('Contains a predefined index pattern', () => {
getIndicatorIndicatorIndex().should('have.text', getThreatIndexPatterns().join(''));
});
it('Does NOT show invalidation text on initial page load', () => {
getIndexPatternInvalidationText().should('not.exist');
});
it('Shows invalidation text if you try to continue without filling it out', () => {
getIndicatorIndicatorIndex().type(`{backspace}{enter}`);
getDefineContinueButton().click();
getIndexPatternInvalidationText().should('exist');
});
});
it('Does NOT show invalidation text on initial page load if indicator index pattern is filled out', () => {
getDefineContinueButton().click();
getIndexPatternInvalidationText().should('not.exist');
describe('custom query input', () => {
beforeEach(() => {
visit(CREATE_RULE_URL);
selectIndicatorMatchType();
});
it('Has a default set of *:*', () => {
getCustomQueryInput().should('have.text', '*:*');
});
it('Shows invalidation text if text is removed', () => {
getCustomQueryInput().type('{selectall}{del}');
getCustomQueryInvalidationText().should('exist');
});
});
it('Shows invalidation text when you try to continue without filling it out', () => {
getIndexPatternClearButton().click();
getIndicatorIndicatorIndex().type(`{backspace}{enter}`);
getDefineContinueButton().click();
getIndexPatternInvalidationText().should('exist');
describe('indicator query input', () => {
beforeEach(() => {
visit(CREATE_RULE_URL);
selectIndicatorMatchType();
});
it(`Has a default set of ${DEFAULT_THREAT_MATCH_QUERY}`, () => {
getCustomIndicatorQueryInput().should('have.text', DEFAULT_THREAT_MATCH_QUERY);
});
it('Shows invalidation text if text is removed', () => {
getCustomIndicatorQueryInput().type('{selectall}{del}');
getThreatMatchQueryInvalidationText().should('exist');
});
});
// FLAKY: https://github.com/elastic/kibana/issues/182669
describe.skip('Indicator mapping', () => {
beforeEach(() => {
const rule = getNewThreatIndicatorRule();
visit(CREATE_RULE_URL);
selectIndicatorMatchType();
if (rule.index) {
fillIndexAndIndicatorIndexPattern(rule.index, rule.threat_index);
}
});
it('Does NOT show invalidation text on initial page load', () => {
getIndicatorInvalidationText().should('not.exist');
});
it('Shows invalidation text when you try to press continue without filling anything out', () => {
getDefineContinueButton().click();
getIndicatorAtLeastOneInvalidationText().should('exist');
});
it('Shows invalidation text when the "AND" button is pressed and both the mappings are blank', () => {
getIndicatorAndButton().click();
getIndicatorInvalidationText().should('exist');
});
it('Shows invalidation text when the "OR" button is pressed and both the mappings are blank', () => {
getIndicatorOrButton().click();
getIndicatorInvalidationText().should('exist');
});
it('Does NOT show invalidation text when there is a valid "index field" and a valid "indicator index field"', () => {
fillIndicatorMatchRow({
indexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].field,
indicatorIndexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].value,
});
getDefineContinueButton().click();
getIndicatorInvalidationText().should('not.exist');
});
it('Shows invalidation text when there is an invalid "index field" and a valid "indicator index field"', () => {
fillIndicatorMatchRow({
indexField: 'non-existent-value',
indicatorIndexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].value,
validColumns: 'indicatorField',
});
getDefineContinueButton().click();
getIndicatorInvalidationText().should('exist');
});
it('Shows invalidation text when there is a valid "index field" and an invalid "indicator index field"', () => {
fillIndicatorMatchRow({
indexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].field,
indicatorIndexField: 'non-existent-value',
validColumns: 'indexField',
});
getDefineContinueButton().click();
getIndicatorInvalidationText().should('exist');
});
it('Deletes the first row when you have two rows. Both rows valid rows of "index fields" and valid "indicator index fields". The second row should become the first row', () => {
fillIndicatorMatchRow({
indexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].field,
indicatorIndexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].value,
});
getIndicatorAndButton().click();
fillIndicatorMatchRow({
rowNumber: 2,
indexField: 'agent.name',
indicatorIndexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].value,
validColumns: 'indicatorField',
});
getIndicatorDeleteButton().click();
getIndicatorIndexComboField().find('input').should('have.value', 'agent.name');
getIndicatorMappingComboField()
.find('input')
.should('have.value', getNewThreatIndicatorRule().threat_mapping[0].entries[0].value);
getIndicatorIndexComboField(2).should('not.exist');
getIndicatorMappingComboField(2).should('not.exist');
});
it('Deletes the first row when you have two rows. Both rows have valid "index fields" and invalid "indicator index fields". The second row should become the first row', () => {
fillIndicatorMatchRow({
indexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].field,
indicatorIndexField: 'non-existent-value',
validColumns: 'indexField',
});
getIndicatorAndButton().click();
fillIndicatorMatchRow({
rowNumber: 2,
indexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].field,
indicatorIndexField: 'second-non-existent-value',
validColumns: 'indexField',
});
getIndicatorDeleteButton().click();
getIndicatorMappingComboField()
.find('input')
.should('have.value', 'second-non-existent-value');
getIndicatorIndexComboField(2).should('not.exist');
getIndicatorMappingComboField(2).should('not.exist');
});
it('Deletes the first row when you have two rows. Both rows have valid "indicator index fields" and invalid "index fields". The second row should become the first row', () => {
fillIndicatorMatchRow({
indexField: 'non-existent-value',
indicatorIndexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].value,
validColumns: 'indicatorField',
});
getIndicatorAndButton().click();
fillIndicatorMatchRow({
rowNumber: 2,
indexField: 'second-non-existent-value',
indicatorIndexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].value,
validColumns: 'indicatorField',
});
getIndicatorDeleteButton().click();
getIndicatorIndexComboField()
.find('input')
.should('have.value', 'second-non-existent-value');
getIndicatorIndexComboField(2).should('not.exist');
getIndicatorMappingComboField(2).should('not.exist');
});
it('Deletes the first row of data but not the UI elements and the text defaults back to the placeholder of Search', () => {
fillIndicatorMatchRow({
indexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].field,
indicatorIndexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].value,
});
getIndicatorDeleteButton().click();
getIndicatorIndexComboField()
.find('input')
.should('value', '')
.should('have.attr', 'placeholder', 'Search');
getIndicatorMappingComboField()
.find('input')
.should('value', '')
.should('have.attr', 'placeholder', 'Search');
getIndicatorIndexComboField(2).should('not.exist');
getIndicatorMappingComboField(2).should('not.exist');
});
it('Deletes the second row when you have three rows. The first row is valid data, the second row is invalid data, and the third row is valid data. Third row should shift up correctly', () => {
fillIndicatorMatchRow({
indexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].field,
indicatorIndexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].value,
});
getIndicatorAndButton().click();
fillIndicatorMatchRow({
rowNumber: 2,
indexField: 'non-existent-value',
indicatorIndexField: 'non-existent-value',
validColumns: 'none',
});
getIndicatorAndButton().click();
fillIndicatorMatchRow({
rowNumber: 3,
indexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].field,
indicatorIndexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].value,
});
getIndicatorDeleteButton(2).click();
getIndicatorIndexComboField(1)
.find('input')
.should('value', getNewThreatIndicatorRule().threat_mapping[0].entries[0].field);
getIndicatorMappingComboField(1)
.find('input')
.should('value', getNewThreatIndicatorRule().threat_mapping[0].entries[0].value);
getIndicatorIndexComboField(2)
.find('input')
.should('value', getNewThreatIndicatorRule().threat_mapping[0].entries[0].field);
getIndicatorMappingComboField(2)
.find('input')
.should('value', getNewThreatIndicatorRule().threat_mapping[0].entries[0].value);
getIndicatorIndexComboField(3).should('not.exist');
getIndicatorMappingComboField(3).should('not.exist');
});
it('Can add two OR rows and delete the second row. The first row has invalid data and the second row has valid data. The first row is deleted and the second row shifts up correctly.', () => {
fillIndicatorMatchRow({
indexField: 'non-existent-value-one',
indicatorIndexField: 'non-existent-value-two',
validColumns: 'none',
});
getIndicatorOrButton().click();
fillIndicatorMatchRow({
rowNumber: 2,
indexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].field,
indicatorIndexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].value,
});
getIndicatorDeleteButton().click();
getIndicatorIndexComboField()
.find('input')
.should('value', getNewThreatIndicatorRule().threat_mapping[0].entries[0].field);
getIndicatorMappingComboField()
.find('input')
.should('value', getNewThreatIndicatorRule().threat_mapping[0].entries[0].value);
getIndicatorIndexComboField(2).should('not.exist');
getIndicatorMappingComboField(2).should('not.exist');
});
});
describe('Schedule', () => {
it('IM rule has 1h time interval and lookback by default', () => {
visit(CREATE_RULE_URL);
selectIndicatorMatchType();
fillDefineIndicatorMatchRuleAndContinue(getNewThreatIndicatorRule());
fillAboutRuleAndContinue(getNewThreatIndicatorRule());
cy.get(SCHEDULE_INTERVAL_AMOUNT_INPUT).invoke('val').should('eql', '1');
cy.get(SCHEDULE_INTERVAL_UNITS_INPUT).invoke('val').should('eql', 'h');
cy.get(SCHEDULE_LOOKBACK_AMOUNT_INPUT).invoke('val').should('eql', '5');
cy.get(SCHEDULE_LOOKBACK_UNITS_INPUT).invoke('val').should('eql', 'm');
});
});
});
describe('Indicator index patterns', () => {
beforeEach(() => {
visit(CREATE_RULE_URL);
selectIndicatorMatchType();
});
it('Contains a predefined index pattern', () => {
getIndicatorIndicatorIndex().should('have.text', getThreatIndexPatterns().join(''));
});
it('Does NOT show invalidation text on initial page load', () => {
getIndexPatternInvalidationText().should('not.exist');
});
it('Shows invalidation text if you try to continue without filling it out', () => {
getIndicatorIndicatorIndex().type(`{backspace}{enter}`);
getDefineContinueButton().click();
getIndexPatternInvalidationText().should('exist');
});
});
describe('custom query input', () => {
beforeEach(() => {
visit(CREATE_RULE_URL);
selectIndicatorMatchType();
});
it('Has a default set of *:*', () => {
getCustomQueryInput().should('have.text', '*:*');
});
it('Shows invalidation text if text is removed', () => {
getCustomQueryInput().type('{selectall}{del}');
getCustomQueryInvalidationText().should('exist');
});
});
describe('indicator query input', () => {
beforeEach(() => {
visit(CREATE_RULE_URL);
selectIndicatorMatchType();
});
it(`Has a default set of ${DEFAULT_THREAT_MATCH_QUERY}`, () => {
getCustomIndicatorQueryInput().should('have.text', DEFAULT_THREAT_MATCH_QUERY);
});
it('Shows invalidation text if text is removed', () => {
getCustomIndicatorQueryInput().type('{selectall}{del}');
getThreatMatchQueryInvalidationText().should('exist');
});
});
// FLAKY: https://github.com/elastic/kibana/issues/182669
describe.skip('Indicator mapping', () => {
beforeEach(() => {
describe('Generating signals', () => {
it('Creates and enables a new Indicator Match rule', () => {
const rule = getNewThreatIndicatorRule();
visit(CREATE_RULE_URL);
selectIndicatorMatchType();
if (rule.index) {
fillIndexAndIndicatorIndexPattern(rule.index, rule.threat_index);
}
fillDefineIndicatorMatchRuleAndContinue(rule);
fillAboutRuleAndContinue(rule);
fillScheduleRuleAndContinue(rule);
createAndEnableRule();
openRuleManagementPageViaBreadcrumbs();
cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)');
expectNumberOfRules(RULES_MANAGEMENT_TABLE, expectedNumberOfRules);
cy.get(RULE_NAME).should('have.text', rule.name);
cy.get(RISK_SCORE).should('have.text', rule.risk_score);
cy.get(SEVERITY).should('have.text', 'Critical');
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
goToRuleDetailsOf(rule.name);
cy.get(RULE_NAME_HEADER).should('contain', `${rule.name}`);
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', rule.description);
cy.get(ABOUT_DETAILS).within(() => {
getDetails(SEVERITY_DETAILS).should('have.text', 'Critical');
getDetails(RISK_SCORE_DETAILS).should('have.text', rule.risk_score);
getDetails(INDICATOR_PREFIX_OVERRIDE).should('have.text', rule.threat_indicator_path);
getDetails(REFERENCE_URLS_DETAILS).should((details) => {
expect(removeExternalLinkText(details.text())).equal(expectedUrls);
});
getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives);
getDetails(MITRE_ATTACK_DETAILS).should((mitre) => {
expect(removeExternalLinkText(mitre.text())).equal(expectedMitre);
});
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
});
cy.get(INVESTIGATION_NOTES_TOGGLE).click();
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN);
cy.get(DEFINITION_DETAILS).within(() => {
if (rule.index) {
getDetails(INDEX_PATTERNS_DETAILS).should('have.text', rule.index.join(''));
}
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', '*:*');
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Indicator Match');
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
getDetails(INDICATOR_INDEX_PATTERNS).should('have.text', rule.threat_index.join(''));
getDetails(INDICATOR_MAPPING).should(
'have.text',
`${rule.threat_mapping[0].entries[0].field} MATCHES ${rule.threat_mapping[0].entries[0].value}`
);
getDetails(INDICATOR_INDEX_QUERY).should('have.text', '*:*');
});
cy.get(SCHEDULE_DETAILS).within(() => {
getDetails(RUNS_EVERY_DETAILS)
.find(INTERVAL_ABBR_VALUE)
.should('have.text', `${rule.interval}`);
const humanizedDuration = getHumanizedDuration(
rule.from ?? 'now-6m',
rule.interval ?? '5m'
);
getDetails(ADDITIONAL_LOOK_BACK_DETAILS)
.find(INTERVAL_ABBR_VALUE)
.should('have.text', `${humanizedDuration}`);
});
waitForTheRuleToBeExecuted();
waitForAlertsToPopulate();
cy.get(ALERTS_COUNT).should('have.text', expectedNumberOfAlerts);
cy.get(ALERT_RULE_NAME).first().should('have.text', rule.name);
cy.get(ALERT_SEVERITY).first().should('have.text', rule.severity?.toLowerCase());
cy.get(ALERT_RISK_SCORE).first().should('have.text', rule.risk_score);
});
it('Does NOT show invalidation text on initial page load', () => {
getIndicatorInvalidationText().should('not.exist');
});
it('Investigate alert in timeline', () => {
loadPrepackagedTimelineTemplates();
createRule(getNewThreatIndicatorRule({ rule_id: 'rule_testing', enabled: true })).then(
(rule) => visitRuleDetailsPage(rule.body.id)
);
it('Shows invalidation text when you try to press continue without filling anything out', () => {
getDefineContinueButton().click();
getIndicatorAtLeastOneInvalidationText().should('exist');
});
waitForAlertsToPopulate();
investigateFirstAlertInTimeline();
it('Shows invalidation text when the "AND" button is pressed and both the mappings are blank', () => {
getIndicatorAndButton().click();
getIndicatorInvalidationText().should('exist');
});
it('Shows invalidation text when the "OR" button is pressed and both the mappings are blank', () => {
getIndicatorOrButton().click();
getIndicatorInvalidationText().should('exist');
});
it('Does NOT show invalidation text when there is a valid "index field" and a valid "indicator index field"', () => {
fillIndicatorMatchRow({
indexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].field,
indicatorIndexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].value,
});
getDefineContinueButton().click();
getIndicatorInvalidationText().should('not.exist');
});
it('Shows invalidation text when there is an invalid "index field" and a valid "indicator index field"', () => {
fillIndicatorMatchRow({
indexField: 'non-existent-value',
indicatorIndexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].value,
validColumns: 'indicatorField',
});
getDefineContinueButton().click();
getIndicatorInvalidationText().should('exist');
});
it('Shows invalidation text when there is a valid "index field" and an invalid "indicator index field"', () => {
fillIndicatorMatchRow({
indexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].field,
indicatorIndexField: 'non-existent-value',
validColumns: 'indexField',
});
getDefineContinueButton().click();
getIndicatorInvalidationText().should('exist');
});
it('Deletes the first row when you have two rows. Both rows valid rows of "index fields" and valid "indicator index fields". The second row should become the first row', () => {
fillIndicatorMatchRow({
indexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].field,
indicatorIndexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].value,
});
getIndicatorAndButton().click();
fillIndicatorMatchRow({
rowNumber: 2,
indexField: 'agent.name',
indicatorIndexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].value,
validColumns: 'indicatorField',
});
getIndicatorDeleteButton().click();
getIndicatorIndexComboField().find('input').should('have.value', 'agent.name');
getIndicatorMappingComboField()
.find('input')
.should('have.value', getNewThreatIndicatorRule().threat_mapping[0].entries[0].value);
getIndicatorIndexComboField(2).should('not.exist');
getIndicatorMappingComboField(2).should('not.exist');
});
it('Deletes the first row when you have two rows. Both rows have valid "index fields" and invalid "indicator index fields". The second row should become the first row', () => {
fillIndicatorMatchRow({
indexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].field,
indicatorIndexField: 'non-existent-value',
validColumns: 'indexField',
});
getIndicatorAndButton().click();
fillIndicatorMatchRow({
rowNumber: 2,
indexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].field,
indicatorIndexField: 'second-non-existent-value',
validColumns: 'indexField',
});
getIndicatorDeleteButton().click();
getIndicatorMappingComboField()
.find('input')
.should('have.value', 'second-non-existent-value');
getIndicatorIndexComboField(2).should('not.exist');
getIndicatorMappingComboField(2).should('not.exist');
});
it('Deletes the first row when you have two rows. Both rows have valid "indicator index fields" and invalid "index fields". The second row should become the first row', () => {
fillIndicatorMatchRow({
indexField: 'non-existent-value',
indicatorIndexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].value,
validColumns: 'indicatorField',
});
getIndicatorAndButton().click();
fillIndicatorMatchRow({
rowNumber: 2,
indexField: 'second-non-existent-value',
indicatorIndexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].value,
validColumns: 'indicatorField',
});
getIndicatorDeleteButton().click();
getIndicatorIndexComboField()
.find('input')
.should('have.value', 'second-non-existent-value');
getIndicatorIndexComboField(2).should('not.exist');
getIndicatorMappingComboField(2).should('not.exist');
});
it('Deletes the first row of data but not the UI elements and the text defaults back to the placeholder of Search', () => {
fillIndicatorMatchRow({
indexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].field,
indicatorIndexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].value,
});
getIndicatorDeleteButton().click();
getIndicatorIndexComboField()
.find('input')
.should('value', '')
.should('have.attr', 'placeholder', 'Search');
getIndicatorMappingComboField()
.find('input')
.should('value', '')
.should('have.attr', 'placeholder', 'Search');
getIndicatorIndexComboField(2).should('not.exist');
getIndicatorMappingComboField(2).should('not.exist');
});
it('Deletes the second row when you have three rows. The first row is valid data, the second row is invalid data, and the third row is valid data. Third row should shift up correctly', () => {
fillIndicatorMatchRow({
indexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].field,
indicatorIndexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].value,
});
getIndicatorAndButton().click();
fillIndicatorMatchRow({
rowNumber: 2,
indexField: 'non-existent-value',
indicatorIndexField: 'non-existent-value',
validColumns: 'none',
});
getIndicatorAndButton().click();
fillIndicatorMatchRow({
rowNumber: 3,
indexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].field,
indicatorIndexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].value,
});
getIndicatorDeleteButton(2).click();
getIndicatorIndexComboField(1)
.find('input')
.should('value', getNewThreatIndicatorRule().threat_mapping[0].entries[0].field);
getIndicatorMappingComboField(1)
.find('input')
.should('value', getNewThreatIndicatorRule().threat_mapping[0].entries[0].value);
getIndicatorIndexComboField(2)
.find('input')
.should('value', getNewThreatIndicatorRule().threat_mapping[0].entries[0].field);
getIndicatorMappingComboField(2)
.find('input')
.should('value', getNewThreatIndicatorRule().threat_mapping[0].entries[0].value);
getIndicatorIndexComboField(3).should('not.exist');
getIndicatorMappingComboField(3).should('not.exist');
});
it('Can add two OR rows and delete the second row. The first row has invalid data and the second row has valid data. The first row is deleted and the second row shifts up correctly.', () => {
fillIndicatorMatchRow({
indexField: 'non-existent-value-one',
indicatorIndexField: 'non-existent-value-two',
validColumns: 'none',
});
getIndicatorOrButton().click();
fillIndicatorMatchRow({
rowNumber: 2,
indexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].field,
indicatorIndexField: getNewThreatIndicatorRule().threat_mapping[0].entries[0].value,
});
getIndicatorDeleteButton().click();
getIndicatorIndexComboField()
.find('input')
.should('value', getNewThreatIndicatorRule().threat_mapping[0].entries[0].field);
getIndicatorMappingComboField()
.find('input')
.should('value', getNewThreatIndicatorRule().threat_mapping[0].entries[0].value);
getIndicatorIndexComboField(2).should('not.exist');
getIndicatorMappingComboField(2).should('not.exist');
});
});
describe('Schedule', () => {
it('IM rule has 1h time interval and lookback by default', () => {
visit(CREATE_RULE_URL);
selectIndicatorMatchType();
fillDefineIndicatorMatchRuleAndContinue(getNewThreatIndicatorRule());
fillAboutRuleAndContinue(getNewThreatIndicatorRule());
cy.get(SCHEDULE_INTERVAL_AMOUNT_INPUT).invoke('val').should('eql', '1');
cy.get(SCHEDULE_INTERVAL_UNITS_INPUT).invoke('val').should('eql', 'h');
cy.get(SCHEDULE_LOOKBACK_AMOUNT_INPUT).invoke('val').should('eql', '5');
cy.get(SCHEDULE_LOOKBACK_UNITS_INPUT).invoke('val').should('eql', 'm');
});
});
});
describe('Generating signals', () => {
it('Creates and enables a new Indicator Match rule', () => {
const rule = getNewThreatIndicatorRule();
visit(CREATE_RULE_URL);
selectIndicatorMatchType();
fillDefineIndicatorMatchRuleAndContinue(rule);
fillAboutRuleAndContinue(rule);
fillScheduleRuleAndContinue(rule);
createAndEnableRule();
openRuleManagementPageViaBreadcrumbs();
cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)');
expectNumberOfRules(RULES_MANAGEMENT_TABLE, expectedNumberOfRules);
cy.get(RULE_NAME).should('have.text', rule.name);
cy.get(RISK_SCORE).should('have.text', rule.risk_score);
cy.get(SEVERITY).should('have.text', 'Critical');
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
goToRuleDetailsOf(rule.name);
cy.get(RULE_NAME_HEADER).should('contain', `${rule.name}`);
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', rule.description);
cy.get(ABOUT_DETAILS).within(() => {
getDetails(SEVERITY_DETAILS).should('have.text', 'Critical');
getDetails(RISK_SCORE_DETAILS).should('have.text', rule.risk_score);
getDetails(INDICATOR_PREFIX_OVERRIDE).should('have.text', rule.threat_indicator_path);
getDetails(REFERENCE_URLS_DETAILS).should((details) => {
expect(removeExternalLinkText(details.text())).equal(expectedUrls);
});
getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives);
getDetails(MITRE_ATTACK_DETAILS).should((mitre) => {
expect(removeExternalLinkText(mitre.text())).equal(expectedMitre);
});
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
});
cy.get(INVESTIGATION_NOTES_TOGGLE).click();
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN);
cy.get(DEFINITION_DETAILS).within(() => {
if (rule.index) {
getDetails(INDEX_PATTERNS_DETAILS).should('have.text', rule.index.join(''));
}
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', '*:*');
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Indicator Match');
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
getDetails(INDICATOR_INDEX_PATTERNS).should('have.text', rule.threat_index.join(''));
getDetails(INDICATOR_MAPPING).should(
cy.get(PROVIDER_BADGE).should('have.length', 3);
cy.get(PROVIDER_BADGE).should(
'have.text',
`${rule.threat_mapping[0].entries[0].field} MATCHES ${rule.threat_mapping[0].entries[0].value}`
`threat.enrichments.matched.atomic: "${
indicatorRuleMatchingDoc.atomic
}"threat.enrichments.matched.type: "indicator_match_rule"threat.enrichments.matched.field: "${
getNewThreatIndicatorRule().threat_mapping[0].entries[0].field
}"`
);
getDetails(INDICATOR_INDEX_QUERY).should('have.text', '*:*');
});
cy.get(SCHEDULE_DETAILS).within(() => {
getDetails(RUNS_EVERY_DETAILS)
.find(INTERVAL_ABBR_VALUE)
.should('have.text', `${rule.interval}`);
const humanizedDuration = getHumanizedDuration(
rule.from ?? 'now-6m',
rule.interval ?? '5m'
);
getDetails(ADDITIONAL_LOOK_BACK_DETAILS)
.find(INTERVAL_ABBR_VALUE)
.should('have.text', `${humanizedDuration}`);
});
waitForTheRuleToBeExecuted();
waitForAlertsToPopulate();
cy.get(ALERTS_COUNT).should('have.text', expectedNumberOfAlerts);
cy.get(ALERT_RULE_NAME).first().should('have.text', rule.name);
cy.get(ALERT_SEVERITY).first().should('have.text', rule.severity?.toLowerCase());
cy.get(ALERT_RISK_SCORE).first().should('have.text', rule.risk_score);
});
it('Investigate alert in timeline', () => {
loadPrepackagedTimelineTemplates();
createRule(getNewThreatIndicatorRule({ rule_id: 'rule_testing', enabled: true })).then(
(rule) => visitRuleDetailsPage(rule.body.id)
);
waitForAlertsToPopulate();
investigateFirstAlertInTimeline();
cy.get(PROVIDER_BADGE).should('have.length', 3);
cy.get(PROVIDER_BADGE).should(
'have.text',
`threat.enrichments.matched.atomic: "${
indicatorRuleMatchingDoc.atomic
}"threat.enrichments.matched.type: "indicator_match_rule"threat.enrichments.matched.field: "${
getNewThreatIndicatorRule().threat_mapping[0].entries[0].field
}"`
);
cy.get(INDICATOR_MATCH_ROW_RENDER).should(
'have.text',
`${getNewThreatIndicatorRule().threat_mapping[0].entries[0].field}matched${
indicatorRuleMatchingDoc.atomic
}indicator_match_ruleprovided` + ` byAbuseCH malware`
);
});
});
describe('Duplicates the indicator rule', () => {
const TESTED_RULE_DATA = getNewThreatIndicatorRule({
name: 'Indicator rule duplicate test',
rule_id: 'rule_testing',
enabled: false,
});
describe('on rule editing page', () => {
beforeEach(() => {
createRule(TESTED_RULE_DATA);
visit(RULES_MANAGEMENT_URL);
disableAutoRefresh();
});
it('Allows the rule to be duplicated from the table', () => {
duplicateFirstRule();
goBackToRuleDetails();
goBackToRulesTable();
checkDuplicatedRule(TESTED_RULE_DATA.name);
});
it("Allows the rule to be duplicated from the table's bulk actions", () => {
selectAllRules();
duplicateSelectedRulesWithExceptions();
checkDuplicatedRule(`${TESTED_RULE_DATA.name} [Duplicate]`);
});
});
describe('on rule details page', () => {
beforeEach(() => {
createRule(getNewThreatIndicatorRule(TESTED_RULE_DATA)).then((rule) =>
visitRuleDetailsPage(rule.body.id)
cy.get(INDICATOR_MATCH_ROW_RENDER).should(
'have.text',
`${getNewThreatIndicatorRule().threat_mapping[0].entries[0].field}matched${
indicatorRuleMatchingDoc.atomic
}indicator_match_ruleprovided` + ` byAbuseCH malware`
);
});
});
it('Allows the rule to be duplicated', () => {
duplicateRuleFromMenu();
goBackToRuleDetails();
goBackToRulesTable();
checkDuplicatedRule(`${TESTED_RULE_DATA.name} [Duplicate]`);
describe('Duplicates the indicator rule', () => {
const TESTED_RULE_DATA = getNewThreatIndicatorRule({
name: 'Indicator rule duplicate test',
rule_id: 'rule_testing',
enabled: false,
});
describe('on rule editing page', () => {
beforeEach(() => {
createRule(TESTED_RULE_DATA);
visit(RULES_MANAGEMENT_URL);
disableAutoRefresh();
});
it('Allows the rule to be duplicated from the table', () => {
duplicateFirstRule();
goBackToRuleDetails();
goBackToRulesTable();
checkDuplicatedRule(TESTED_RULE_DATA.name);
});
it("Allows the rule to be duplicated from the table's bulk actions", () => {
selectAllRules();
duplicateSelectedRulesWithExceptions();
checkDuplicatedRule(`${TESTED_RULE_DATA.name} [Duplicate]`);
});
});
describe('on rule details page', () => {
beforeEach(() => {
createRule(getNewThreatIndicatorRule(TESTED_RULE_DATA)).then((rule) =>
visitRuleDetailsPage(rule.body.id)
);
});
it('Allows the rule to be duplicated', () => {
duplicateRuleFromMenu();
goBackToRuleDetails();
goBackToRulesTable();
checkDuplicatedRule(`${TESTED_RULE_DATA.name} [Duplicate]`);
});
});
});
});
});
});
}
);

View file

@ -1,70 +0,0 @@
/*
* 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 { getNewThreatIndicatorRule } from '../../../../objects/rule';
import { DEFINITION_DETAILS, SUPPRESS_BY_DETAILS } from '../../../../screens/rule_details';
import {
fillDefineIndicatorMatchRule,
fillAlertSuppressionFields,
selectIndicatorMatchType,
fillAboutRuleMinimumAndContinue,
createRuleWithoutEnabling,
skipScheduleRuleAction,
continueFromDefineStep,
} from '../../../../tasks/create_new_rule';
import { login } from '../../../../tasks/login';
import { visit } from '../../../../tasks/navigation';
import { getDetails } from '../../../../tasks/rule_details';
import { CREATE_RULE_URL } from '../../../../urls/navigation';
import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
const SUPPRESS_BY_FIELDS = ['myhash.mysha256', 'source.ip.keyword'];
describe(
'Detection rules, Indicator Match, Alert Suppression',
{
tags: ['@serverless'],
env: {
ftrConfig: {
productTypes: [
{ product_line: 'security', product_tier: 'essentials' },
{ product_line: 'endpoint', product_tier: 'essentials' },
],
},
},
},
() => {
const rule = getNewThreatIndicatorRule();
beforeEach(() => {
cy.task('esArchiverLoad', { archiveName: 'threat_indicator' });
cy.task('esArchiverLoad', { archiveName: 'suspicious_source_event' });
deleteAlertsAndRules();
login();
});
it('creates rule with per rule execution suppression for essentials license', () => {
visit(CREATE_RULE_URL);
selectIndicatorMatchType();
fillDefineIndicatorMatchRule(rule);
// selecting only suppression fields, the rest options would be default
fillAlertSuppressionFields(SUPPRESS_BY_FIELDS);
continueFromDefineStep();
fillAboutRuleMinimumAndContinue(rule);
skipScheduleRuleAction();
createRuleWithoutEnabling();
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(SUPPRESS_BY_DETAILS).should('have.text', SUPPRESS_BY_FIELDS.join(''));
});
});
}
);

View file

@ -5,43 +5,12 @@
* 2.0.
*/
import { formatMitreAttackDescription, getHumanizedDuration } from '../../../../helpers/rules';
import { getMachineLearningRule } from '../../../../objects/rule';
import {
RISK_SCORE,
RULES_MANAGEMENT_TABLE,
RULE_NAME,
RULE_SWITCH,
SEVERITY,
} from '../../../../screens/alerts_detection_rules';
import {
ABOUT_DETAILS,
ABOUT_RULE_DESCRIPTION,
ADDITIONAL_LOOK_BACK_DETAILS,
ANOMALY_SCORE_DETAILS,
DEFINITION_DETAILS,
FALSE_POSITIVES_DETAILS,
removeExternalLinkText,
MACHINE_LEARNING_JOB_ID,
// MACHINE_LEARNING_JOB_STATUS,
MITRE_ATTACK_DETAILS,
REFERENCE_URLS_DETAILS,
RISK_SCORE_DETAILS,
RULE_NAME_HEADER,
RULE_TYPE_DETAILS,
RUNS_EVERY_DETAILS,
SCHEDULE_DETAILS,
SEVERITY_DETAILS,
TAGS_DETAILS,
TIMELINE_TEMPLATE_DETAILS,
INTERVAL_ABBR_VALUE,
} from '../../../../screens/rule_details';
import { RULE_NAME_HEADER } from '../../../../screens/rule_details';
import { getDetails } from '../../../../tasks/rule_details';
import { expectNumberOfRules, goToRuleDetailsOf } from '../../../../tasks/alerts_detection_rules';
import {
createAndEnableRule,
createRuleWithoutEnabling,
fillAboutRuleAndContinue,
fillDefineMachineLearningRuleAndContinue,
fillScheduleRuleAndContinue,
@ -49,22 +18,11 @@ import {
} from '../../../../tasks/create_new_rule';
import { login } from '../../../../tasks/login';
import { visit } from '../../../../tasks/navigation';
import { openRuleManagementPageViaBreadcrumbs } from '../../../../tasks/rules_management';
import { CREATE_RULE_URL } from '../../../../urls/navigation';
import { forceStopAndCloseJob } from '../../../../support/machine_learning';
import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
// https://github.com/elastic/kibana/issues/187622
describe('Machine Learning rules', { tags: ['@ess', '@serverless, @skipInServerlessMKI'] }, () => {
const expectedUrls = (getMachineLearningRule().references ?? []).join('');
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('');
describe('Machine Learning - Rule Creation', { tags: ['@ess', '@serverless'] }, () => {
before(() => {
const machineLearningJobIds = ([] as string[]).concat(
getMachineLearningRule().machine_learning_job_id
@ -79,60 +37,15 @@ describe('Machine Learning rules', { tags: ['@ess', '@serverless, @skipInServerl
visit(CREATE_RULE_URL);
});
it('Creates and enables a new ml rule', () => {
it('Creates a new ml rule', () => {
const mlRule = getMachineLearningRule();
selectMachineLearningRuleType();
fillDefineMachineLearningRuleAndContinue(mlRule);
fillAboutRuleAndContinue(mlRule);
fillScheduleRuleAndContinue(mlRule);
createAndEnableRule();
openRuleManagementPageViaBreadcrumbs();
createRuleWithoutEnabling();
expectNumberOfRules(RULES_MANAGEMENT_TABLE, 1);
cy.get(RULE_NAME).should('have.text', mlRule.name);
cy.get(RISK_SCORE).should('have.text', mlRule.risk_score);
cy.get(SEVERITY).should('have.text', 'Critical');
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
goToRuleDetailsOf(mlRule.name);
cy.get(RULE_NAME_HEADER).should('contain', `${mlRule.name}`);
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', mlRule.description);
cy.get(ABOUT_DETAILS).within(() => {
getDetails(SEVERITY_DETAILS).should('have.text', 'Critical');
getDetails(RISK_SCORE_DETAILS).should('have.text', mlRule.risk_score);
getDetails(REFERENCE_URLS_DETAILS).should((details) => {
expect(removeExternalLinkText(details.text())).equal(expectedUrls);
});
getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives);
getDetails(MITRE_ATTACK_DETAILS).should((mitre) => {
expect(removeExternalLinkText(mitre.text())).equal(expectedMitre);
});
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
});
cy.get(DEFINITION_DETAILS).within(() => {
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');
// 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', expectedJobText);
});
cy.get(SCHEDULE_DETAILS).within(() => {
getDetails(RUNS_EVERY_DETAILS)
.find(INTERVAL_ABBR_VALUE)
.should('have.text', `${mlRule.interval}`);
const humanizedDuration = getHumanizedDuration(
mlRule.from ?? 'now-6m',
mlRule.interval ?? '5m'
);
getDetails(ADDITIONAL_LOOK_BACK_DETAILS)
.find(INTERVAL_ABBR_VALUE)
.should('have.text', `${humanizedDuration}`);
});
cy.log('Asserting we have a new rule created');
cy.get(RULE_NAME_HEADER).should('contain', mlRule.name);
});
});

View file

@ -5,197 +5,44 @@
* 2.0.
*/
import { formatMitreAttackDescription, getHumanizedDuration } from '../../../../helpers/rules';
import { getIndexPatterns, getNewTermsRule } from '../../../../objects/rule';
import { getNewTermsRule } from '../../../../objects/rule';
import { ALERT_DATA_GRID } from '../../../../screens/alerts';
import {
CUSTOM_RULES_BTN,
RISK_SCORE,
RULES_MANAGEMENT_TABLE,
RULE_NAME,
RULE_SWITCH,
SEVERITY,
} from '../../../../screens/alerts_detection_rules';
import {
ABOUT_DETAILS,
ABOUT_INVESTIGATION_NOTES,
ABOUT_RULE_DESCRIPTION,
ADDITIONAL_LOOK_BACK_DETAILS,
CUSTOM_QUERY_DETAILS,
DEFINITION_DETAILS,
FALSE_POSITIVES_DETAILS,
removeExternalLinkText,
INDEX_PATTERNS_DETAILS,
INVESTIGATION_NOTES_MARKDOWN,
INVESTIGATION_NOTES_TOGGLE,
MITRE_ATTACK_DETAILS,
REFERENCE_URLS_DETAILS,
RISK_SCORE_DETAILS,
RULE_NAME_HEADER,
RULE_TYPE_DETAILS,
RUNS_EVERY_DETAILS,
SCHEDULE_DETAILS,
SEVERITY_DETAILS,
TAGS_DETAILS,
TIMELINE_TEMPLATE_DETAILS,
NEW_TERMS_HISTORY_WINDOW_DETAILS,
NEW_TERMS_FIELDS_DETAILS,
SUPPRESS_BY_DETAILS,
SUPPRESS_FOR_DETAILS,
SUPPRESS_MISSING_FIELD,
INTERVAL_ABBR_VALUE,
} from '../../../../screens/rule_details';
import { RULE_NAME_HEADER } from '../../../../screens/rule_details';
import { getDetails } from '../../../../tasks/rule_details';
import { expectNumberOfRules, goToRuleDetailsOf } from '../../../../tasks/alerts_detection_rules';
import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
import {
createAndEnableRule,
fillAboutRuleAndContinue,
fillDefineNewTermsRule,
fillDefineNewTermsRuleAndContinue,
fillScheduleRuleAndContinue,
selectNewTermsRuleType,
waitForAlertsToPopulate,
fillAlertSuppressionFields,
fillAboutRuleMinimumAndContinue,
createRuleWithoutEnabling,
skipScheduleRuleAction,
continueFromDefineStep,
selectAlertSuppressionPerInterval,
setAlertSuppressionDuration,
selectDoNotSuppressForMissingFields,
} from '../../../../tasks/create_new_rule';
import { login } from '../../../../tasks/login';
import { visit } from '../../../../tasks/navigation';
import { CREATE_RULE_URL } from '../../../../urls/navigation';
import { openRuleManagementPageViaBreadcrumbs } from '../../../../tasks/rules_management';
// Skipped in MKI due to flake
describe(
'New Terms rules',
'New Terms Rule - Rule Creation',
{
tags: ['@ess', '@serverless', '@skipInServerlessMKI'],
tags: ['@ess', '@serverless'],
},
() => {
describe('Detection rules, New Terms', () => {
const rule = getNewTermsRule();
const expectedUrls = rule.references?.join('');
const expectedFalsePositives = rule.false_positives?.join('');
const expectedTags = rule.tags?.join('');
const mitreAttack = rule.threat;
const expectedMitre = formatMitreAttackDescription(mitreAttack ?? []);
const expectedNumberOfRules = 1;
const rule = getNewTermsRule();
beforeEach(() => {
deleteAlertsAndRules();
login();
visit(CREATE_RULE_URL);
selectNewTermsRuleType();
});
beforeEach(() => {
deleteAlertsAndRules();
login();
visit(CREATE_RULE_URL);
selectNewTermsRuleType();
});
it('Creates a new terms rule', function () {
fillDefineNewTermsRuleAndContinue(rule);
fillAboutRuleAndContinue(rule);
fillScheduleRuleAndContinue(rule);
createRuleWithoutEnabling();
it('Creates and enables a new terms rule', function () {
fillDefineNewTermsRuleAndContinue(rule);
fillAboutRuleAndContinue(rule);
fillScheduleRuleAndContinue(rule);
createAndEnableRule();
openRuleManagementPageViaBreadcrumbs();
cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)');
expectNumberOfRules(RULES_MANAGEMENT_TABLE, expectedNumberOfRules);
cy.get(RULE_NAME).should('have.text', rule.name);
cy.get(RISK_SCORE).should('have.text', rule.risk_score);
cy.get(SEVERITY).should('have.text', 'High');
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
goToRuleDetailsOf(rule.name);
cy.get(RULE_NAME_HEADER).should('contain', `${rule.name}`);
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', rule.description);
cy.get(ABOUT_DETAILS).within(() => {
getDetails(SEVERITY_DETAILS).should('have.text', 'High');
getDetails(RISK_SCORE_DETAILS).should('have.text', rule.risk_score);
getDetails(REFERENCE_URLS_DETAILS).should((details) => {
expect(removeExternalLinkText(details.text())).equal(expectedUrls);
});
getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives);
getDetails(MITRE_ATTACK_DETAILS).should((mitre) => {
expect(removeExternalLinkText(mitre.text())).equal(expectedMitre);
});
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
});
cy.get(INVESTIGATION_NOTES_TOGGLE).click();
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN);
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(INDEX_PATTERNS_DETAILS).should('have.text', getIndexPatterns().join(''));
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', rule.query);
getDetails(RULE_TYPE_DETAILS).should('have.text', 'New Terms');
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
getDetails(NEW_TERMS_FIELDS_DETAILS).should('have.text', 'host.name');
getDetails(NEW_TERMS_HISTORY_WINDOW_DETAILS).should('have.text', '51000h');
});
cy.get(SCHEDULE_DETAILS).within(() => {
getDetails(RUNS_EVERY_DETAILS)
.find(INTERVAL_ABBR_VALUE)
.should('have.text', `${rule.interval}`);
const humanizedDuration = getHumanizedDuration(
rule.from ?? 'now-6m',
rule.interval ?? '5m'
);
getDetails(ADDITIONAL_LOOK_BACK_DETAILS)
.find(INTERVAL_ABBR_VALUE)
.should('have.text', `${humanizedDuration}`);
});
waitForAlertsToPopulate();
cy.get(ALERT_DATA_GRID)
.invoke('text')
.then((text) => {
expect(text).contains(rule.name);
expect(text).contains(rule.severity);
expect(text).contains(rule.risk_score);
});
});
it('Creates rule rule with time interval suppression', () => {
const SUPPRESS_BY_FIELDS = ['agent.hostname', 'agent.type'];
fillDefineNewTermsRule(rule);
// fill suppress by fields and select non-default suppression options
fillAlertSuppressionFields(SUPPRESS_BY_FIELDS);
selectAlertSuppressionPerInterval();
setAlertSuppressionDuration(45, 'm');
selectDoNotSuppressForMissingFields();
continueFromDefineStep();
// ensures details preview works correctly
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(SUPPRESS_BY_DETAILS).should('have.text', SUPPRESS_BY_FIELDS.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'
);
});
fillAboutRuleMinimumAndContinue(rule);
skipScheduleRuleAction();
createRuleWithoutEnabling();
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(SUPPRESS_BY_DETAILS).should('have.text', SUPPRESS_BY_FIELDS.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'
);
});
});
cy.log('Asserting we have a new rule created');
cy.get(RULE_NAME_HEADER).should('contain', rule.name);
});
}
);

View file

@ -5,157 +5,35 @@
* 2.0.
*/
import { formatMitreAttackDescription, getHumanizedDuration } from '../../../../helpers/rules';
import {
getIndexPatterns,
getNewOverrideRule,
getSeveritiesOverride,
} from '../../../../objects/rule';
import { ALERT_GRID_CELL, ALERTS_COUNT } from '../../../../screens/alerts';
import {
CUSTOM_RULES_BTN,
RISK_SCORE,
RULES_MANAGEMENT_TABLE,
RULE_NAME,
RULE_SWITCH,
SEVERITY,
} from '../../../../screens/alerts_detection_rules';
import {
ABOUT_INVESTIGATION_NOTES,
ABOUT_DETAILS,
ABOUT_RULE_DESCRIPTION,
ADDITIONAL_LOOK_BACK_DETAILS,
CUSTOM_QUERY_DETAILS,
DEFINITION_DETAILS,
DETAILS_DESCRIPTION,
DETAILS_TITLE,
FALSE_POSITIVES_DETAILS,
removeExternalLinkText,
INDEX_PATTERNS_DETAILS,
INVESTIGATION_NOTES_MARKDOWN,
INVESTIGATION_NOTES_TOGGLE,
MITRE_ATTACK_DETAILS,
REFERENCE_URLS_DETAILS,
RISK_SCORE_DETAILS,
RISK_SCORE_OVERRIDE_DETAILS,
RULE_NAME_HEADER,
RULE_NAME_OVERRIDE_DETAILS,
RULE_TYPE_DETAILS,
RUNS_EVERY_DETAILS,
SCHEDULE_DETAILS,
SEVERITY_DETAILS,
TAGS_DETAILS,
TIMELINE_TEMPLATE_DETAILS,
TIMESTAMP_OVERRIDE_DETAILS,
INTERVAL_ABBR_VALUE,
} from '../../../../screens/rule_details';
import { getNewOverrideRule } from '../../../../objects/rule';
import { RULE_NAME_HEADER } from '../../../../screens/rule_details';
import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
import { expectNumberOfRules, goToRuleDetailsOf } from '../../../../tasks/alerts_detection_rules';
import {
createAndEnableRule,
createRuleWithoutEnabling,
fillAboutRuleWithOverrideAndContinue,
fillDefineCustomRuleAndContinue,
fillScheduleRuleAndContinue,
waitForAlertsToPopulate,
} from '../../../../tasks/create_new_rule';
import { login } from '../../../../tasks/login';
import { visit } from '../../../../tasks/navigation';
import { getDetails } from '../../../../tasks/rule_details';
import { CREATE_RULE_URL } from '../../../../urls/navigation';
import { openRuleManagementPageViaBreadcrumbs } from '../../../../tasks/rules_management';
describe('Rules override', { tags: ['@ess', '@serverless'] }, () => {
describe('Rule Overrides - Rule Creation', { tags: ['@ess', '@serverless'] }, () => {
const rule = getNewOverrideRule();
const expectedUrls = rule.references?.join('');
const expectedFalsePositives = rule.false_positives?.join('');
const expectedTags = rule.tags?.join('');
const mitreAttack = rule.threat;
const expectedMitre = formatMitreAttackDescription(mitreAttack ?? []);
beforeEach(() => {
login();
deleteAlertsAndRules();
login();
visit(CREATE_RULE_URL);
});
it('Creates and enables a new custom rule with override option', function () {
visit(CREATE_RULE_URL);
it('Creates a new custom rule with override options', function () {
fillDefineCustomRuleAndContinue(rule);
fillAboutRuleWithOverrideAndContinue(rule);
fillScheduleRuleAndContinue(rule);
createAndEnableRule();
openRuleManagementPageViaBreadcrumbs();
createRuleWithoutEnabling();
cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)');
expectNumberOfRules(RULES_MANAGEMENT_TABLE, 1);
cy.get(RULE_NAME).should('have.text', rule.name);
cy.get(RISK_SCORE).should('have.text', rule.risk_score);
cy.get(SEVERITY).should('have.text', 'High');
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
goToRuleDetailsOf(rule.name);
cy.get(RULE_NAME_HEADER).should('contain', `${rule.name}`);
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', rule.description);
cy.get(ABOUT_DETAILS).within(() => {
getDetails(SEVERITY_DETAILS).should('have.text', 'High');
getDetails(RISK_SCORE_DETAILS).should('have.text', rule.risk_score);
getDetails(RISK_SCORE_OVERRIDE_DETAILS).should(
'have.text',
`${rule.risk_score_mapping?.[0].field}kibana.alert.risk_score`
);
getDetails(RULE_NAME_OVERRIDE_DETAILS).should('have.text', rule.rule_name_override);
getDetails(REFERENCE_URLS_DETAILS).should((details) => {
expect(removeExternalLinkText(details.text())).equal(expectedUrls);
});
getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives);
getDetails(MITRE_ATTACK_DETAILS).should((mitre) => {
expect(removeExternalLinkText(mitre.text())).equal(expectedMitre);
});
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
getDetails(TIMESTAMP_OVERRIDE_DETAILS).should('have.text', rule.timestamp_override);
cy.contains(DETAILS_TITLE, 'Severity override')
.invoke('index', DETAILS_TITLE) // get index relative to other titles, not all siblings
.then((severityOverrideIndex) => {
rule.severity_mapping?.forEach((severity, i) => {
cy.get(DETAILS_DESCRIPTION)
.eq(severityOverrideIndex + i)
.should(
'have.text',
`${severity.field}:${severity.value}${getSeveritiesOverride()[i]}`
);
});
});
});
cy.get(INVESTIGATION_NOTES_TOGGLE).click();
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN);
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(INDEX_PATTERNS_DETAILS).should('have.text', getIndexPatterns().join(''));
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', rule.query);
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query');
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
});
cy.get(SCHEDULE_DETAILS).within(() => {
getDetails(RUNS_EVERY_DETAILS)
.find(INTERVAL_ABBR_VALUE)
.should('have.text', `${rule.interval}`);
const humanizedDuration = getHumanizedDuration(rule.from ?? 'now-6m', rule.interval ?? '5m');
getDetails(ADDITIONAL_LOOK_BACK_DETAILS)
.find(INTERVAL_ABBR_VALUE)
.should('have.text', `${humanizedDuration}`);
});
waitForAlertsToPopulate();
cy.get(ALERTS_COUNT)
.invoke('text')
.should('match', /^[1-9].+$/); // Any number of alerts
cy.get(ALERT_GRID_CELL).contains('winlogbeat');
cy.get(ALERT_GRID_CELL).contains('high');
cy.get(ALERT_GRID_CELL).contains('80');
cy.log('Asserting we have a new rule created');
cy.get(RULE_NAME_HEADER).should('contain', rule.name);
});
});

View file

@ -5,80 +5,28 @@
* 2.0.
*/
import { formatMitreAttackDescription, getHumanizedDuration } from '../../../../helpers/rules';
import { getIndexPatterns, getNewThresholdRule } from '../../../../objects/rule';
import { ALERTS_COUNT, ALERT_GRID_CELL } from '../../../../screens/alerts';
import {
CUSTOM_RULES_BTN,
RISK_SCORE,
RULES_MANAGEMENT_TABLE,
RULE_NAME,
RULE_SWITCH,
SEVERITY,
} from '../../../../screens/alerts_detection_rules';
import {
ABOUT_DETAILS,
ABOUT_INVESTIGATION_NOTES,
ABOUT_RULE_DESCRIPTION,
ADDITIONAL_LOOK_BACK_DETAILS,
CUSTOM_QUERY_DETAILS,
FALSE_POSITIVES_DETAILS,
DEFINITION_DETAILS,
removeExternalLinkText,
INDEX_PATTERNS_DETAILS,
INVESTIGATION_NOTES_MARKDOWN,
INVESTIGATION_NOTES_TOGGLE,
MITRE_ATTACK_DETAILS,
REFERENCE_URLS_DETAILS,
RISK_SCORE_DETAILS,
RULE_NAME_HEADER,
RULE_TYPE_DETAILS,
RUNS_EVERY_DETAILS,
SCHEDULE_DETAILS,
SEVERITY_DETAILS,
TAGS_DETAILS,
THRESHOLD_DETAILS,
TIMELINE_TEMPLATE_DETAILS,
SUPPRESS_FOR_DETAILS,
INTERVAL_ABBR_VALUE,
} from '../../../../screens/rule_details';
import { expectNumberOfRules, goToRuleDetailsOf } from '../../../../tasks/alerts_detection_rules';
import { getNewThresholdRule } from '../../../../objects/rule';
import { RULE_NAME_HEADER } from '../../../../screens/rule_details';
import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
import {
createAndEnableRule,
createRuleWithoutEnabling,
fillAboutRuleMinimumAndContinue,
enablesAndPopulatesThresholdSuppression,
skipScheduleRuleAction,
fillAboutRuleAndContinue,
fillDefineThresholdRuleAndContinue,
fillScheduleRuleAndContinue,
selectThresholdRuleType,
waitForAlertsToPopulate,
fillDefineThresholdRule,
continueFromDefineStep,
} from '../../../../tasks/create_new_rule';
import { login } from '../../../../tasks/login';
import { visit } from '../../../../tasks/navigation';
import { getDetails, assertDetailsNotExist } from '../../../../tasks/rule_details';
import { openRuleManagementPageViaBreadcrumbs } from '../../../../tasks/rules_management';
import { CREATE_RULE_URL } from '../../../../urls/navigation';
describe(
'Threshold rules',
'Threshold Rule - Rule Creation',
{
tags: ['@ess', '@serverless'],
},
() => {
const rule = getNewThresholdRule();
const expectedUrls = rule.references?.join('');
const expectedFalsePositives = rule.false_positives?.join('');
const expectedTags = rule.tags?.join('');
const mitreAttack = rule.threat;
const expectedMitre = formatMitreAttackDescription(mitreAttack ?? []);
beforeEach(() => {
deleteAlertsAndRules();
@ -91,87 +39,10 @@ describe(
fillDefineThresholdRuleAndContinue(rule);
fillAboutRuleAndContinue(rule);
fillScheduleRuleAndContinue(rule);
createAndEnableRule();
openRuleManagementPageViaBreadcrumbs();
cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)');
expectNumberOfRules(RULES_MANAGEMENT_TABLE, 1);
cy.get(RULE_NAME).should('have.text', rule.name);
cy.get(RISK_SCORE).should('have.text', rule.risk_score);
cy.get(SEVERITY).should('have.text', 'High');
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
goToRuleDetailsOf(rule.name);
cy.get(RULE_NAME_HEADER).should('contain', `${rule.name}`);
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', rule.description);
cy.get(ABOUT_DETAILS).within(() => {
getDetails(SEVERITY_DETAILS).should('have.text', 'High');
getDetails(RISK_SCORE_DETAILS).should('have.text', rule.risk_score);
getDetails(REFERENCE_URLS_DETAILS).should((details) => {
expect(removeExternalLinkText(details.text())).equal(expectedUrls);
});
getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives);
getDetails(MITRE_ATTACK_DETAILS).should((mitre) => {
expect(removeExternalLinkText(mitre.text())).equal(expectedMitre);
});
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
});
cy.get(INVESTIGATION_NOTES_TOGGLE).click();
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN);
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(INDEX_PATTERNS_DETAILS).should('have.text', getIndexPatterns().join(''));
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', rule.query);
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Threshold');
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
getDetails(THRESHOLD_DETAILS).should(
'have.text',
`Results aggregated by ${rule.threshold.field} >= ${rule.threshold.value}`
);
assertDetailsNotExist(SUPPRESS_FOR_DETAILS);
});
cy.get(SCHEDULE_DETAILS).within(() => {
getDetails(RUNS_EVERY_DETAILS)
.find(INTERVAL_ABBR_VALUE)
.should('have.text', `${rule.interval}`);
const humanizedDuration = getHumanizedDuration(
rule.from ?? 'now-6m',
rule.interval ?? '5m'
);
getDetails(ADDITIONAL_LOOK_BACK_DETAILS)
.find(INTERVAL_ABBR_VALUE)
.should('have.text', `${humanizedDuration}`);
});
waitForAlertsToPopulate();
cy.get(ALERTS_COUNT).should(($count) => expect(+$count.text().split(' ')[0]).to.be.lt(100));
cy.get(ALERT_GRID_CELL).contains(rule.name);
});
it('Creates a new threshold rule with suppression enabled', () => {
selectThresholdRuleType();
fillDefineThresholdRule(rule);
enablesAndPopulatesThresholdSuppression(5, 'h');
continueFromDefineStep();
// ensures duration displayed on define step in preview mode
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(SUPPRESS_FOR_DETAILS).should('have.text', '5h');
});
fillAboutRuleMinimumAndContinue(rule);
skipScheduleRuleAction();
createRuleWithoutEnabling();
openRuleManagementPageViaBreadcrumbs();
goToRuleDetailsOf(rule.name);
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(SUPPRESS_FOR_DETAILS).should('have.text', '5h');
});
cy.log('Asserting we have a new rule created');
cy.get(RULE_NAME_HEADER).should('contain', rule.name);
});
}
);

View file

@ -0,0 +1,186 @@
/*
* 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 { getNewRule, getSavedQueryRule } from '../../../../objects/rule';
import { LOAD_QUERY_DYNAMICALLY_CHECKBOX } from '../../../../screens/create_new_rule';
import { TOASTER } from '../../../../screens/alerts_detection_rules';
import {
SAVED_QUERY_NAME_DETAILS,
SAVED_QUERY_DETAILS,
DEFINE_RULE_PANEL_PROGRESS,
CUSTOM_QUERY_DETAILS,
} from '../../../../screens/rule_details';
import { editFirstRule } from '../../../../tasks/alerts_detection_rules';
import { createSavedQuery, deleteSavedQueries } from '../../../../tasks/api_calls/saved_queries';
import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
import {
selectAndLoadSavedQuery,
getCustomQueryInput,
checkLoadQueryDynamically,
uncheckLoadQueryDynamically,
} from '../../../../tasks/create_new_rule';
import { saveEditedRule, visitEditRulePage } from '../../../../tasks/edit_rule';
import { login } from '../../../../tasks/login';
import { visit } from '../../../../tasks/navigation';
import { assertDetailsNotExist, getDetails } from '../../../../tasks/rule_details';
import { createRule } from '../../../../tasks/api_calls/rules';
import { RULES_MANAGEMENT_URL } from '../../../../urls/rules_management';
const savedQueryName = 'custom saved query';
const savedQueryQuery = 'process.name: test';
describe('Saved query rules, rule edit', { tags: ['@ess', '@serverless'] }, () => {
beforeEach(() => {
login();
deleteAlertsAndRules();
deleteSavedQueries();
});
it('Allows to update query rule as saved_query rule type', () => {
createSavedQuery(savedQueryName, savedQueryQuery);
createRule(getNewRule()).then((rule) => visitEditRulePage(rule.body.id));
selectAndLoadSavedQuery(savedQueryName, savedQueryQuery);
checkLoadQueryDynamically();
cy.intercept('PUT', '/api/detection_engine/rules').as('editedRule');
saveEditedRule();
cy.wait('@editedRule').then(({ response }) => {
// updated rule should be saved as saved_query type once Load query dynamically checkbox was checked
cy.wrap(response?.body.type).should('equal', 'saved_query');
});
cy.get(DEFINE_RULE_PANEL_PROGRESS).should('not.exist');
getDetails(SAVED_QUERY_NAME_DETAILS).should('contain', savedQueryName);
getDetails(SAVED_QUERY_DETAILS).should('contain', savedQueryQuery);
});
it('Allows to update saved_query rule as query rule type', () => {
const expectedCustomTestQuery = 'random test query';
createSavedQuery(savedQueryName, savedQueryQuery).then((response) => {
cy.log(JSON.stringify(response.body, null, 2));
createRule(getSavedQueryRule({ saved_id: response.body.id, query: undefined })).then((rule) =>
visitEditRulePage(rule.body.id)
);
});
// query input should be disabled and has value of saved query
getCustomQueryInput().should('have.value', savedQueryQuery).should('be.disabled');
// after unchecking Load Query Dynamically checkbox, query input becomes enabled, type custom query
uncheckLoadQueryDynamically();
getCustomQueryInput().should('be.enabled').clear().type(expectedCustomTestQuery);
cy.intercept('PUT', '/api/detection_engine/rules').as('editedRule');
saveEditedRule();
cy.wait('@editedRule').then(({ response }) => {
// updated rule should be saved as query type once Load query dynamically checkbox was unchecked
cy.wrap(response?.body.type).should('equal', 'query');
});
getDetails(CUSTOM_QUERY_DETAILS).should('contain', expectedCustomTestQuery);
});
it('Allows to update saved_query rule with non-existent query by adding custom query', () => {
const expectedCustomTestQuery = 'random test query';
createRule(getSavedQueryRule({ saved_id: 'non-existent', query: undefined })).then((rule) =>
visitEditRulePage(rule.body.id)
);
uncheckLoadQueryDynamically();
// type custom query, ensure Load dynamically checkbox is absent, as rule can't be saved win non valid saved query
getCustomQueryInput().type(expectedCustomTestQuery);
cy.get(LOAD_QUERY_DYNAMICALLY_CHECKBOX).should('not.visible');
cy.intercept('PUT', '/api/detection_engine/rules').as('editedRule');
saveEditedRule();
cy.wait('@editedRule').then(({ response }) => {
// updated rule should be saved as query type once Load query dynamically checkbox was unchecked
cy.wrap(response?.body.type).should('equal', 'query');
});
getDetails(CUSTOM_QUERY_DETAILS).should('contain', expectedCustomTestQuery);
});
it('Allows to update saved_query rule with non-existent query by selecting another saved query', () => {
createSavedQuery(savedQueryName, savedQueryQuery);
createRule(getSavedQueryRule({ saved_id: 'non-existent', query: undefined })).then((rule) =>
visitEditRulePage(rule.body.id)
);
visit(RULES_MANAGEMENT_URL);
editFirstRule();
uncheckLoadQueryDynamically();
// select another saved query, edit query input, which later should be dismissed once Load query dynamically checkbox checked
selectAndLoadSavedQuery(savedQueryName, savedQueryQuery);
getCustomQueryInput().type('AND this part wil be dismissed');
checkLoadQueryDynamically();
getCustomQueryInput().should('have.value', savedQueryQuery);
cy.intercept('PUT', '/api/detection_engine/rules').as('editedRule');
saveEditedRule();
cy.wait('@editedRule').then(({ response }) => {
// updated rule type shouldn't change
cy.wrap(response?.body.type).should('equal', 'saved_query');
});
cy.get(DEFINE_RULE_PANEL_PROGRESS).should('not.exist');
getDetails(SAVED_QUERY_NAME_DETAILS).should('contain', savedQueryName);
getDetails(SAVED_QUERY_DETAILS).should('contain', savedQueryQuery);
});
context('Non existent saved query', () => {
const FAILED_TO_LOAD_ERROR = 'Failed to load the saved query';
beforeEach(() => {
createRule(
getSavedQueryRule({
saved_id: 'non-existent',
query: undefined,
})
).then((rule) => visitEditRulePage(rule.body.id));
});
it('Shows validation error on rule edit when saved query can not be loaded', function () {
cy.get(TOASTER).should('contain', FAILED_TO_LOAD_ERROR);
});
// https://github.com/elastic/kibana/issues/187623
it(
'Allows to update saved_query rule with non-existent query',
{ tags: ['@skipInServerlessMKI'] },
() => {
cy.get(LOAD_QUERY_DYNAMICALLY_CHECKBOX).should('exist');
cy.intercept('PUT', '/api/detection_engine/rules').as('editedRule');
saveEditedRule();
cy.wait('@editedRule').then(({ response }) => {
// updated rule type shouldn't change
cy.wrap(response?.body.type).should('equal', 'saved_query');
});
cy.get(DEFINE_RULE_PANEL_PROGRESS).should('not.exist');
assertDetailsNotExist(SAVED_QUERY_NAME_DETAILS);
assertDetailsNotExist(SAVED_QUERY_DETAILS);
}
);
});
});