[Security Solution] Exceptions Cypress tests (#81759)

* improves 'Creates and activates a new custom rule' test

* fixes constant problem

* improves 'Creates and activates a new custom rule with override option' test

* improves 'Creates and activates a new threshold rule' test

* refactor

* fixes type check issue

* improves assertions

* removes unused code

* changes variables for constants

* improves 'waitForTheRuleToBeExecuted' test

* improves readability

* fixes jenkins error

* refactor

* blah

* more things

* finishes 'Creates an exception from rule details and deletes the excpetion' implementation

* implements 'Creates an exception from an alert and deletes the exception'

* updates VALUES_INPUT locator

* updates archiver

* refactor

* improves the code

* fixes CI error

* renames exceptions archive

* refactor

* fixes merge issue

* fixes CI issue

* debug

* refactor

* improves test data

* removes signals index after the execution

* removes unused line

* removes unused variable

* refactors 'numberOfauditbeatExceptionsAlerts' constant to camel case

* simplifies the archive

* waits for the rule to be executed after navigating to opened alerts tab

* cleaning data

* fixes tests flakiness

* cleans test data

* refactors code

* removes unsused archives

* cleans data

* simplifies data

* fixes CI issue

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
MadameSheema 2020-11-30 10:37:42 +01:00 committed by GitHub
parent 767286e2da
commit 454635228e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 7572 additions and 27 deletions

View file

@ -0,0 +1,178 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { exception } from '../objects/exception';
import { newRule } from '../objects/rule';
import { RULE_STATUS } from '../screens/create_new_rule';
import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline';
import {
addExceptionFromFirstAlert,
goToClosedAlerts,
goToManageAlertsDetectionRules,
goToOpenedAlerts,
waitForAlertsIndexToBeCreated,
} from '../tasks/alerts';
import { createCustomRule, deleteCustomRule, removeSignalsIndex } from '../tasks/api_calls';
import { goToRuleDetails } from '../tasks/alerts_detection_rules';
import { waitForAlertsToPopulate } from '../tasks/create_new_rule';
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
import {
activatesRule,
addsException,
addsExceptionFromRuleSettings,
goToAlertsTab,
goToExceptionsTab,
removeException,
waitForTheRuleToBeExecuted,
} from '../tasks/rule_details';
import { refreshPage } from '../tasks/security_header';
import { DETECTIONS_URL } from '../urls/navigation';
const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = 1;
describe('Exceptions', () => {
beforeEach(() => {
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
waitForAlertsIndexToBeCreated();
createCustomRule(newRule);
goToManageAlertsDetectionRules();
goToRuleDetails();
cy.get(RULE_STATUS).should('have.text', '—');
esArchiverLoad('auditbeat_for_exceptions');
activatesRule();
waitForTheRuleToBeExecuted();
waitForAlertsToPopulate();
refreshPage();
cy.get(SERVER_SIDE_EVENT_COUNT)
.invoke('text')
.then((numberOfInitialAlertsText) => {
cy.wrap(parseInt(numberOfInitialAlertsText, 10)).should(
'eql',
NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS
);
});
});
afterEach(() => {
esArchiverUnload('auditbeat_for_exceptions');
esArchiverUnload('auditbeat_for_exceptions2');
removeSignalsIndex();
deleteCustomRule();
});
context('From rule', () => {
it('Creates an exception and deletes it', () => {
goToExceptionsTab();
addsExceptionFromRuleSettings(exception);
esArchiverLoad('auditbeat_for_exceptions2');
waitForTheRuleToBeExecuted();
goToAlertsTab();
refreshPage();
cy.get(SERVER_SIDE_EVENT_COUNT)
.invoke('text')
.then((numberOfAlertsAfterCreatingExceptionText) => {
cy.wrap(parseInt(numberOfAlertsAfterCreatingExceptionText, 10)).should('eql', 0);
});
goToClosedAlerts();
refreshPage();
cy.get(SERVER_SIDE_EVENT_COUNT)
.invoke('text')
.then((numberOfClosedAlertsAfterCreatingExceptionText) => {
cy.wrap(parseInt(numberOfClosedAlertsAfterCreatingExceptionText, 10)).should(
'eql',
NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS
);
});
goToOpenedAlerts();
waitForTheRuleToBeExecuted();
refreshPage();
cy.get(SERVER_SIDE_EVENT_COUNT)
.invoke('text')
.then((numberOfOpenedAlertsAfterCreatingExceptionText) => {
cy.wrap(parseInt(numberOfOpenedAlertsAfterCreatingExceptionText, 10)).should('eql', 0);
});
goToExceptionsTab();
removeException();
refreshPage();
goToAlertsTab();
waitForTheRuleToBeExecuted();
waitForAlertsToPopulate();
refreshPage();
cy.get(SERVER_SIDE_EVENT_COUNT)
.invoke('text')
.then((numberOfAlertsAfterRemovingExceptionsText) => {
cy.wrap(parseInt(numberOfAlertsAfterRemovingExceptionsText, 10)).should(
'eql',
NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS
);
});
});
});
context('From alert', () => {
it('Creates an exception and deletes it', () => {
addExceptionFromFirstAlert();
addsException(exception);
esArchiverLoad('auditbeat_for_exceptions2');
cy.get(SERVER_SIDE_EVENT_COUNT)
.invoke('text')
.then((numberOfAlertsAfterCreatingExceptionText) => {
cy.wrap(parseInt(numberOfAlertsAfterCreatingExceptionText, 10)).should('eql', 0);
});
goToClosedAlerts();
refreshPage();
cy.get(SERVER_SIDE_EVENT_COUNT)
.invoke('text')
.then((numberOfClosedAlertsAfterCreatingExceptionText) => {
cy.wrap(parseInt(numberOfClosedAlertsAfterCreatingExceptionText, 10)).should(
'eql',
NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS
);
});
goToOpenedAlerts();
waitForTheRuleToBeExecuted();
refreshPage();
cy.get(SERVER_SIDE_EVENT_COUNT)
.invoke('text')
.then((numberOfOpenedAlertsAfterCreatingExceptionText) => {
cy.wrap(parseInt(numberOfOpenedAlertsAfterCreatingExceptionText, 10)).should('eql', 0);
});
goToExceptionsTab();
removeException();
goToAlertsTab();
waitForTheRuleToBeExecuted();
waitForAlertsToPopulate();
refreshPage();
cy.get(SERVER_SIDE_EVENT_COUNT)
.invoke('text')
.then((numberOfAlertsAfterRemovingExceptionsText) => {
cy.wrap(parseInt(numberOfAlertsAfterRemovingExceptionsText, 10)).should(
'eql',
NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS
);
});
});
});
});

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export interface Exception {
field: string;
operator: string;
values: string[];
}
export const exception: Exception = {
field: 'host.name',
operator: 'is',
values: ['suricata-iowa'],
};

View file

@ -4,6 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const ADD_EXCEPTION_BTN = '[data-test-subj="addExceptionButton"]';
export const ALERTS = '[data-test-subj="event"]';
export const ALERT_CHECKBOX = '[data-test-subj="select-event-container"] .euiCheckbox__input';

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export const ADD_EXCEPTIONS_BTN = '[data-test-subj="exceptionsHeaderAddExceptionBtn"]';
export const CLOSE_ALERTS_CHECKBOX =
'[data-test-subj="bulk-close-alert-on-add-add-exception-checkbox"]';
export const CONFIRM_BTN = '[data-test-subj="add-exception-confirm-button"]';
export const FIELD_INPUT =
'[data-test-subj="fieldAutocompleteComboBox"] [data-test-subj="comboBoxInput"]';
export const FIELD_INPUT_RESULT = '.euiFilterSelectItem';
export const LOADING_SPINNER = '[data-test-subj="loading-spinner"]';
export const OPERATOR_INPUT = '[data-test-subj="operatorAutocompleteComboBox"]';
export const VALUES_INPUT =
'[data-test-subj="valuesAutocompleteMatch"] [data-test-subj="comboBoxInput"]';

View file

@ -15,6 +15,8 @@ export const ABOUT_DETAILS =
export const ADDITIONAL_LOOK_BACK_DETAILS = 'Additional look-back time';
export const ALERTS_TAB = '[data-test-subj="alertsTab"]';
export const ANOMALY_SCORE_DETAILS = 'Anomaly score';
export const CUSTOM_QUERY_DETAILS = 'Custom query';
@ -22,11 +24,13 @@ export const CUSTOM_QUERY_DETAILS = 'Custom query';
export const DEFINITION_DETAILS =
'[data-test-subj=definitionRule] [data-test-subj="listItemColumnStepRuleDescription"]';
export const DELETE_RULE = '[data-test-subj=rules-details-delete-rule]';
export const DETAILS_DESCRIPTION = '.euiDescriptionList__description';
export const DETAILS_TITLE = '.euiDescriptionList__title';
export const DELETE_RULE = '[data-test-subj=rules-details-delete-rule]';
export const EXCEPTIONS_TAB = '[data-test-subj="exceptionsTab"]';
export const FALSE_POSITIVES_DETAILS = 'False positive examples';
@ -42,6 +46,8 @@ export const MACHINE_LEARNING_JOB_STATUS = '[data-test-subj="machineLearningJobS
export const MITRE_ATTACK_DETAILS = 'MITRE ATT&CK';
export const REFRESH_BUTTON = '[data-test-subj="refreshButton"]';
export const RULE_ABOUT_DETAILS_HEADER_TOGGLE = '[data-test-subj="stepAboutDetailsToggle"]';
export const RULE_NAME_HEADER = '[data-test-subj="header-page-title"]';
@ -54,6 +60,12 @@ export const RISK_SCORE_OVERRIDE_DETAILS = 'Risk score override';
export const REFERENCE_URLS_DETAILS = 'Reference URLs';
export const REMOVE_EXCEPTION_BTN = '[data-test-subj="exceptionsViewerDeleteBtn"]';
export const RULE_SWITCH = '[data-test-subj="ruleSwitch"]';
export const RULE_SWITCH_LOADER = '[data-test-subj="rule-switch-loader"]';
export const RULE_TYPE_DETAILS = 'Rule type';
export const RUNS_EVERY_DETAILS = 'Runs every';

View file

@ -5,28 +5,34 @@
*/
import {
CLOSED_ALERTS_FILTER_BTN,
EXPAND_ALERT_BTN,
LOADING_ALERTS_PANEL,
MANAGE_ALERT_DETECTION_RULES_BTN,
OPENED_ALERTS_FILTER_BTN,
SEND_ALERT_TO_TIMELINE_BTN,
ADD_EXCEPTION_BTN,
ALERT_RISK_SCORE_HEADER,
ALERTS,
ALERT_CHECKBOX,
TIMELINE_CONTEXT_MENU_BTN,
CLOSE_ALERT_BTN,
TAKE_ACTION_POPOVER_BTN,
CLOSE_SELECTED_ALERTS_BTN,
CLOSED_ALERTS_FILTER_BTN,
EXPAND_ALERT_BTN,
IN_PROGRESS_ALERTS_FILTER_BTN,
OPEN_ALERT_BTN,
OPEN_SELECTED_ALERTS_BTN,
LOADING_ALERTS_PANEL,
MANAGE_ALERT_DETECTION_RULES_BTN,
MARK_ALERT_IN_PROGRESS_BTN,
MARK_SELECTED_ALERTS_IN_PROGRESS_BTN,
ALERT_RISK_SCORE_HEADER,
OPEN_ALERT_BTN,
OPEN_SELECTED_ALERTS_BTN,
OPENED_ALERTS_FILTER_BTN,
SEND_ALERT_TO_TIMELINE_BTN,
TAKE_ACTION_POPOVER_BTN,
TIMELINE_CONTEXT_MENU_BTN,
} from '../screens/alerts';
import { REFRESH_BUTTON } from '../screens/security_header';
import { TIMELINE_COLUMN_SPINNER } from '../screens/timeline';
export const addExceptionFromFirstAlert = () => {
cy.get(TIMELINE_CONTEXT_MENU_BTN).first().click();
cy.get(ADD_EXCEPTION_BTN).click();
};
export const closeFirstAlert = () => {
cy.get(TIMELINE_CONTEXT_MENU_BTN).first().click({ force: true });
cy.get(CLOSE_ALERT_BTN).click();
@ -43,6 +49,9 @@ export const expandFirstAlert = () => {
export const goToClosedAlerts = () => {
cy.get(CLOSED_ALERTS_FILTER_BTN).click();
cy.get(REFRESH_BUTTON).should('not.have.text', 'Updating');
cy.get(REFRESH_BUTTON).should('have.text', 'Refresh');
cy.get(TIMELINE_COLUMN_SPINNER).should('not.exist');
};
export const goToManageAlertsDetectionRules = () => {
@ -51,6 +60,9 @@ export const goToManageAlertsDetectionRules = () => {
export const goToOpenedAlerts = () => {
cy.get(OPENED_ALERTS_FILTER_BTN).click({ force: true });
cy.get(REFRESH_BUTTON).should('not.have.text', 'Updating');
cy.get(REFRESH_BUTTON).should('have.text', 'Refresh');
cy.get(TIMELINE_COLUMN_SPINNER).should('not.exist');
};
export const openFirstAlert = () => {

View file

@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { CustomRule } from '../objects/rule';
export const createCustomRule = (rule: CustomRule) => {
cy.request({
method: 'POST',
url: 'api/detection_engine/rules',
body: {
rule_id: 'rule_testing',
risk_score: parseInt(rule.riskScore, 10),
description: rule.description,
interval: '10s',
name: rule.name,
severity: rule.severity.toLocaleLowerCase(),
type: 'query',
from: 'now-17520h',
index: ['exceptions-*'],
query: rule.customQuery,
language: 'kuery',
enabled: false,
},
headers: { 'kbn-xsrf': 'cypress-creds' },
});
};
export const deleteCustomRule = () => {
cy.request({
method: 'DELETE',
url: 'api/detection_engine/rules?rule_id=rule_testing',
headers: { 'kbn-xsrf': 'cypress-creds' },
});
};
export const removeSignalsIndex = () => {
cy.request({
method: 'DELETE',
url: `api/detection_engine/index`,
headers: { 'kbn-xsrf': 'delete-signals' },
});
};

View file

@ -11,7 +11,6 @@ import {
OverrideRule,
ThresholdRule,
} from '../objects/rule';
import { NUMBER_OF_ALERTS } from '../screens/alerts';
import {
ABOUT_CONTINUE_BTN,
ABOUT_EDIT_TAB,
@ -65,6 +64,7 @@ import {
EQL_QUERY_PREVIEW_HISTOGRAM,
EQL_QUERY_VALIDATION_SPINNER,
} from '../screens/create_new_rule';
import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline';
import { NOTIFICATION_TOASTS, TOAST_ERROR_CLASS } from '../screens/shared';
import { TIMELINE } from '../screens/timelines';
import { refreshPage } from './security_header';
@ -273,6 +273,22 @@ export const selectThresholdRuleType = () => {
cy.get(THRESHOLD_TYPE).click({ force: true });
};
export const waitForAlertsToPopulate = async () => {
cy.waitUntil(
() => {
refreshPage();
return cy
.get(SERVER_SIDE_EVENT_COUNT)
.invoke('text')
.then((countText) => {
const alertCount = parseInt(countText, 10) || 0;
return alertCount > 0;
});
},
{ interval: 500, timeout: 12000 }
);
};
export const waitForTheRuleToBeExecuted = () => {
cy.waitUntil(() => {
cy.get(REFRESH_BUTTON).click();
@ -283,19 +299,6 @@ export const waitForTheRuleToBeExecuted = () => {
});
};
export const waitForAlertsToPopulate = async () => {
cy.waitUntil(() => {
refreshPage();
return cy
.get(NUMBER_OF_ALERTS)
.invoke('text')
.then((countText) => {
const alertCount = parseInt(countText, 10) || 0;
return alertCount > 0;
});
});
};
export const selectEqlRuleType = () => {
cy.get(EQL_TYPE).click({ force: true });
};

View file

@ -0,0 +1,92 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Exception } from '../objects/exception';
import { RULE_STATUS } from '../screens/create_new_rule';
import {
ADD_EXCEPTIONS_BTN,
CLOSE_ALERTS_CHECKBOX,
CONFIRM_BTN,
FIELD_INPUT,
LOADING_SPINNER,
OPERATOR_INPUT,
VALUES_INPUT,
} from '../screens/exceptions';
import {
ALERTS_TAB,
EXCEPTIONS_TAB,
REFRESH_BUTTON,
REMOVE_EXCEPTION_BTN,
RULE_SWITCH,
} from '../screens/rule_details';
export const activatesRule = () => {
cy.server();
cy.route('PATCH', '**/api/detection_engine/rules/_bulk_update').as('bulk_update');
cy.get(RULE_SWITCH).should('be.visible');
cy.get(RULE_SWITCH).click();
cy.wait('@bulk_update').then((response) => {
cy.wrap(response.status).should('eql', 200);
});
};
export const deactivatesRule = () => {
cy.get(RULE_SWITCH).should('be.visible');
cy.get(RULE_SWITCH).click();
};
export const addsException = (exception: Exception) => {
cy.get(LOADING_SPINNER).should('exist');
cy.get(LOADING_SPINNER).should('not.exist');
cy.get(FIELD_INPUT).should('exist');
cy.get(FIELD_INPUT).type(`${exception.field}{enter}`);
cy.get(OPERATOR_INPUT).type(`${exception.operator}{enter}`);
exception.values.forEach((value) => {
cy.get(VALUES_INPUT).type(`${value}{enter}`);
});
cy.get(CLOSE_ALERTS_CHECKBOX).click({ force: true });
cy.get(CONFIRM_BTN).click();
cy.get(CONFIRM_BTN).should('have.attr', 'disabled');
cy.get(CONFIRM_BTN).should('not.have.attr', 'disabled');
};
export const addsExceptionFromRuleSettings = (exception: Exception) => {
cy.get(ADD_EXCEPTIONS_BTN).click();
cy.get(LOADING_SPINNER).should('exist');
cy.get(LOADING_SPINNER).should('not.exist');
cy.get(LOADING_SPINNER).should('exist');
cy.get(LOADING_SPINNER).should('not.exist');
cy.get(FIELD_INPUT).should('be.visible');
cy.get(FIELD_INPUT).type(`${exception.field}{enter}`);
cy.get(OPERATOR_INPUT).type(`${exception.operator}{enter}`);
exception.values.forEach((value) => {
cy.get(VALUES_INPUT).type(`${value}{enter}`);
});
cy.get(CLOSE_ALERTS_CHECKBOX).click({ force: true });
cy.get(CONFIRM_BTN).click();
cy.get(CONFIRM_BTN).should('have.attr', 'disabled');
cy.get(CONFIRM_BTN).should('not.have.attr', 'disabled');
};
export const goToAlertsTab = () => {
cy.get(ALERTS_TAB).click();
};
export const goToExceptionsTab = () => {
cy.get(EXCEPTIONS_TAB).click();
};
export const removeException = () => {
cy.get(REMOVE_EXCEPTION_BTN).click();
};
export const waitForTheRuleToBeExecuted = async () => {
let status = '';
while (status !== 'succeeded') {
cy.get(REFRESH_BUTTON).click({ force: true });
status = await cy.get(RULE_STATUS).invoke('text').promisify();
}
};

View file

@ -334,7 +334,9 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
onClick={handleAddExceptionClick}
disabled={!canUserCRUD || !hasIndexWrite || !areExceptionsAllowed}
>
<EuiText size="m">{i18n.ACTION_ADD_EXCEPTION}</EuiText>
<EuiText data-test-subj="addExceptionButton" size="m">
{i18n.ACTION_ADD_EXCEPTION}
</EuiText>
</EuiContextMenuItem>
);

View file

@ -110,16 +110,19 @@ const getRuleDetailsTabs = (rule: Rule | null) => {
id: RuleDetailTabs.alerts,
name: detectionI18n.ALERT,
disabled: false,
dataTestSubj: 'alertsTab',
},
{
id: RuleDetailTabs.exceptions,
name: i18n.EXCEPTIONS_TAB,
disabled: !canUseExceptions,
dataTestSubj: 'exceptionsTab',
},
{
id: RuleDetailTabs.failures,
name: i18n.FAILURE_HISTORY_TAB,
disabled: false,
dataTestSubj: 'failureHistoryTab',
},
];
};
@ -263,6 +266,7 @@ export const RuleDetailsPageComponent: FC<PropsFromRedux> = ({
isSelected={tab.id === ruleDetailTab}
disabled={tab.disabled}
key={tab.id}
data-test-subj={tab.dataTestSubj}
>
{tab.name}
</EuiTab>