diff --git a/x-pack/plugins/security_solution/cypress/data/detection_engine.ts b/x-pack/plugins/security_solution/cypress/data/detection_engine.ts new file mode 100644 index 000000000000..cdd72af22b78 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/data/detection_engine.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + RiskScore, + RuleInterval, + RuleIntervalFrom, + Severity, + Threat, + ThreatSubtechnique, + ThreatTechnique, +} from '@kbn/securitysolution-io-ts-alerting-types'; + +import type { + IndexPatternArray, + InvestigationGuide, + RuleDescription, + RuleFalsePositiveArray, + RuleQuery, + RuleName, + RuleReferenceArray, + RuleTagArray, +} from '../../common/detection_engine/rule_schema'; + +interface RuleFields { + defaultIndexPatterns: IndexPatternArray; + falsePositives: RuleFalsePositiveArray; + investigationGuide: InvestigationGuide; + referenceUrls: RuleReferenceArray; + riskScore: RiskScore; + ruleDescription: RuleDescription; + ruleInterval: RuleInterval; + ruleIntervalFrom: RuleIntervalFrom; + ruleQuery: RuleQuery; + ruleName: RuleName; + ruleTags: RuleTagArray; + ruleSeverity: Severity; + threat: Threat; + threatSubtechnique: ThreatSubtechnique; + threatTechnique: ThreatTechnique; +} + +export const ruleFields: RuleFields = { + defaultIndexPatterns: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'traces-apm*', + 'winlogbeat-*', + '-*elastic-cloud-logs-*', + ], + falsePositives: ['False1', 'False2'], + investigationGuide: '# test markdown', + referenceUrls: ['http://example.com/', 'https://example.com/'], + riskScore: 17, + ruleDescription: 'The rule description', + ruleInterval: '5m', + ruleIntervalFrom: '50000h', + ruleQuery: 'host.name: *', + ruleName: 'Test Rule', + ruleTags: ['test', 'newRule'], + ruleSeverity: 'high', + threat: { + framework: 'MITRE ATT&CK', + tactic: { + name: 'Credential Access', + id: 'TA0006', + reference: 'https://attack.mitre.org/tactics/TA0006', + }, + }, + threatSubtechnique: { + name: '/etc/passwd and /etc/shadow', + id: 'T1003.008', + reference: 'https://attack.mitre.org/techniques/T1003/008', + }, + threatTechnique: { + id: 'T1003', + name: 'OS Credential Dumping', + reference: 'https://attack.mitre.org/techniques/T1003', + }, +}; diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule.cy.ts index b81806e2ce65..b5869987f4d5 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule.cy.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { formatMitreAttackDescription } from '../../helpers/rules'; -import type { Mitre } from '../../objects/rule'; +import { ruleFields } from '../../data/detection_engine'; import { getNewRule, getExistingRule, @@ -14,7 +13,7 @@ import { getEditedRule, getNewOverrideRule, } from '../../objects/rule'; -import type { CompleteTimeline } from '../../objects/timeline'; +import { getTimeline } from '../../objects/timeline'; import { ALERT_GRID_CELL, NUMBER_OF_ALERTS } from '../../screens/alerts'; import { @@ -56,7 +55,6 @@ import { INDEX_PATTERNS_DETAILS, INVESTIGATION_NOTES_MARKDOWN, INVESTIGATION_NOTES_TOGGLE, - MITRE_ATTACK_DETAILS, REFERENCE_URLS_DETAILS, RISK_SCORE_DETAILS, RULE_NAME_HEADER, @@ -66,6 +64,9 @@ import { SEVERITY_DETAILS, TAGS_DETAILS, TIMELINE_TEMPLATE_DETAILS, + THREAT_TACTIC, + THREAT_TECHNIQUE, + THREAT_SUBTECHNIQUE, } from '../../screens/rule_details'; import { @@ -82,14 +83,26 @@ import { createTimeline } from '../../tasks/api_calls/timelines'; import { cleanKibana, deleteAlertsAndRules } from '../../tasks/common'; import { addEmailConnectorAndRuleAction } from '../../tasks/common/rule_actions'; import { + continueWithNextSection, createAndEnableRule, + expandAdvancedSettings, fillAboutRule, - fillAboutRuleAndContinue, - fillDefineCustomRuleAndContinue, - fillScheduleRuleAndContinue, + fillDescription, + fillFalsePositiveExamples, + fillFrom, + fillNote, + fillReferenceUrls, + fillRiskScore, + fillRuleName, + fillRuleTags, + fillSeverity, + fillThreat, + fillThreatSubtechnique, + fillThreatTechnique, goToAboutStepTab, goToActionsStepTab, goToScheduleStepTab, + importSavedQuery, waitForAlertsToPopulate, waitForTheRuleToBeExecuted, } from '../../tasks/create_new_rule'; @@ -105,98 +118,125 @@ describe('Custom query rules', () => { login(); }); describe('Custom detection rules creation', () => { - const expectedUrls = getNewRule().referenceUrls?.join(''); - const expectedFalsePositives = getNewRule().falsePositivesExamples?.join(''); - const expectedTags = getNewRule().tags?.join(''); - const mitreAttack = getNewRule().mitre as Mitre[]; - const expectedMitre = formatMitreAttackDescription(mitreAttack); const expectedNumberOfRules = 1; beforeEach(() => { - const timeline = getNewRule().timeline as CompleteTimeline; deleteAlertsAndRules(); - createTimeline(timeline).then((response) => { - cy.wrap({ - ...getNewRule(), - timeline: { - ...timeline, - id: response.body.data.persistTimeline.timeline.savedObjectId, - }, - }).as('rule'); - }); + createTimeline(getTimeline()) + .then((response) => { + return response.body.data.persistTimeline.timeline.savedObjectId; + }) + .as('timelineId'); }); it('Creates and enables a new rule', function () { visit(RULE_CREATION); - fillDefineCustomRuleAndContinue(this.rule); - fillAboutRuleAndContinue(this.rule); - fillScheduleRuleAndContinue(this.rule); + + cy.log('Filling define section'); + importSavedQuery(this.timelineId); + continueWithNextSection(); + + cy.log('Filling about section'); + fillRuleName(); + fillDescription(); + fillSeverity(); + fillRiskScore(); + fillRuleTags(); + expandAdvancedSettings(); + fillReferenceUrls(); + fillFalsePositiveExamples(); + fillThreat(); + fillThreatTechnique(); + fillThreatSubtechnique(); + fillNote(); + continueWithNextSection(); + + 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', this.rule.customQuery); + cy.get(CUSTOM_QUERY_INPUT).should('have.value', ruleFields.ruleQuery); cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); cy.get(DEFINE_CONTINUE_BUTTON).should('not.exist'); // expect about step to populate cy.get(ABOUT_EDIT_BUTTON).click(); - cy.get(RULE_NAME_INPUT).invoke('val').should('eql', this.rule.name); + cy.get(RULE_NAME_INPUT).invoke('val').should('eql', ruleFields.ruleName); cy.get(ABOUT_CONTINUE_BTN).should('exist').click({ force: true }); cy.get(ABOUT_CONTINUE_BTN).should('not.exist'); cy.get(SCHEDULE_CONTINUE_BUTTON).click({ force: true }); createAndEnableRule(); + 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'); cy.get(RULES_TABLE).find(RULES_ROW).should('have.length', expectedNumberOfRules); - cy.get(RULE_NAME).should('have.text', this.rule.name); - cy.get(RISK_SCORE).should('have.text', this.rule.riskScore); - cy.get(SEVERITY).should('have.text', this.rule.severity); + 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'); goToRuleDetails(); - cy.get(RULE_NAME_HEADER).should('contain', `${this.rule.name}`); - cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', this.rule.description); + 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).should('have.text', this.rule.severity); - getDetails(RISK_SCORE_DETAILS).should('have.text', this.rule.riskScore); + 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(expectedUrls); - }); - getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); - getDetails(MITRE_ATTACK_DETAILS).should((mitre) => { - expect(removeExternalLinkText(mitre.text())).equal(expectedMitre); + expect(removeExternalLinkText(details.text())).equal(ruleFields.referenceUrls.join('')); }); - getDetails(TAGS_DETAILS).should('have.text', expectedTags); + 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({ force: true }); 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', this.rule.customQuery); + 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', 'None'); }); cy.get(SCHEDULE_DETAILS).within(() => { - getDetails(RUNS_EVERY_DETAILS).should( - 'have.text', - `${getNewRule().runsEvery?.interval}${getNewRule().runsEvery?.type}` - ); - getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should( - 'have.text', - `${getNewRule().lookBack?.interval}${getNewRule().lookBack?.type}` - ); + 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(NUMBER_OF_ALERTS) .invoke('text') .should('match', /^[1-9].+$/); // Any number of alerts - cy.get(ALERT_GRID_CELL).contains(this.rule.name); + cy.get(ALERT_GRID_CELL).contains(ruleFields.ruleName); }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts index 0d14673f4cf2..c57e2c603e46 100644 --- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts @@ -72,6 +72,8 @@ export const DATA_VIEW_COMBO_BOX = export const DATA_VIEW_OPTION = '[data-test-subj="rule-index-toggle-dataView"]'; +export const CONTINUE_BUTTON = '[data-test-subj$=-continue]'; + export const DEFINE_CONTINUE_BUTTON = '[data-test-subj="define-continue"]'; export const DEFINE_EDIT_BUTTON = '[data-test-subj="edit-define-rule"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index 606ee4ae7a04..05e99f2e97c3 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -125,3 +125,9 @@ export const DEFINE_RULE_PANEL_PROGRESS = '[data-test-subj="defineRule"] [data-test-subj="stepPanelProgress"]'; export const EDIT_RULE_SETTINGS_LINK = '[data-test-subj="editRuleSettingsLink"]'; + +export const THREAT_TACTIC = '[data-test-subj="threatTacticLink"]'; + +export const THREAT_TECHNIQUE = '[data-test-subj="threatTechniqueLink"]'; + +export const THREAT_SUBTECHNIQUE = '[data-test-subj="threatSubtechniqueLink"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index 7fdc101f8b14..fc1f3b389bdf 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -5,6 +5,12 @@ * 2.0. */ +import type { + RuleIntervalFrom, + Threat, + ThreatSubtechnique, + ThreatTechnique, +} from '@kbn/securitysolution-io-ts-alerting-types'; import type { CustomRule, MachineLearningRule, @@ -97,6 +103,7 @@ import { NEW_TERMS_HISTORY_TIME_TYPE, NEW_TERMS_INPUT_AREA, ACTIONS_THROTTLE_INPUT, + CONTINUE_BUTTON, } from '../screens/create_new_rule'; import { INDEX_SELECTOR, @@ -109,6 +116,7 @@ import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline'; import { TIMELINE } from '../screens/timelines'; import { refreshPage } from './security_header'; import { EUI_FILTER_SELECT_ITEM, COMBO_BOX_INPUT } from '../screens/common/controls'; +import { ruleFields } from '../data/detection_engine'; export const createAndEnableRule = () => { cy.get(CREATE_AND_ENABLE_BTN).click({ force: true }); @@ -149,11 +157,16 @@ export const fillAboutRule = ( } }; -const fillNote = (note: string) => { +export const expandAdvancedSettings = () => { + cy.get(ADVANCED_SETTINGS_BTN).click({ force: true }); +}; + +export const fillNote = (note: string = ruleFields.investigationGuide) => { cy.get(INVESTIGATION_NOTES_TEXTAREA).clear({ force: true }).type(note, { force: true }); + return note; }; -const fillMitre = (mitreAttacks: Mitre[]) => { +export const fillMitre = (mitreAttacks: Mitre[]) => { let techniqueIndex = 0; let subtechniqueInputIndex = 0; mitreAttacks.forEach((mitre, tacticIndex) => { @@ -178,9 +191,32 @@ const fillMitre = (mitreAttacks: Mitre[]) => { cy.get(MITRE_ATTACK_ADD_TACTIC_BUTTON).click({ force: true }); }); + return mitreAttacks; +}; + +export const fillThreat = (threat: Threat = ruleFields.threat) => { + cy.get(MITRE_ATTACK_TACTIC_DROPDOWN).first().click({ force: true }); + cy.contains(MITRE_TACTIC, threat.tactic.name).click(); + return threat; +}; + +export const fillThreatTechnique = (technique: ThreatTechnique = ruleFields.threatTechnique) => { + cy.get(MITRE_ATTACK_ADD_TECHNIQUE_BUTTON).first().click({ force: true }); + cy.get(MITRE_ATTACK_TECHNIQUE_DROPDOWN).first().click({ force: true }); + cy.contains(MITRE_TACTIC, technique.name).click(); + return technique; +}; + +export const fillThreatSubtechnique = ( + subtechnique: ThreatSubtechnique = ruleFields.threatSubtechnique +) => { + cy.get(MITRE_ATTACK_ADD_SUBTECHNIQUE_BUTTON).first().click({ force: true }); + cy.get(MITRE_ATTACK_SUBTECHNIQUE_DROPDOWN).first().click({ force: true }); + cy.contains(MITRE_TACTIC, subtechnique.name).click(); + return subtechnique; }; -const fillFalsePositiveExamples = (falsePositives: string[]) => { +export const fillFalsePositiveExamples = (falsePositives: string[] = ruleFields.falsePositives) => { falsePositives.forEach((falsePositive, index) => { cy.get(FALSE_POSITIVES_INPUT) .eq(index) @@ -188,28 +224,49 @@ const fillFalsePositiveExamples = (falsePositives: string[]) => { .type(falsePositive, { force: true }); cy.get(ADD_FALSE_POSITIVE_BTN).click({ force: true }); }); + return falsePositives; }; -const fillSeverity = (severity: string) => { +export const importSavedQuery = (timelineId: string) => { + cy.get(IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK).click(); + cy.get(TIMELINE(timelineId)).click(); + cy.get(CUSTOM_QUERY_INPUT).should('not.be.empty'); +}; + +export const fillRuleName = (ruleName: string = ruleFields.ruleName) => { + cy.get(RULE_NAME_INPUT).clear({ force: true }).type(ruleName, { force: true }); + return ruleName; +}; + +export const fillDescription = (description: string = ruleFields.ruleDescription) => { + cy.get(RULE_DESCRIPTION_INPUT).clear({ force: true }).type(description, { force: true }); + return description; +}; + +export const fillSeverity = (severity: string = ruleFields.ruleSeverity) => { cy.get(SEVERITY_DROPDOWN).click({ force: true }); cy.get(`#${severity.toLowerCase()}`).click(); + return severity; }; -const fillRiskScore = (riskScore: string) => { +export const fillRiskScore = (riskScore: string = ruleFields.riskScore.toString()) => { cy.get(DEFAULT_RISK_SCORE_INPUT).type(`{selectall}${riskScore}`, { force: true }); + return riskScore; }; -const fillRuleTags = (tags: string[]) => { +export const fillRuleTags = (tags: string[] = ruleFields.ruleTags) => { tags.forEach((tag) => { cy.get(TAGS_INPUT).type(`${tag}{enter}`, { force: true }); }); + return tags; }; -const fillReferenceUrls = (referenceUrls: string[]) => { +export const fillReferenceUrls = (referenceUrls: string[] = ruleFields.referenceUrls) => { referenceUrls.forEach((url, index) => { cy.get(REFERENCE_URLS_INPUT).eq(index).clear({ force: true }).type(url, { force: true }); cy.get(ADD_REFERENCE_URL_BTN).click({ force: true }); }); + return referenceUrls; }; export const fillAboutRuleAndContinue = ( @@ -286,6 +343,10 @@ const fillCustomQuery = (rule: CustomRule | OverrideRule) => { } }; +export const continueWithNextSection = () => { + cy.get(CONTINUE_BUTTON).should('exist').click(); +}; + export const fillDefineCustomRuleAndContinue = (rule: CustomRule | OverrideRule) => { if (rule.dataSource.type === 'dataView') { cy.get(DATA_VIEW_OPTION).click(); @@ -308,6 +369,13 @@ export const fillScheduleRuleAndContinue = (rule: CustomRule | MachineLearningRu cy.get(SCHEDULE_CONTINUE_BUTTON).click({ force: true }); }; +export const fillFrom = (from: RuleIntervalFrom = ruleFields.ruleIntervalFrom) => { + const value = from.slice(0, from.length - 1); + const type = from.slice(from.length - 1); + cy.get(LOOK_BACK_INTERVAL).type('{selectAll}').type(value); + cy.get(LOOK_BACK_TIME_TYPE).select(type); +}; + export const fillRuleAction = (rule: CustomRule) => { if (rule.actions) { cy.get(ACTIONS_THROTTLE_INPUT).select(rule.actions.throttle); @@ -614,3 +682,20 @@ export const checkLoadQueryDynamically = () => { export const uncheckLoadQueryDynamically = () => { cy.get(LOAD_QUERY_DYNAMICALLY_CHECKBOX).click({ force: true }).should('not.be.checked'); }; + +export const defineSection = { importSavedQuery }; +export const aboutSection = { + fillRuleName, + fillDescription, + fillSeverity, + fillRiskScore, + fillRuleTags, + expandAdvancedSettings, + fillReferenceUrls, + fillFalsePositiveExamples, + fillThreat, + fillThreatTechnique, + fillThreatSubtechnique, + fillNote, +}; +export const scheduleSection = { fillFrom };