[DE][Cypress] Re-enable cypress serverless tests subset (#166501)

## Summary

Re-enable a subset of cypress serverless tests:

- Restructured
`x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation/custom_query_rule.cy.ts`
to break it down into and enable the following tests:
    - `rule_creation/common_components.cy.ts`
    - `rule_creation/custom_query_rule.cy.ts`
    - `rule_details/common_components.cy.ts`
    - `rule_details/custom_query_rule.cy.ts`
    - `rule_edit/custom_query_rule.cy.ts`
This commit is contained in:
Yara Tercero 2023-10-01 23:10:14 -04:00 committed by GitHub
parent 07f1c36df7
commit 50ddc979ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 579 additions and 449 deletions

3
.github/CODEOWNERS vendored
View file

@ -1230,7 +1230,7 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib
/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules @elastic/security-detection-rule-management
/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management @elastic/security-detection-rule-management
/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_details @elastic/security-detection-rule-management
/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules @elastic/security-detection-rule-management
/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/rule_management @elastic/security-detection-rule-management
@ -1294,6 +1294,7 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib
/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts @elastic/security-detection-engine
/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_actions @elastic/security-detection-engine
/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation @elastic/security-detection-engine
/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_edit @elastic/security-detection-engine
/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/value_lists @elastic/security-detection-engine
/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics @elastic/security-detection-engine
/x-pack/test/security_solution_cypress/cypress/e2e/exceptions @elastic/security-detection-engine

View file

@ -19,6 +19,7 @@ interface MultiSelectAutocompleteProps {
field: FieldHook;
fullWidth?: boolean;
disabledText?: string;
dataTestSubj?: string;
}
const FIELD_COMBO_BOX_WIDTH = 410;
@ -31,6 +32,7 @@ export const MultiSelectAutocompleteComponent: React.FC<MultiSelectAutocompleteP
isDisabled,
field,
fullWidth = false,
dataTestSubj,
}: MultiSelectAutocompleteProps) => {
const fieldEuiFieldProps = useMemo(
() => ({
@ -45,7 +47,12 @@ export const MultiSelectAutocompleteComponent: React.FC<MultiSelectAutocompleteP
[browserFields, isDisabled, fullWidth]
);
const fieldComponent = (
<Field field={field} idAria={fieldDescribedByIds} euiFieldProps={fieldEuiFieldProps} />
<Field
field={field}
idAria={fieldDescribedByIds}
euiFieldProps={fieldEuiFieldProps}
data-test-subj={dataTestSubj}
/>
);
return isDisabled ? (
<EuiToolTip position="right" content={disabledText}>

View file

@ -249,6 +249,7 @@ const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({
browserFields: indexPattern.fields,
isDisabled: isLoading || indexPatternLoading,
fullWidth: true,
dataTestSubj: 'detectionEngineStepAboutRuleInvestigationFields',
}}
/>
<EuiSpacer size="l" />

View file

@ -18,6 +18,7 @@ import type {
import type {
IndexPatternArray,
InvestigationGuide,
InvestigationFields,
RuleDescription,
RuleFalsePositiveArray,
RuleQuery,
@ -30,6 +31,7 @@ interface RuleFields {
defaultIndexPatterns: IndexPatternArray;
falsePositives: RuleFalsePositiveArray;
investigationGuide: InvestigationGuide;
investigationFields: InvestigationFields;
referenceUrls: RuleReferenceArray;
riskScore: RiskScore;
ruleDescription: RuleDescription;
@ -58,6 +60,9 @@ export const ruleFields: RuleFields = {
],
falsePositives: ['False1', 'False2'],
investigationGuide: '# test markdown',
investigationFields: {
field_names: ['agent.hostname'],
},
referenceUrls: ['http://example.com/', 'https://example.com/'],
riskScore: 17,
ruleDescription: 'The rule description',

View file

@ -0,0 +1,100 @@
/*
* 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 { ruleFields } from '../../../data/detection_engine';
import { getTimeline } from '../../../objects/timeline';
import {
ABOUT_CONTINUE_BTN,
ABOUT_EDIT_BUTTON,
CUSTOM_QUERY_INPUT,
DEFINE_CONTINUE_BUTTON,
DEFINE_EDIT_BUTTON,
RULE_NAME_INPUT,
SCHEDULE_CONTINUE_BUTTON,
} from '../../../screens/create_new_rule';
import { RULE_NAME_HEADER } from '../../../screens/rule_details';
import { createTimeline } from '../../../tasks/api_calls/timelines';
import { deleteAlertsAndRules } from '../../../tasks/common';
import {
createAndEnableRuleOnly,
expandAdvancedSettings,
fillCustomInvestigationFields,
fillDescription,
fillFalsePositiveExamples,
fillFrom,
fillNote,
fillReferenceUrls,
fillRiskScore,
fillRuleName,
fillRuleTags,
fillSeverity,
fillThreat,
fillThreatSubtechnique,
fillThreatTechnique,
importSavedQuery,
} from '../../../tasks/create_new_rule';
import { login } from '../../../tasks/login';
import { CREATE_RULE_URL } from '../../../urls/navigation';
import { visit } from '../../../tasks/navigation';
// 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
// in the creation form. For any rule type specific functionalities, please include
// them in the relevant /rule_creation/[RULE_TYPE].cy.ts test.
describe('Common rule creation flows', { tags: ['@ess', '@serverless'] }, () => {
beforeEach(() => {
deleteAlertsAndRules();
createTimeline(getTimeline())
.then((response) => {
return response.body.data.persistTimeline.timeline.savedObjectId;
})
.as('timelineId');
login();
visit(CREATE_RULE_URL);
});
it('Creates and enables a rule', function () {
cy.log('Filling define section');
importSavedQuery(this.timelineId);
cy.get(DEFINE_CONTINUE_BUTTON).click();
cy.log('Filling about section');
fillRuleName();
fillDescription();
fillSeverity();
fillRiskScore();
fillRuleTags();
expandAdvancedSettings();
fillReferenceUrls();
fillFalsePositiveExamples();
fillThreat();
fillThreatTechnique();
fillThreatSubtechnique();
fillCustomInvestigationFields();
fillNote();
cy.get(ABOUT_CONTINUE_BTN).click();
cy.log('Filling schedule section');
fillFrom();
// expect define step to repopulate
cy.get(DEFINE_EDIT_BUTTON).click();
cy.get(CUSTOM_QUERY_INPUT).should('have.value', ruleFields.ruleQuery);
cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click();
// expect about step to populate
cy.get(ABOUT_EDIT_BUTTON).click();
cy.get(RULE_NAME_INPUT).invoke('val').should('eql', ruleFields.ruleName);
cy.get(ABOUT_CONTINUE_BTN).should('exist').click();
cy.get(SCHEDULE_CONTINUE_BUTTON).click();
createAndEnableRuleOnly();
cy.get(RULE_NAME_HEADER).should('contain', ruleFields.ruleName);
});
});

View file

@ -5,464 +5,42 @@
* 2.0.
*/
import { ruleFields } from '../../../data/detection_engine';
import {
getNewRule,
getExistingRule,
getEditedRule,
getNewOverrideRule,
} from '../../../objects/rule';
import { getTimeline } from '../../../objects/timeline';
import { ALERTS_COUNT, ALERT_GRID_CELL } from '../../../screens/alerts';
import { getNewRule } from '../../../objects/rule';
import { RULE_NAME_HEADER } from '../../../screens/rule_details';
import { deleteAlertsAndRules } from '../../../tasks/common';
import {
CUSTOM_RULES_BTN,
RISK_SCORE,
RULE_NAME,
RULES_MANAGEMENT_TABLE,
RULE_SWITCH,
SEVERITY,
} from '../../../screens/alerts_detection_rules';
import {
ACTIONS_NOTIFY_WHEN_BUTTON,
ACTIONS_SUMMARY_BUTTON,
} from '../../../screens/common/rule_actions';
import {
ABOUT_CONTINUE_BTN,
ABOUT_EDIT_BUTTON,
CUSTOM_QUERY_INPUT,
DEFINE_CONTINUE_BUTTON,
DEFINE_EDIT_BUTTON,
DEFINE_INDEX_INPUT,
DEFAULT_RISK_SCORE_INPUT,
RULE_DESCRIPTION_INPUT,
RULE_NAME_INPUT,
SCHEDULE_INTERVAL_AMOUNT_INPUT,
SCHEDULE_INTERVAL_UNITS_INPUT,
SCHEDULE_CONTINUE_BUTTON,
SEVERITY_DROPDOWN,
TAGS_CLEAR_BUTTON,
TAGS_FIELD,
} 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,
REFERENCE_URLS_DETAILS,
RISK_SCORE_DETAILS,
RULE_NAME_HEADER,
RULE_TYPE_DETAILS,
RUNS_EVERY_DETAILS,
SCHEDULE_DETAILS,
SEVERITY_DETAILS,
TAGS_DETAILS,
TIMELINE_TEMPLATE_DETAILS,
THREAT_TACTIC,
THREAT_TECHNIQUE,
THREAT_SUBTECHNIQUE,
} from '../../../screens/rule_details';
import {
deleteFirstRule,
deleteRuleFromDetailsPage,
expectManagementTableRules,
getRulesManagementTableRows,
goToRuleDetailsOf,
selectRulesByName,
} from '../../../tasks/alerts_detection_rules';
import { deleteSelectedRules } from '../../../tasks/rules_bulk_actions';
import { createRule, findAllRules } from '../../../tasks/api_calls/rules';
import { createTimeline } from '../../../tasks/api_calls/timelines';
import { deleteAlertsAndRules, deleteConnectors } from '../../../tasks/common';
import { addEmailConnectorAndRuleAction } from '../../../tasks/common/rule_actions';
import {
createAndEnableRule,
expandAdvancedSettings,
fillAboutRule,
fillDescription,
fillFalsePositiveExamples,
fillFrom,
fillNote,
fillReferenceUrls,
fillRiskScore,
fillRuleName,
fillRuleTags,
fillSeverity,
fillThreat,
fillThreatSubtechnique,
fillThreatTechnique,
goToAboutStepTab,
goToActionsStepTab,
goToScheduleStepTab,
importSavedQuery,
waitForAlertsToPopulate,
createAndEnableRuleOnly,
fillScheduleRuleAndContinue,
fillAboutRuleMinimumAndContinue,
fillDefineCustomRuleAndContinue,
} from '../../../tasks/create_new_rule';
import { saveEditedRule, visitEditRulePage } from '../../../tasks/edit_rule';
import { login } from '../../../tasks/login';
import { visitWithTimeRange } from '../../../tasks/navigation';
import {
enablesRule,
getDetails,
visitRuleDetailsPage,
waitForTheRuleToBeExecuted,
} from '../../../tasks/rule_details';
import { visit } from '../../../tasks/navigation';
import { CREATE_RULE_URL } from '../../../urls/navigation';
import { visitRulesManagementTable } from '../../../tasks/rules_management';
// TODO: https://github.com/elastic/kibana/issues/161539
describe('Custom query rules', { tags: ['@ess', '@serverless', '@brokenInServerless'] }, () => {
describe('Create custom query rule', { tags: ['@ess', '@serverless'] }, () => {
const rule = getNewRule();
beforeEach(() => {
deleteAlertsAndRules();
});
describe('Custom detection rules creation', () => {
beforeEach(() => {
createTimeline(getTimeline())
.then((response) => {
return response.body.data.persistTimeline.timeline.savedObjectId;
})
.as('timelineId');
deleteAlertsAndRules();
login();
visit(CREATE_RULE_URL);
});
it('Creates and enables a new rule', function () {
visitWithTimeRange(CREATE_RULE_URL);
cy.log('Filling define section');
importSavedQuery(this.timelineId);
cy.get(DEFINE_CONTINUE_BUTTON).click();
cy.log('Filling about section');
fillRuleName('Test Rule');
fillDescription();
fillSeverity();
fillRiskScore();
fillRuleTags();
expandAdvancedSettings();
fillReferenceUrls();
fillFalsePositiveExamples();
fillThreat();
fillThreatTechnique();
fillThreatSubtechnique();
fillNote();
cy.get(ABOUT_CONTINUE_BTN).click();
cy.log('Filling schedule section');
fillFrom();
// expect define step to repopulate
cy.get(DEFINE_EDIT_BUTTON).click();
cy.get(CUSTOM_QUERY_INPUT).should('have.value', ruleFields.ruleQuery);
cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click();
// expect about step to populate
cy.get(ABOUT_EDIT_BUTTON).click();
cy.get(RULE_NAME_INPUT).invoke('val').should('eql', ruleFields.ruleName);
cy.get(ABOUT_CONTINUE_BTN).should('exist').click();
cy.get(SCHEDULE_CONTINUE_BUTTON).click();
createAndEnableRule();
it('Creates and enables a rule', function () {
fillDefineCustomRuleAndContinue(rule);
fillAboutRuleMinimumAndContinue(rule);
fillScheduleRuleAndContinue(rule);
createAndEnableRuleOnly();
cy.log('Asserting we have a new rule created');
cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)');
cy.log('Asserting rule view in rules list');
expectManagementTableRules(['Test Rule']);
cy.get(RULE_NAME).should('have.text', ruleFields.ruleName);
cy.get(RISK_SCORE).should('have.text', ruleFields.riskScore);
cy.get(SEVERITY)
.invoke('text')
.then((text) => {
cy.wrap(text.toLowerCase()).should('equal', ruleFields.ruleSeverity);
});
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');
goToRuleDetailsOf(ruleFields.ruleName);
cy.log('Asserting rule details');
cy.get(RULE_NAME_HEADER).should('contain', ruleFields.ruleName);
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', ruleFields.ruleDescription);
cy.get(ABOUT_DETAILS).within(() => {
getDetails(SEVERITY_DETAILS)
.invoke('text')
.then((text) => {
cy.wrap(text.toLowerCase()).should('equal', ruleFields.ruleSeverity);
});
getDetails(RISK_SCORE_DETAILS).should('have.text', ruleFields.riskScore);
getDetails(REFERENCE_URLS_DETAILS).should((details) => {
expect(removeExternalLinkText(details.text())).equal(ruleFields.referenceUrls.join(''));
});
getDetails(FALSE_POSITIVES_DETAILS).should('have.text', ruleFields.falsePositives.join(''));
getDetails(TAGS_DETAILS).should('have.text', ruleFields.ruleTags.join(''));
});
cy.get(THREAT_TACTIC).should(
'contain',
`${ruleFields.threat.tactic.name} (${ruleFields.threat.tactic.id})`
);
cy.get(THREAT_TECHNIQUE).should(
'contain',
`${ruleFields.threatTechnique.name} (${ruleFields.threatTechnique.id})`
);
cy.get(THREAT_SUBTECHNIQUE).should(
'contain',
`${ruleFields.threatSubtechnique.name} (${ruleFields.threatSubtechnique.id})`
);
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', 'auditbeat-*');
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', ruleFields.ruleQuery);
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).should('have.text', ruleFields.ruleInterval);
getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should('have.text', ruleFields.ruleIntervalFrom);
});
waitForTheRuleToBeExecuted();
waitForAlertsToPopulate();
cy.log('Asserting that alerts have been generated after the creation');
cy.get(ALERTS_COUNT)
.invoke('text')
.should('match', /^[1-9].+$/); // Any number of alerts
cy.get(ALERT_GRID_CELL).contains(ruleFields.ruleName);
});
});
describe('Custom detection rules deletion and edition', () => {
context('Deletion', () => {
const TESTED_RULE_DATA = getNewRule({
rule_id: 'rule1',
name: 'New Rule Test',
enabled: false,
max_signals: 500,
});
beforeEach(() => {
createRule(TESTED_RULE_DATA);
createRule(
getNewOverrideRule({
rule_id: 'rule2',
name: 'Override Rule',
enabled: false,
max_signals: 500,
})
);
createRule(getExistingRule({ rule_id: 'rule3', name: 'Rule 1', enabled: false }));
login();
visitRulesManagementTable();
});
it('Deletes one rule', () => {
getRulesManagementTableRows().then((rules) => {
const initialNumberOfRules = rules.length;
const expectedNumberOfRulesAfterDeletion = initialNumberOfRules - 1;
findAllRules().then(({ body }) => {
const numberOfRules = body.data.length;
expect(numberOfRules).to.eql(initialNumberOfRules);
});
deleteFirstRule();
getRulesManagementTableRows().should('have.length', expectedNumberOfRulesAfterDeletion);
findAllRules().then(({ body }) => {
const numberOfRules = body.data.length;
expect(numberOfRules).to.eql(expectedNumberOfRulesAfterDeletion);
});
cy.get(CUSTOM_RULES_BTN).should(
'have.text',
`Custom rules (${expectedNumberOfRulesAfterDeletion})`
);
});
});
it('Deletes more than one rule', () => {
getRulesManagementTableRows().then((rules) => {
const rulesToDelete = [TESTED_RULE_DATA.name, 'Override Rule'] as const;
const initialNumberOfRules = rules.length;
const numberOfRulesToBeDeleted = 2;
const expectedNumberOfRulesAfterDeletion =
initialNumberOfRules - numberOfRulesToBeDeleted;
selectRulesByName(rulesToDelete);
deleteSelectedRules();
getRulesManagementTableRows()
.first()
.within(() => {
cy.get(RULE_SWITCH).should('not.exist');
});
getRulesManagementTableRows().should('have.length', expectedNumberOfRulesAfterDeletion);
findAllRules().then(({ body }) => {
const numberOfRules = body.data.length;
expect(numberOfRules).to.eql(expectedNumberOfRulesAfterDeletion);
});
getRulesManagementTableRows()
.first()
.within(() => {
cy.get(RULE_SWITCH).should('exist');
});
cy.get(CUSTOM_RULES_BTN).should(
'have.text',
`Custom rules (${expectedNumberOfRulesAfterDeletion})`
);
});
});
it('Deletes one rule from detail page', () => {
getRulesManagementTableRows().then((rules) => {
const initialNumberOfRules = rules.length;
const expectedNumberOfRulesAfterDeletion = initialNumberOfRules - 1;
goToRuleDetailsOf(TESTED_RULE_DATA.name);
cy.intercept('POST', '/api/detection_engine/rules/_bulk_delete').as('deleteRule');
deleteRuleFromDetailsPage();
// @ts-expect-error update types
cy.waitFor('@deleteRule').then(() => {
cy.get(RULES_MANAGEMENT_TABLE).should('exist');
getRulesManagementTableRows().should('have.length', expectedNumberOfRulesAfterDeletion);
findAllRules().then(({ body }) => {
const numberOfRules = body.data.length;
expect(numberOfRules).to.eql(expectedNumberOfRulesAfterDeletion);
});
cy.get(CUSTOM_RULES_BTN).should(
'have.text',
`Custom rules (${expectedNumberOfRulesAfterDeletion})`
);
});
});
});
});
context('Edition', () => {
const editedRuleData = getEditedRule();
const expectedEditedTags = editedRuleData.tags?.join('');
const expectedEditedIndexPatterns = editedRuleData.index;
describe('on rule details page', () => {
beforeEach(() => {
deleteConnectors();
login();
createRule(getExistingRule({ rule_id: 'rule1', enabled: true })).then((rule) =>
visitRuleDetailsPage(rule.body.id)
);
});
it('Only modifies rule active status on enable/disable', () => {
enablesRule();
cy.intercept('GET', `/api/detection_engine/rules?id=*`).as('fetchRuleDetails');
cy.wait('@fetchRuleDetails').then(({ response }) => {
cy.wrap(response?.statusCode).should('eql', 200);
cy.wrap(response?.body.max_signals).should('eql', getExistingRule().max_signals);
cy.wrap(response?.body.enabled).should('eql', false);
});
});
});
describe('on rule editing page', () => {
beforeEach(() => {
deleteConnectors();
login();
createRule(getExistingRule({ rule_id: 'rule1', enabled: true })).then((rule) =>
visitEditRulePage(rule.body.id)
);
});
it('Allows a rule to be edited', () => {
const existingRule = getExistingRule();
// expect define step to populate
cy.get(CUSTOM_QUERY_INPUT).should('have.value', existingRule.query);
cy.get(DEFINE_INDEX_INPUT).should('have.text', existingRule.index?.join(''));
goToAboutStepTab();
// expect about step to populate
cy.get(RULE_NAME_INPUT).invoke('val').should('eql', existingRule.name);
cy.get(RULE_DESCRIPTION_INPUT).should('have.text', existingRule.description);
cy.get(TAGS_FIELD).should('have.text', existingRule.tags?.join(''));
cy.get(SEVERITY_DROPDOWN).should('have.text', 'High');
cy.get(DEFAULT_RISK_SCORE_INPUT)
.invoke('val')
.should('eql', `${existingRule.risk_score}`);
goToScheduleStepTab();
// expect schedule step to populate
const interval = existingRule.interval;
const intervalParts = interval != null && interval.match(/[0-9]+|[a-zA-Z]+/g);
if (intervalParts) {
const [amount, unit] = intervalParts;
cy.get(SCHEDULE_INTERVAL_AMOUNT_INPUT).invoke('val').should('eql', amount);
cy.get(SCHEDULE_INTERVAL_UNITS_INPUT).invoke('val').should('eql', unit);
} else {
throw new Error('Cannot assert scheduling info on a rule without an interval');
}
goToActionsStepTab();
addEmailConnectorAndRuleAction('test@example.com', 'Subject');
cy.get(ACTIONS_SUMMARY_BUTTON).should('have.text', 'Summary of alerts');
cy.get(ACTIONS_NOTIFY_WHEN_BUTTON).should('have.text', 'Per rule run');
goToAboutStepTab();
cy.get(TAGS_CLEAR_BUTTON).click();
fillAboutRule(getEditedRule());
cy.intercept('GET', '/api/detection_engine/rules?id*').as('getRule');
saveEditedRule();
cy.wait('@getRule').then(({ response }) => {
cy.wrap(response?.statusCode).should('eql', 200);
// ensure that editing rule does not modify max_signals
cy.wrap(response?.body.max_signals).should('eql', existingRule.max_signals);
});
cy.get(RULE_NAME_HEADER).should('contain', `${getEditedRule().name}`);
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', getEditedRule().description);
cy.get(ABOUT_DETAILS).within(() => {
getDetails(SEVERITY_DETAILS).should('have.text', 'Medium');
getDetails(RISK_SCORE_DETAILS).should('have.text', `${getEditedRule().risk_score}`);
getDetails(TAGS_DETAILS).should('have.text', expectedEditedTags);
});
cy.get(INVESTIGATION_NOTES_TOGGLE).click();
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', getEditedRule().note);
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(INDEX_PATTERNS_DETAILS).should(
'have.text',
expectedEditedIndexPatterns?.join('')
);
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', getEditedRule().query);
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query');
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
});
if (getEditedRule().interval) {
cy.get(SCHEDULE_DETAILS).within(() => {
getDetails(RUNS_EVERY_DETAILS).should('have.text', getEditedRule().interval);
});
}
});
});
cy.get(RULE_NAME_HEADER).should('contain', rule.name);
});
});
});

View file

@ -0,0 +1,180 @@
/*
* 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 { deleteRuleFromDetailsPage } from '../../../tasks/alerts_detection_rules';
import {
CUSTOM_RULES_BTN,
RULES_MANAGEMENT_TABLE,
RULES_ROW,
} from '../../../screens/alerts_detection_rules';
import { createRule } from '../../../tasks/api_calls/rules';
import { getDetails } from '../../../tasks/rule_details';
import { ruleFields } from '../../../data/detection_engine';
import { getTimeline } from '../../../objects/timeline';
import { getExistingRule, getNewRule } from '../../../objects/rule';
import {
ABOUT_DETAILS,
ABOUT_INVESTIGATION_NOTES,
ABOUT_RULE_DESCRIPTION,
ADDITIONAL_LOOK_BACK_DETAILS,
CUSTOM_QUERY_DETAILS,
DEFINITION_DETAILS,
FALSE_POSITIVES_DETAILS,
INDEX_PATTERNS_DETAILS,
INVESTIGATION_NOTES_MARKDOWN,
INVESTIGATION_NOTES_TOGGLE,
REFERENCE_URLS_DETAILS,
removeExternalLinkText,
RISK_SCORE_DETAILS,
RULE_NAME_HEADER,
RULE_SWITCH,
RULE_TYPE_DETAILS,
RUNS_EVERY_DETAILS,
SCHEDULE_DETAILS,
SEVERITY_DETAILS,
TAGS_DETAILS,
THREAT_SUBTECHNIQUE,
THREAT_TACTIC,
THREAT_TECHNIQUE,
TIMELINE_TEMPLATE_DETAILS,
} from '../../../screens/rule_details';
import { createTimeline } from '../../../tasks/api_calls/timelines';
import { cleanKibana, deleteAlertsAndRules, deleteConnectors } from '../../../tasks/common';
import { login } from '../../../tasks/login';
import { visit } from '../../../tasks/navigation';
import { ruleDetailsUrl } from '../../../urls/rule_details';
// This test is meant to test all common aspects of the rule details page that should function
// the same regardless of rule type. For any rule type specific functionalities, please include
// them in the relevant /rule_details/[RULE_TYPE].cy.ts test.
describe('Common rule detail flows', { tags: ['@ess', '@serverless'] }, () => {
before(() => {
cleanKibana();
});
beforeEach(() => {
deleteAlertsAndRules();
deleteConnectors();
login();
createTimeline(getTimeline()).then((response) => {
createRule({
...getNewRule({
rule_id: 'rulez',
description: ruleFields.ruleDescription,
name: ruleFields.ruleName,
severity: ruleFields.ruleSeverity,
risk_score: ruleFields.riskScore,
tags: ruleFields.ruleTags,
false_positives: ruleFields.falsePositives,
note: ruleFields.investigationGuide,
timeline_id: response.body.data.persistTimeline.timeline.savedObjectId,
timeline_title: response.body.data.persistTimeline.timeline.title ?? '',
interval: ruleFields.ruleInterval,
from: `now-1h`,
query: ruleFields.ruleQuery,
enabled: false,
max_signals: 500,
threat: [
{
...ruleFields.threat,
technique: [
{
...ruleFields.threatTechnique,
subtechnique: [ruleFields.threatSubtechnique],
},
],
},
],
}),
}).then((rule) => {
visit(ruleDetailsUrl(rule.body.id));
});
});
});
it('Only modifies rule active status on enable/disable', () => {
cy.get(RULE_NAME_HEADER).should('contain', ruleFields.ruleName);
cy.intercept('POST', '/api/detection_engine/rules/_bulk_action?dry_run=false').as(
'bulk_action'
);
cy.get(RULE_SWITCH).should('be.visible');
cy.get(RULE_SWITCH).click();
cy.wait('@bulk_action').then(({ response }) => {
cy.wrap(response?.statusCode).should('eql', 200);
cy.wrap(response?.body.attributes.results.updated[0].max_signals).should(
'eql',
getExistingRule().max_signals
);
cy.wrap(response?.body.attributes.results.updated[0].enabled).should('eql', true);
});
});
it('Displays rule details', function () {
cy.get(RULE_NAME_HEADER).should('contain', ruleFields.ruleName);
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', ruleFields.ruleDescription);
cy.get(ABOUT_DETAILS).within(() => {
getDetails(SEVERITY_DETAILS)
.invoke('text')
.then((text) => {
cy.wrap(text.toLowerCase()).should('equal', ruleFields.ruleSeverity);
});
getDetails(RISK_SCORE_DETAILS).should('have.text', ruleFields.riskScore);
getDetails(REFERENCE_URLS_DETAILS).should((details) => {
expect(removeExternalLinkText(details.text())).equal(ruleFields.referenceUrls.join(''));
});
getDetails(FALSE_POSITIVES_DETAILS).should('have.text', ruleFields.falsePositives.join(''));
getDetails(TAGS_DETAILS).should('have.text', ruleFields.ruleTags.join(''));
});
cy.get(THREAT_TACTIC).should(
'contain',
`${ruleFields.threat.tactic.name} (${ruleFields.threat.tactic.id})`
);
cy.get(THREAT_TECHNIQUE).should(
'contain',
`${ruleFields.threatTechnique.name} (${ruleFields.threatTechnique.id})`
);
cy.get(THREAT_SUBTECHNIQUE).should(
'contain',
`${ruleFields.threatSubtechnique.name} (${ruleFields.threatSubtechnique.id})`
);
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',
ruleFields.defaultIndexPatterns.join('')
);
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', ruleFields.ruleQuery);
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query');
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'Security Timeline');
});
cy.get(SCHEDULE_DETAILS).within(() => {
getDetails(RUNS_EVERY_DETAILS).should('have.text', ruleFields.ruleInterval);
getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should('have.text', '55m');
});
});
it('Deletes one rule from detail page', () => {
cy.intercept('POST', '/api/detection_engine/rules/_bulk_delete').as('deleteRule');
deleteRuleFromDetailsPage();
// @ts-expect-error update types
cy.waitFor('@deleteRule').then(() => {
cy.get(RULES_MANAGEMENT_TABLE).should('exist');
cy.get(RULES_MANAGEMENT_TABLE).find(RULES_ROW).should('have.length', 0);
cy.request({ url: '/api/detection_engine/rules/_find' }).then(({ body }) => {
const numberOfRules = body.data.length;
expect(numberOfRules).to.eql(0);
});
cy.get(CUSTOM_RULES_BTN).should('have.text', `Custom rules (${0})`);
});
});
});

View file

@ -0,0 +1,143 @@
/*
* 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 { getExistingRule, getEditedRule } from '../../../objects/rule';
import {
ACTIONS_NOTIFY_WHEN_BUTTON,
ACTIONS_SUMMARY_BUTTON,
} from '../../../screens/common/rule_actions';
import {
CUSTOM_QUERY_INPUT,
DEFINE_INDEX_INPUT,
DEFAULT_RISK_SCORE_INPUT,
RULE_DESCRIPTION_INPUT,
RULE_NAME_INPUT,
SCHEDULE_INTERVAL_AMOUNT_INPUT,
SCHEDULE_INTERVAL_UNITS_INPUT,
SEVERITY_DROPDOWN,
TAGS_CLEAR_BUTTON,
TAGS_FIELD,
} from '../../../screens/create_new_rule';
import {
ABOUT_DETAILS,
ABOUT_INVESTIGATION_NOTES,
ABOUT_RULE_DESCRIPTION,
CUSTOM_QUERY_DETAILS,
DEFINITION_DETAILS,
INDEX_PATTERNS_DETAILS,
INVESTIGATION_NOTES_TOGGLE,
RISK_SCORE_DETAILS,
RULE_NAME_HEADER,
RULE_TYPE_DETAILS,
RUNS_EVERY_DETAILS,
SCHEDULE_DETAILS,
SEVERITY_DETAILS,
TAGS_DETAILS,
TIMELINE_TEMPLATE_DETAILS,
} from '../../../screens/rule_details';
import { createRule } from '../../../tasks/api_calls/rules';
import { deleteAlertsAndRules, deleteConnectors } from '../../../tasks/common';
import { addEmailConnectorAndRuleAction } from '../../../tasks/common/rule_actions';
import {
fillAboutRule,
goToAboutStepTab,
goToActionsStepTab,
goToScheduleStepTab,
} from '../../../tasks/create_new_rule';
import { saveEditedRule, visitEditRulePage } from '../../../tasks/edit_rule';
import { login } from '../../../tasks/login';
import { getDetails } from '../../../tasks/rule_details';
describe('Custom query rules', { tags: ['@ess', '@serverless'] }, () => {
const rule = getEditedRule();
const expectedEditedtags = rule.tags?.join('');
const expectedEditedIndexPatterns = rule.index;
beforeEach(() => {
deleteConnectors();
deleteAlertsAndRules();
login();
createRule(getExistingRule({ rule_id: 'rule1', enabled: true })).then((createdRule) => {
visitEditRulePage(createdRule.body.id);
});
});
it('Allows a rule to be edited', () => {
const existingRule = getExistingRule();
// expect define step to populate
cy.get(CUSTOM_QUERY_INPUT).should('have.value', existingRule.query);
cy.get(DEFINE_INDEX_INPUT).should('have.text', existingRule.index?.join(''));
goToAboutStepTab();
// expect about step to populate
cy.get(RULE_NAME_INPUT).invoke('val').should('eql', existingRule.name);
cy.get(RULE_DESCRIPTION_INPUT).should('have.text', existingRule.description);
cy.get(TAGS_FIELD).should('have.text', existingRule.tags?.join(''));
cy.get(SEVERITY_DROPDOWN).should('have.text', 'High');
cy.get(DEFAULT_RISK_SCORE_INPUT).invoke('val').should('eql', `${existingRule.risk_score}`);
goToScheduleStepTab();
// expect schedule step to populate
const interval = existingRule.interval;
const intervalParts = interval != null && interval.match(/[0-9]+|[a-zA-Z]+/g);
if (intervalParts) {
const [amount, unit] = intervalParts;
cy.get(SCHEDULE_INTERVAL_AMOUNT_INPUT).invoke('val').should('eql', amount);
cy.get(SCHEDULE_INTERVAL_UNITS_INPUT).invoke('val').should('eql', unit);
} else {
throw new Error('Cannot assert scheduling info on a rule without an interval');
}
goToActionsStepTab();
addEmailConnectorAndRuleAction('test@example.com', 'Subject');
cy.get(ACTIONS_SUMMARY_BUTTON).should('have.text', 'Summary of alerts');
cy.get(ACTIONS_NOTIFY_WHEN_BUTTON).should('have.text', 'Per rule run');
goToAboutStepTab();
cy.get(TAGS_CLEAR_BUTTON).click();
fillAboutRule(getEditedRule());
cy.intercept('GET', '/api/detection_engine/rules?id*').as('getRule');
saveEditedRule();
cy.wait('@getRule').then(({ response }) => {
cy.wrap(response?.statusCode).should('eql', 200);
// ensure that editing rule does not modify max_signals
cy.wrap(response?.body.max_signals).should('eql', existingRule.max_signals);
});
cy.get(RULE_NAME_HEADER).should('contain', `${getEditedRule().name}`);
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', getEditedRule().description);
cy.get(ABOUT_DETAILS).within(() => {
getDetails(SEVERITY_DETAILS).should('have.text', 'Medium');
getDetails(RISK_SCORE_DETAILS).should('have.text', `${getEditedRule().risk_score}`);
getDetails(TAGS_DETAILS).should('have.text', expectedEditedtags);
});
cy.get(INVESTIGATION_NOTES_TOGGLE).click();
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', getEditedRule().note);
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(INDEX_PATTERNS_DETAILS).should('have.text', expectedEditedIndexPatterns?.join(''));
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', getEditedRule().query);
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query');
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
});
if (getEditedRule().interval) {
cy.get(SCHEDULE_DETAILS).within(() => {
getDetails(RUNS_EVERY_DETAILS).should('have.text', getEditedRule().interval);
});
}
});
});

View file

@ -0,0 +1,89 @@
/*
* 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 { visitRulesManagementTable } from '../../../../../tasks/rules_management';
import { getNewRule } from '../../../../../objects/rule';
import { RULE_SWITCH } from '../../../../../screens/alerts_detection_rules';
import {
deleteFirstRule,
getRulesManagementTableRows,
selectRulesByName,
} from '../../../../../tasks/alerts_detection_rules';
import { deleteSelectedRules } from '../../../../../tasks/rules_bulk_actions';
import { createRule, findAllRules } from '../../../../../tasks/api_calls/rules';
import { deleteAlertsAndRules } from '../../../../../tasks/common';
import { login } from '../../../../../tasks/login';
describe('Rule deletion', { tags: ['@ess', '@serverless', '@skipInServerless'] }, () => {
const testRules = [
getNewRule({ rule_id: 'rule1', name: 'Rule 1', enabled: false }),
getNewRule({ rule_id: 'rule2', name: 'Rule 2', enabled: false }),
getNewRule({ rule_id: 'rule3', name: 'Rule 3', enabled: false }),
];
beforeEach(() => {
deleteAlertsAndRules();
createRule(testRules[0]);
createRule(testRules[1]);
createRule(testRules[2]);
login();
visitRulesManagementTable();
});
it('User can delete an individual rule', () => {
getRulesManagementTableRows().then((rules) => {
const initialNumberOfRules = rules.length;
const expectedNumberOfRulesAfterDeletion = initialNumberOfRules - 1;
findAllRules().then(({ body }) => {
const numberOfRules = body.data.length;
expect(numberOfRules).to.eql(initialNumberOfRules);
});
deleteFirstRule();
getRulesManagementTableRows().should('have.length', expectedNumberOfRulesAfterDeletion);
findAllRules().then(({ body }) => {
const numberOfRules = body.data.length;
expect(numberOfRules).to.eql(expectedNumberOfRulesAfterDeletion);
});
});
});
it('User can delete multiple selected rules via a bulk action', () => {
getRulesManagementTableRows().then((rules) => {
const rulesToDelete = ['Rule 1', 'Rule 2'] as const;
const initialNumberOfRules = rules.length;
const numberOfRulesToBeDeleted = 2;
const expectedNumberOfRulesAfterDeletion = initialNumberOfRules - numberOfRulesToBeDeleted;
selectRulesByName(rulesToDelete);
deleteSelectedRules();
// During deletion, rule switch is not shown and instead there's loading spinner
getRulesManagementTableRows()
.first()
.within(() => {
cy.get(RULE_SWITCH).should('not.exist');
});
getRulesManagementTableRows().should('have.length', expectedNumberOfRulesAfterDeletion);
findAllRules().then(({ body }) => {
const numberOfRules = body.data.length;
expect(numberOfRules).to.eql(expectedNumberOfRulesAfterDeletion);
});
// Once bulk delete is done and one rule remains, checking for enable/disable switch to exist
getRulesManagementTableRows()
.first()
.within(() => {
cy.get(RULE_SWITCH).should('exist');
});
});
});
});

View file

@ -204,6 +204,9 @@ export const TAGS_INPUT =
export const TAGS_CLEAR_BUTTON =
'[data-test-subj="detectionEngineStepAboutRuleTags"] [data-test-subj="comboBoxClearButton"]';
export const INVESTIGATIONS_INPUT =
'[data-test-subj="detectionEngineStepAboutRuleInvestigationFields"] [data-test-subj="comboBoxSearchInput"]';
export const THRESHOLD_INPUT_AREA = '[data-test-subj="thresholdInput"]';
export const THRESHOLD_TYPE = '[data-test-subj="thresholdRuleType"]';

View file

@ -142,7 +142,8 @@ export const THREAT_TECHNIQUE = '[data-test-subj="threatTechniqueLink"]';
export const THREAT_SUBTECHNIQUE = '[data-test-subj="threatSubtechniqueLink"]';
export const BACK_TO_RULES_TABLE = '[data-test-subj="breadcrumb"][title="Detection rules (SIEM)"]';
export const BACK_TO_RULES_TABLE =
'[data-test-subj="breadcrumbs"] a[title="Detection rules (SIEM)"]';
export const HIGHLIGHTED_ROWS_IN_TABLE =
'[data-test-subj="euiDataGridBody"] .alertsTableHighlightedRow';

View file

@ -114,6 +114,7 @@ import {
CREATE_WITHOUT_ENABLING_BTN,
RULE_INDICES,
ALERTS_INDEX_BUTTON,
INVESTIGATIONS_INPUT,
} from '../screens/create_new_rule';
import {
INDEX_SELECTOR,
@ -131,11 +132,15 @@ import { waitForAlerts } from './alerts';
import { refreshPage } from './security_header';
import { EMPTY_ALERT_TABLE } from '../screens/alerts';
export const createAndEnableRuleOnly = () => {
cy.get(CREATE_AND_ENABLE_BTN).click({ force: true });
cy.get(CREATE_AND_ENABLE_BTN).should('not.exist');
};
export const createAndEnableRule = () => {
cy.get(CREATE_AND_ENABLE_BTN).click({ force: true });
cy.get(CREATE_AND_ENABLE_BTN).should('not.exist');
cy.get(BACK_TO_RULES_TABLE).click({ force: true });
cy.get(BACK_TO_RULES_TABLE).should('not.exist');
};
export const pressRuleCreateBtn = () => {
@ -303,6 +308,23 @@ export const fillReferenceUrls = (referenceUrls: string[] = ruleFields.reference
return referenceUrls;
};
export const fillCustomInvestigationFields = (
fields: string[] = ruleFields.investigationFields.field_names
) => {
fields.forEach((field) => {
cy.get(INVESTIGATIONS_INPUT).type(`${field}{enter}`, { force: true });
});
return fields;
};
export const fillAboutRuleMinimumAndContinue = (rule: RuleCreateProps) => {
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);
getAboutContinueButton().should('exist').click();
};
export const fillAboutRuleAndContinue = (rule: RuleCreateProps) => {
fillAboutRule(rule);
getAboutContinueButton().should('exist').click({ force: true });

View file

@ -13,8 +13,8 @@
"cypress:run:respops:ess": "yarn cypress:ess --spec './cypress/e2e/(detection_alerts|detection_rules|exceptions)/*.cy.ts'",
"cypress:investigations:run:ess": "yarn cypress:ess --spec './cypress/e2e/investigations/**/*.cy.ts'",
"cypress:explore:run:ess": "yarn cypress:ess --spec './cypress/e2e/explore/**/*.cy.ts'",
"cypress:changed-specs-only:ess": "yarn cypress:ess --changed-specs-only --env burn=2",
"cypress:burn:ess": "yarn cypress:ess --env burn=2",
"cypress:changed-specs-only:ess": "yarn cypress:ess --changed-specs-only --env burn=5",
"cypress:burn:ess": "yarn cypress:ess --env burn=5",
"junit:merge": "../../../node_modules/.bin/mochawesome-merge ../../../target/kibana-security-solution/cypress/results/mochawesome*.json > ../../../target/kibana-security-solution/cypress/results/output.json && ../../../node_modules/.bin/marge ../../../target/kibana-security-solution/cypress/results/output.json --reportDir ../../../target/kibana-security-solution/cypress/results && yarn junit:transform && mkdir -p ../../../target/junit && cp ../../../target/kibana-security-solution/cypress/results/*.xml ../../../target/junit/",
"junit:transform": "node ../../plugins/security_solution/scripts/junit_transformer --pathPattern '../../../target/kibana-security-solution/cypress/results/*.xml' --rootDirectory ../../../ --reportName 'Security Solution Cypress' --writeInPlace",
"cypress:serverless": "TZ=UTC NODE_OPTIONS=--openssl-legacy-provider node ../../plugins/security_solution/scripts/start_cypress_parallel --config-file ../../test/security_solution_cypress/cypress/cypress_ci_serverless.config.ts --ftr-config-file ../../test/security_solution_cypress/serverless_config",
@ -25,7 +25,7 @@
"cypress:run:cloud:serverless": "yarn cypress:cloud:serverless run --config-file ./cypress/cypress_ci_serverless.config.ts --env CLOUD_SERVERLESS=true",
"cypress:investigations:run:serverless": "yarn cypress:serverless --spec './cypress/e2e/investigations/**/*.cy.ts'",
"cypress:explore:run:serverless": "yarn cypress:serverless --spec './cypress/e2e/explore/**/*.cy.ts'",
"cypress:changed-specs-only:serverless": "yarn cypress:serverless --changed-specs-only --env burn=2",
"cypress:burn:serverless": "yarn cypress:serverless --env burn=2"
"cypress:changed-specs-only:serverless": "yarn cypress:serverless --changed-specs-only --env burn=5",
"cypress:burn:serverless": "yarn cypress:serverless --env burn=5"
}
}