diff --git a/cypress/integration/1_detectors.spec.js b/cypress/integration/1_detectors.spec.js index 59c2214e6..f9e111f81 100644 --- a/cypress/integration/1_detectors.spec.js +++ b/cypress/integration/1_detectors.spec.js @@ -108,14 +108,16 @@ const validatePendingFieldMappingsPanel = (mappings) => { }); }; -const createDetector = (detectorName, dataSource, expectFailure) => { - getCreateDetectorButton().click({ force: true }); - - // TEST DETAILS PAGE +const fillDetailsForm = (detectorName, dataSource) => { getNameField().type(detectorName); getDataSourceField().selectComboboxItem(dataSource); - selectDnsLogType(); +}; + +const createDetector = (detectorName, dataSource, expectFailure) => { + getCreateDetectorButton().click({ force: true }); + + fillDetailsForm(detectorName, dataSource); cy.getElementByText('.euiAccordion .euiTitle', 'Detection rules (14 selected)') .click({ force: true, timeout: 5000 }) @@ -226,6 +228,11 @@ const createDetector = (detectorName, dataSource, expectFailure) => { } }; +const openCreateForm = () => getCreateDetectorButton().click({ force: true }); + +const getDescriptionField = () => cy.getTextareaByLabel('Description - optional'); +const getTriggerNameField = () => cy.getFieldByLabel('Trigger name'); + describe('Detectors', () => { before(() => { cy.cleanUpTests(); @@ -252,203 +259,289 @@ describe('Detectors', () => { cy.createRule(dns_type_rule_data); }); - beforeEach(() => { - cy.intercept('/detectors/_search').as('detectorsSearch'); + describe('...should validate form fields', () => { + beforeEach(() => { + cy.intercept('/detectors/_search').as('detectorsSearch'); - // Visit Detectors page before any test - cy.visit(`${OPENSEARCH_DASHBOARDS_URL}/detectors`); - cy.wait('@detectorsSearch').should('have.property', 'state', 'Complete'); - }); + // Visit Detectors page before any test + cy.visit(`${OPENSEARCH_DASHBOARDS_URL}/detectors`); + cy.wait('@detectorsSearch').should('have.property', 'state', 'Complete'); + + openCreateForm(); + }); + + it('...should validate name field', () => { + getNameField().should('be.empty'); + getNameField().focus().blur(); + getNameField().parentsUntil('.euiFormRow__fieldWrapper').siblings().contains('Enter a name.'); + + getNameField().type('text').focus().blur(); + + getNameField() + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .contains( + 'Name should only consist of upper and lowercase letters, numbers 0-9, hyphens, spaces, and underscores. Use between 5 and 50 characters.' + ); + + getNameField().type('{selectall}').type('{backspace}').type('tex&').focus().blur(); + + getNameField() + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .contains( + 'Name should only consist of upper and lowercase letters, numbers 0-9, hyphens, spaces, and underscores. Use between 5 and 50 characters.' + ); + + getNameField() + .type('{selectall}') + .type('{backspace}') + .type('Detector name') + .focus() + .blur() + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .should('not.exist'); + }); - it('...should validate form', () => { - getCreateDetectorButton().click({ force: true }); + it('...should validate description field', () => { + const longDescriptionText = + 'This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text.'; + + getDescriptionField().should('be.empty'); + + getDescriptionField().type(longDescriptionText).focus().blur(); + + getDescriptionField() + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .contains( + 'Description should only consist of upper and lowercase letters, numbers 0-9, commas, hyphens, periods, spaces, and underscores. Max limit of 500 characters.' + ); + + getDescriptionField() + .type('{selectall}') + .type('{backspace}') + .type('Detector description...') + .focus() + .blur(); + + getDescriptionField() + .type('{selectall}') + .type('{backspace}') + .type('Detector name') + .focus() + .blur() + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .should('not.exist'); + }); - getNextButton().should('be.disabled'); + it('...should validate data source field', () => { + getDataSourceField() + .focus() + .blur() + .parentsUntil('.euiFormRow__fieldWrapper') + .siblings() + .contains('Select an input source.'); + + getDataSourceField().selectComboboxItem(cypressIndexDns); + getDataSourceField() + .focus() + .blur() + .parentsUntil('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .should('not.exist'); + }); - getNameField().should('be.empty'); - getNameField().type('text').focus().blur(); + it('...should validate next button', () => { + getNextButton().should('be.disabled'); - getNameField() - .parentsUntil('.euiFormRow__fieldWrapper') - .siblings() - .contains( - 'Name should only consist of upper and lowercase letters, numbers 0-9, hyphens, spaces, and underscores. Use between 5 and 50 characters.' - ); + fillDetailsForm(detectorName, cypressIndexDns); + getNextButton().should('be.enabled'); + }); - getNameField() - .type(' and more text') - .focus() - .blur() - .parentsUntil('.euiFormRow__fieldWrapper') - .siblings() - .should('not.exist'); - getNextButton().should('be.disabled'); + it('...should validate alerts page', () => { + fillDetailsForm(detectorName, cypressIndexDns); + getNextButton().click({ force: true }); + getTriggerNameField().should('be.empty'); - getDataSourceField() - .focus() - .blur() - .parentsUntil('.euiFormRow__fieldWrapper') - .siblings() - .contains('Select an input source'); - getNextButton().should('be.disabled'); - - getDataSourceField().selectComboboxItem(cypressIndexDns); - getDataSourceField() - .focus() - .blur() - .parentsUntil('.euiFormRow__fieldWrapper') - .find('.euiFormErrorText') - .should('not.exist'); - getNextButton().should('not.be.disabled'); - }); + getTriggerNameField().focus().blur(); + getTriggerNameField() + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .contains('Enter a name.'); - it('...should show mappings warning', () => { - getCreateDetectorButton().click({ force: true }); + getTriggerNameField().type('Trigger name').focus().blur(); - getDataSourceField().selectComboboxItem(cypressIndexDns); + getTriggerNameField() + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .should('not.exist'); - selectDnsLogType(); + getNextButton().should('be.enabled'); - getDataSourceField().selectComboboxItem(cypressIndexWindows); - getDataSourceField().focus().blur(); + getTriggerNameField().type('{selectall}').type('{backspace}').focus().blur(); + getNextButton().should('be.disabled'); - cy.get('.euiCallOut') - .should('be.visible') - .contains( - 'To avoid issues with field mappings, we recommend creating separate detectors for different log types.' - ); - }); + cy.getButtonByText('Remove').click({ force: true }); + getNextButton().should('be.enabled'); + }); - it('...can fail creation', () => { - createDetector(`${detectorName}_fail`, '.kibana_1', true); - cy.getElementByText('.euiCallOut', 'Create detector failed.'); - }); + it('...should show mappings warning', () => { + fillDetailsForm(detectorName, cypressIndexDns); + + getDataSourceField().selectComboboxItem(cypressIndexWindows); + getDataSourceField().focus().blur(); - it('...can be created', () => { - createDetector(detectorName, cypressIndexDns, false); - cy.getElementByText('.euiCallOut', 'Detector created successfully'); + cy.get('.euiCallOut') + .should('be.visible') + .contains( + 'To avoid issues with field mappings, we recommend creating separate detectors for different log types.' + ); + }); }); - it('...basic details can be edited', () => { - cy.intercept('GET', '/indices').as('getIndices'); - openDetectorDetails(detectorName); + describe('...validate create detector flow', () => { + beforeEach(() => { + cy.intercept('/detectors/_search').as('detectorsSearch'); - editDetectorDetails(detectorName, 'Detector details'); + // Visit Detectors page before any test + cy.visit(`${OPENSEARCH_DASHBOARDS_URL}/detectors`); + cy.wait('@detectorsSearch').should('have.property', 'state', 'Complete'); + }); + + it('...can fail creation', () => { + createDetector(`${detectorName}_fail`, '.kibana_1', true); + cy.getElementByText('.euiCallOut', 'Create detector failed.'); + }); - cy.urlShouldContain('edit-detector-details').then(() => { - cy.getElementByText('.euiTitle', 'Edit detector details'); + it('...can be created', () => { + createDetector(detectorName, cypressIndexDns, false); + cy.getElementByText('.euiCallOut', 'Detector created successfully'); }); - cy.wait('@getIndices'); - getNameField().type('{selectall}{backspace}').type('test detector edited'); - cy.getTextareaByLabel('Description - optional').type('Edited description'); + it('...basic details can be edited', () => { + cy.intercept('GET', '/indices').as('getIndices'); + openDetectorDetails(detectorName); + + editDetectorDetails(detectorName, 'Detector details'); - getDataSourceField().clearCombobox(); - getDataSourceField().selectComboboxItem(cypressIndexWindows); + cy.urlShouldContain('edit-detector-details').then(() => { + cy.getElementByText('.euiTitle', 'Edit detector details'); + }); + + cy.wait('@getIndices'); + getNameField().type('{selectall}{backspace}').type('test detector edited'); + cy.getTextareaByLabel('Description - optional').type('Edited description'); - cy.getFieldByLabel('Run every').type('{selectall}{backspace}').type('10'); - cy.getFieldByLabel('Run every', 'select').select('Hours'); + getDataSourceField().clearCombobox(); + getDataSourceField().selectComboboxItem(cypressIndexWindows); - cy.getElementByText('button', 'Save changes').click({ force: true }); + cy.getFieldByLabel('Run every').type('{selectall}{backspace}').type('10'); + cy.getFieldByLabel('Run every', 'select').select('Hours'); - cy.urlShouldContain('detector-details').then(() => { - cy.validateDetailsItem('Detector name', 'test detector edited'); - cy.validateDetailsItem('Description', 'Edited description'); - cy.validateDetailsItem('Detector schedule', 'Every 10 hours'); - cy.validateDetailsItem('Data source', cypressIndexWindows); + cy.getElementByText('button', 'Save changes').click({ force: true }); + + cy.urlShouldContain('detector-details').then(() => { + cy.validateDetailsItem('Detector name', 'test detector edited'); + cy.validateDetailsItem('Description', 'Edited description'); + cy.validateDetailsItem('Detector schedule', 'Every 10 hours'); + cy.validateDetailsItem('Data source', cypressIndexWindows); + }); }); - }); - it('...rules can be edited', () => { - openDetectorDetails(detectorName); + it('...rules can be edited', () => { + openDetectorDetails(detectorName); - editDetectorDetails(detectorName, 'Active rules'); - cy.getElementByText('.euiTitle', 'Detection rules (14)'); + editDetectorDetails(detectorName, 'Active rules'); + cy.getElementByText('.euiTitle', 'Detection rules (14)'); - cy.getInputByPlaceholder('Search...').type(`${cypressDNSRule}`).pressEnterKey(); + cy.getInputByPlaceholder('Search...').type(`${cypressDNSRule}`).pressEnterKey(); - cy.getElementByText('.euiTableCellContent button', cypressDNSRule) - .parents('td') - .prev() - .find('.euiTableCellContent button') - .click(); + cy.getElementByText('.euiTableCellContent button', cypressDNSRule) + .parents('td') + .prev() + .find('.euiTableCellContent button') + .click(); - cy.getElementByText('.euiTitle', 'Detection rules (13)'); - cy.getElementByText('button', 'Save changes').click({ force: true }); - cy.urlShouldContain('detector-details').then(() => { - cy.getElementByText('.euiTitle', detectorName); - cy.getElementByText('.euiPanel .euiTitle', 'Active rules (13)'); + cy.getElementByText('.euiTitle', 'Detection rules (13)'); + cy.getElementByText('button', 'Save changes').click({ force: true }); + cy.urlShouldContain('detector-details').then(() => { + cy.getElementByText('.euiTitle', detectorName); + cy.getElementByText('.euiPanel .euiTitle', 'Active rules (13)'); + }); }); - }); - it('...should update field mappings if data source is changed', () => { - cy.intercept('mappings/view').as('getMappingsView'); - cy.intercept('GET', '/indices').as('getIndices'); - openDetectorDetails(detectorName); + it('...should update field mappings if data source is changed', () => { + cy.intercept('mappings/view').as('getMappingsView'); + cy.intercept('GET', '/indices').as('getIndices'); + openDetectorDetails(detectorName); - editDetectorDetails(detectorName, 'Detector details'); + editDetectorDetails(detectorName, 'Detector details'); - cy.urlShouldContain('edit-detector-details').then(() => { - cy.getElementByText('.euiTitle', 'Edit detector details'); - }); + cy.urlShouldContain('edit-detector-details').then(() => { + cy.getElementByText('.euiTitle', 'Edit detector details'); + }); - cy.wait('@getIndices'); - cy.get('.reviewFieldMappings').should('not.exist'); + cy.wait('@getIndices'); + cy.get('.reviewFieldMappings').should('not.exist'); - getDataSourceField().clearCombobox(); - getDataSourceField().should('not.have.value'); - getDataSourceField().type(`${cypressIndexDns}{enter}`); + getDataSourceField().clearCombobox(); + getDataSourceField().should('not.have.value'); + getDataSourceField().type(`${cypressIndexDns}{enter}`); - validateFieldMappingsTable('data source is changed'); + validateFieldMappingsTable('data source is changed'); - cy.getElementByText('button', 'Save changes').click({ force: true }); - }); + cy.getElementByText('button', 'Save changes').click({ force: true }); + }); - it('...should show field mappings if rule selection is changed', () => { - cy.intercept('mappings/view').as('getMappingsView'); + it('...should show field mappings if rule selection is changed', () => { + cy.intercept('mappings/view').as('getMappingsView'); - openDetectorDetails(detectorName); + openDetectorDetails(detectorName); - editDetectorDetails(detectorName, 'Active rules'); + editDetectorDetails(detectorName, 'Active rules'); - cy.urlShouldContain('edit-detector-rules').then(() => { - cy.getElementByText('.euiTitle', 'Edit detector rules'); - }); + cy.urlShouldContain('edit-detector-rules').then(() => { + cy.getElementByText('.euiTitle', 'Edit detector rules'); + }); - cy.get('.reviewFieldMappings').should('not.exist'); + cy.get('.reviewFieldMappings').should('not.exist'); - cy.wait('@detectorsSearch'); + cy.wait('@detectorsSearch'); - // Toggle single search result to unchecked - cy.get( - '[data-test-subj="edit-detector-rules-table"] table thead tr:first th:first button' - ).click({ force: true }); + // Toggle single search result to unchecked + cy.get( + '[data-test-subj="edit-detector-rules-table"] table thead tr:first th:first button' + ).click({ force: true }); - validateFieldMappingsTable('rules are changed'); - }); + validateFieldMappingsTable('rules are changed'); + }); - it('...can be deleted', () => { - cy.intercept('/_plugins/_security_analytics/rules/_search?prePackaged=true').as( - 'getSigmaRules' - ); - cy.intercept('/_plugins/_security_analytics/rules/_search?prePackaged=false').as( - 'getCustomRules' - ); - openDetectorDetails(detectorName); - - cy.wait('@detectorsSearch'); - cy.wait('@getCustomRules'); - cy.wait('@getSigmaRules'); - - cy.getButtonByText('Actions') - .click({ force: true }) - .then(() => { - cy.intercept('/detectors').as('detectors'); - cy.getElementByText('.euiContextMenuItem', 'Delete').click({ force: true }); - cy.wait('@detectors').then(() => { - cy.contains('There are no existing detectors'); + it('...can be deleted', () => { + cy.intercept('/_plugins/_security_analytics/rules/_search?prePackaged=true').as( + 'getSigmaRules' + ); + cy.intercept('/_plugins/_security_analytics/rules/_search?prePackaged=false').as( + 'getCustomRules' + ); + openDetectorDetails(detectorName); + + cy.wait('@detectorsSearch'); + cy.wait('@getCustomRules'); + cy.wait('@getSigmaRules'); + + cy.getButtonByText('Actions') + .click({ force: true }) + .then(() => { + cy.intercept('/detectors').as('detectors'); + cy.getElementByText('.euiContextMenuItem', 'Delete').click({ force: true }); + cy.wait('@detectors').then(() => { + cy.contains('There are no existing detectors'); + }); }); - }); + }); }); after(() => cy.cleanUpTests()); diff --git a/cypress/integration/2_rules.spec.js b/cypress/integration/2_rules.spec.js index fd1ca9ac9..fde8195c6 100644 --- a/cypress/integration/2_rules.spec.js +++ b/cypress/integration/2_rules.spec.js @@ -10,16 +10,7 @@ const SAMPLE_RULE = { name: `Cypress test rule ${uniqueId}`, logType: 'windows', description: 'This is a rule used to test the rule creation workflow.', - detectionLine: [ - 'condition: Selection_1', - 'Selection_1:', - 'Provider_Name|contains:', - '- Service Control Manager', - 'EventID|contains:', - "- '7045'", - 'ServiceName|contains:', - '- ZzNetSvc', - ], + detectionLine: ['condition: Selection_1', 'Selection_1:', 'FieldKey|contains:', '- FieldValue'], severity: 'critical', tags: ['attack.persistence', 'attack.privilege_escalation', 'attack.t1543.003'], references: 'https://nohello.com', @@ -129,204 +120,500 @@ const checkRulesFlyout = () => { }); }; +const getCreateButton = () => cy.get('[data-test-subj="create_rule_button"]'); +const getNameField = () => cy.getFieldByLabel('Rule name'); +const getRuleStatusField = () => cy.getFieldByLabel('Rule Status'); +const getDescriptionField = () => cy.getFieldByLabel('Description - optional'); +const getAuthorField = () => cy.getFieldByLabel('Author'); +const getLogTypeField = () => cy.getFieldByLabel('Log type'); +const getRuleLevelField = () => cy.getFieldByLabel('Rule level (severity)'); +const getSelectionPanelByIndex = (index) => + cy.get(`[data-test-subj="detection-visual-editor-${index}"]`); +const getSelectionNameField = () => cy.get('[data-test-subj="selection_name"]'); +const getMapKeyField = () => cy.get('[data-test-subj="selection_field_key_name"]'); +const getMapValueField = () => cy.get('[data-test-subj="selection_field_value"]'); +const getMapListField = () => cy.get('[data-test-subj="selection_field_list"]'); +const getListRadioField = () => cy.get('[for="selection-map-list-0-0"]'); +const getTextRadioField = () => cy.get('[for="selection-map-value-0-0"]'); +const getConditionField = () => cy.get('[data-test-subj="rule_detection_field"]'); +const getConditionAddButton = () => cy.get('[data-test-subj="condition-add-selection-btn"]'); +const getConditionRemoveButton = (index) => + cy.get(`[data-test-subj="selection-exp-field-item-remove-${index}"]`); +const getRuleSubmitButton = () => cy.get('[data-test-subj="submit_rule_form_button"]'); +const getTagField = (index) => cy.get(`[data-test-subj="rule_tags_field_${index}"]`); +const getReferenceFieldByIndex = (index) => + cy.get(`[data-test-subj="rule_references_field_${index}"]`); +const getFalsePositiveFieldByIndex = (index) => + cy.get(`[data-test-subj="rule_false_positives_field_${index}"]`); + +const toastShouldExist = () => { + submitRule(); + cy.get('.euiToast').contains('Failed to create rule:'); +}; + +const submitRule = () => getRuleSubmitButton().click({ force: true }); +const fillCreateForm = () => { + // rule overview + getNameField().type(SAMPLE_RULE.name); + getDescriptionField().type(SAMPLE_RULE.description); + getAuthorField().type(`${SAMPLE_RULE.author}`); + + // rule details + getLogTypeField().type(SAMPLE_RULE.logType); + getRuleLevelField().selectComboboxItem(SAMPLE_RULE.severity); + + // rule detection + getSelectionPanelByIndex(0).within(() => { + getSelectionNameField().should('have.value', 'Selection_1'); + getMapKeyField().type('FieldKey'); + + getTextRadioField().click({ force: true }); + getMapValueField().type('FieldValue'); + }); + + getConditionAddButton().click({ force: true }); + + // rule additional details + SAMPLE_RULE.tags.forEach((tag, idx) => { + getTagField(idx).type(tag); + idx < SAMPLE_RULE.tags.length - 1 && cy.getButtonByText('Add tag').click({ force: true }); + }); + + getReferenceFieldByIndex(0).type(SAMPLE_RULE.references); + getFalsePositiveFieldByIndex(0).type(SAMPLE_RULE.falsePositive); +}; + describe('Rules', () => { before(() => cy.cleanUpTests()); - beforeEach(() => { - cy.intercept('/rules/_search').as('rulesSearch'); - // Visit Rules page - cy.visit(`${OPENSEARCH_DASHBOARDS_URL}/rules`); - cy.wait('@rulesSearch').should('have.property', 'state', 'Complete'); - - // Check that correct page is showing - cy.waitForPageLoad('rules', { - contains: 'Detection rules', - }); - }); - it('...can be created', () => { - // Click "create new rule" button - cy.get('[data-test-subj="create_rule_button"]').click({ - force: true, + describe('...should validate form fields', () => { + beforeEach(() => { + cy.intercept('/rules/_search').as('rulesSearch'); + // Visit Rules page + cy.visit(`${OPENSEARCH_DASHBOARDS_URL}/rules`); + cy.wait('@rulesSearch').should('have.property', 'state', 'Complete'); + + // Check that correct page is showing + cy.waitForPageLoad('rules', { + contains: 'Detection rules', + }); + + getCreateButton().click({ force: true }); }); - // Enter the log type - cy.get('[data-test-subj="rule_status_dropdown"]').type(SAMPLE_RULE.status); + it('...should validate rule name', () => { + getNameField().containsHelperText( + 'Rule name must contain 5-50 characters. Valid characters are a-z, A-Z, 0-9, hyphens, spaces, and underscores' + ); - // Enter the name - cy.get('[data-test-subj="rule_name_field"]').type(SAMPLE_RULE.name); + getNameField().should('be.empty'); + getNameField().focus().blur(); + getNameField().containsError('Rule name is required'); + getNameField().type('text').focus().blur(); + getNameField().containsError('Invalid rule name.'); + + getNameField().type('{selectall}').type('{backspace}').type('tex&').focus().blur(); + getNameField().containsError('Invalid rule name.'); + + getNameField() + .type('{selectall}') + .type('{backspace}') + .type('Rule name') + .focus() + .blur() + .shouldNotHaveError(); + }); - // Enter the log type - cy.get('[data-test-subj="rule_type_dropdown"]').type(SAMPLE_RULE.logType); + it('...should validate rule description field', () => { + const longDescriptionText = + 'This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text.'; + + getDescriptionField().should('be.empty'); + getDescriptionField().type(longDescriptionText).focus().blur(); + + getDescriptionField() + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .contains( + 'Description should only consist of upper and lowercase letters, numbers 0-9, commas, hyphens, periods, spaces, and underscores. Max limit of 500 characters.' + ); + + getDescriptionField() + .type('{selectall}') + .type('{backspace}') + .type('Detector description...') + .focus() + .blur(); + + getDescriptionField() + .type('{selectall}') + .type('{backspace}') + .type('Detector name') + .focus() + .blur() + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .should('not.exist'); + }); - // Enter the description - cy.get('[data-test-subj="rule_description_field"]').type(SAMPLE_RULE.description); + it('...should validate author', () => { + getAuthorField().containsHelperText('Combine multiple authors separated with a comma'); + + getAuthorField().should('be.empty'); + getAuthorField().focus().blur(); + getAuthorField().containsError('Author name is required'); + getAuthorField().type('text').focus().blur(); + getAuthorField().containsError('Invalid author.'); + + getAuthorField().type('{selectall}').type('{backspace}').type('tex&').focus().blur(); + getAuthorField().containsError('Invalid author.'); + + getAuthorField() + .type('{selectall}') + .type('{backspace}') + .type('Rule name') + .focus() + .blur() + .shouldNotHaveError(); + }); - // Enter the severity - cy.get('[data-test-subj="rule_severity_dropdown"]').type(SAMPLE_RULE.severity); + it('...should validate log type field', () => { + getLogTypeField().should('be.empty'); + getLogTypeField().focus().blur(); + getLogTypeField().containsError('Log type is required'); - // Enter the tags - SAMPLE_RULE.tags.forEach((tag) => - cy.get('[data-test-subj="rule_tags_dropdown"]').type(`${tag}{enter}`) - ); + getLogTypeField().selectComboboxItem(SAMPLE_RULE.logType); + getLogTypeField().focus().blur().shouldNotHaveError(); + }); - // Enter the reference - cy.contains('Add another URL').click(); - cy.get('[data-test-subj="rule_references_field_0"]').type(SAMPLE_RULE.references); + it('...should validate rule level field', () => { + getRuleLevelField().should('be.empty'); + getRuleLevelField().focus().blur(); + getRuleLevelField().containsError('Rule level is required'); - // Enter the false positive cases - cy.get('[data-test-subj="rule_false_positives_field_0"]').type( - `${SAMPLE_RULE.falsePositive}{enter}` - ); + getRuleLevelField().selectComboboxItem(SAMPLE_RULE.severity); + getRuleLevelField().focus().blur().shouldNotHaveError(); + }); - // Enter the author - cy.get('[data-test-subj="rule_author_field"]').type(`${SAMPLE_RULE.author}{enter}`); + it('...should validate rule status field', () => { + getRuleStatusField().containsValue(SAMPLE_RULE.status); + getRuleStatusField().focus().blur().shouldNotHaveError(); - cy.get('[data-test-subj="detection-visual-editor-0"]').within(() => { - cy.getFieldByLabel('Key').type('Provider_Name'); - cy.getInputByPlaceholder('Value').type('Service Control Manager'); + getRuleStatusField().clearCombobox(); + getRuleStatusField().focus().blur(); + getRuleStatusField().containsError('Rule status is required'); + }); - cy.getButtonByText('Add map').click(); - cy.get('[data-test-subj="Map-1"]').within(() => { - cy.getFieldByLabel('Key').type('EventID'); - cy.getInputByPlaceholder('Value').type('7045'); + it('...should validate selection', () => { + getSelectionPanelByIndex(0).within(() => { + getSelectionNameField().should('have.value', 'Selection_1'); + getSelectionNameField().clearValue(); + getSelectionNameField().focus().blur(); + getSelectionNameField() + .parentsUntil('.euiFormRow__fieldWrapper') + .siblings() + .contains('Selection name is required'); + + getSelectionNameField().type('Selection_1'); + getSelectionNameField() + .focus() + .blur() + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .should('not.exist'); }); + }); - cy.getButtonByText('Add map').click(); - cy.get('[data-test-subj="Map-2"]').within(() => { - cy.getFieldByLabel('Key').type('ServiceName'); - cy.getInputByPlaceholder('Value').type('ZzNetSvc'); + it('...should validate selection map key field', () => { + getSelectionPanelByIndex(0).within(() => { + getMapKeyField().should('be.empty'); + getMapKeyField().focus().blur(); + getMapKeyField() + .parentsUntil('.euiFormRow__fieldWrapper') + .siblings() + .contains('Key name is required'); + + getMapKeyField().type('FieldKey'); + getMapKeyField() + .focus() + .blur() + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .should('not.exist'); }); }); - cy.get('[data-test-subj="rule_detection_field"] textarea').type('Selection_1', { - force: true, + + it('...should validate selection map value field', () => { + getSelectionPanelByIndex(0).within(() => { + getMapValueField().should('be.empty'); + getMapValueField().focus().blur(); + getMapValueField() + .parentsUntil('.euiFormRow__fieldWrapper') + .siblings() + .contains('Value is required'); + + getMapValueField().type('FieldValue'); + getMapValueField() + .focus() + .blur() + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .should('not.exist'); + }); }); - // Switch to YAML editor - cy.get('[data-test-subj="change-editor-type"] label:nth-child(2)').click({ - force: true, + it('...should validate selection map list field', () => { + getSelectionPanelByIndex(0).within(() => { + getListRadioField().click({ force: true }); + getMapListField().should('be.empty'); + getMapListField().focus().blur(); + getMapListField().parentsUntil('.euiFormRow').contains('Value is required'); + + getMapListField().type('FieldValue'); + getMapListField() + .focus() + .blur() + .parents('.euiFormRow') + .find('.euiFormErrorText') + .should('not.exist'); + }); }); - YAML_RULE_LINES.forEach((line) => cy.get('[data-test-subj="rule_yaml_editor"]').contains(line)); + it('...should validate condition field', () => { + getConditionField().scrollIntoView(); + getConditionField().find('.euiFormErrorText').should('not.exist'); + getRuleSubmitButton().click({ force: true }); + getConditionField().parents('.euiFormRow__fieldWrapper').contains('Condition is required'); - cy.intercept({ - url: '/rules', - }).as('getRules'); + getConditionAddButton().click({ force: true }); + getConditionField().find('.euiFormErrorText').should('not.exist'); - // Click "create" button - cy.get('[data-test-subj="submit_rule_form_button"]').click({ - force: true, + getConditionRemoveButton(0).click({ force: true }); + getConditionField().parents('.euiFormRow__fieldWrapper').contains('Condition is required'); }); - cy.wait('@getRules'); - - cy.waitForPageLoad('rules', { - contains: 'Detection rules', + it('...should validate tag field', () => { + getTagField(0).should('be.empty'); + getTagField(0).type('wrong.tag').focus().blur(); + getTagField(0) + .parents('.euiFormRow__fieldWrapper') + .contains("Tags must start with 'attack.'"); + + getTagField(0).clearValue().type('attack.tag'); + getTagField(0) + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .should('not.exist'); }); - checkRulesFlyout(); + it('...should validate form', () => { + toastShouldExist(); + fillCreateForm(); + + // rule name field + getNameField().clearValue(); + toastShouldExist(); + getNameField().type('Rule name'); + + // author field + getAuthorField().clearValue(); + toastShouldExist(); + getAuthorField().type('John Doe'); + + // log field + getLogTypeField().clearCombobox(); + toastShouldExist(); + getLogTypeField().selectComboboxItem(SAMPLE_RULE.logType); + + // severity field + getRuleLevelField().clearCombobox(); + toastShouldExist(); + getRuleLevelField().selectComboboxItem(SAMPLE_RULE.severity); + + // status field + getRuleStatusField().clearCombobox(); + toastShouldExist(); + getRuleStatusField().selectComboboxItem(SAMPLE_RULE.status); + + // selection name field + getSelectionPanelByIndex(0).within(() => + getSelectionNameField().type('{selectall}').type('{backspace}') + ); + toastShouldExist(); + getSelectionPanelByIndex(0).within(() => getSelectionNameField().type('Selection_1')); + + // selection map key field + getSelectionPanelByIndex(0).within(() => + getMapKeyField().type('{selectall}').type('{backspace}') + ); + toastShouldExist(); + getSelectionPanelByIndex(0).within(() => getMapKeyField().type('FieldKey')); + + // selection map value field + getSelectionPanelByIndex(0).within(() => + getMapValueField().type('{selectall}').type('{backspace}') + ); + toastShouldExist(); + getSelectionPanelByIndex(0).within(() => getMapValueField().type('FieldValue')); + + // selection map list field + getSelectionPanelByIndex(0).within(() => { + getListRadioField().click({ force: true }); + getMapListField().clearValue(); + }); + toastShouldExist(); + getSelectionPanelByIndex(0).within(() => { + getListRadioField().click({ force: true }); + getMapListField().type('FieldValue'); + }); + + // condition field + getConditionRemoveButton(0).click({ force: true }); + toastShouldExist(); + getConditionAddButton().click({ force: true }); + + // tags field + getTagField(0).clearValue().type('wrong.tag'); + toastShouldExist(); + getTagField(0).clearValue().type('attack.tag'); + }); }); - it('...can be edited', () => { - cy.waitForPageLoad('rules', { - contains: 'Detection rules', + describe('...should validate create rule flow', () => { + beforeEach(() => { + cy.intercept('/rules/_search').as('rulesSearch'); + // Visit Rules page + cy.visit(`${OPENSEARCH_DASHBOARDS_URL}/rules`); + cy.wait('@rulesSearch').should('have.property', 'state', 'Complete'); + + // Check that correct page is showing + cy.waitForPageLoad('rules', { + contains: 'Detection rules', + }); }); - cy.get(`input[placeholder="Search rules"]`).ospSearch(SAMPLE_RULE.name); - cy.get(`[data-test-subj="rule_link_${SAMPLE_RULE.name}"]`).click({ force: true }); + it('...can be created', () => { + getCreateButton().click({ force: true }); - cy.get(`[data-test-subj="rule_flyout_${SAMPLE_RULE.name}"]`) - .find('button') - .contains('Action') - .click({ force: true }) - .then(() => { - // Confirm arrival at detectors page - cy.get('.euiPopover__panel').find('button').contains('Edit').click(); + fillCreateForm(); + + // Switch to YAML editor + cy.get('[data-test-subj="change-editor-type"] label:nth-child(2)').click({ + force: true, }); - const ruleNameSelector = '[data-test-subj="rule_name_field"]'; - cy.get(ruleNameSelector).clear(); - - SAMPLE_RULE.name += ' edited'; - cy.get(ruleNameSelector).type(SAMPLE_RULE.name); - cy.get(ruleNameSelector).should('have.value', SAMPLE_RULE.name); - - // Enter the log type - const logSelector = '[data-test-subj="rule_type_dropdown"]'; - cy.get(logSelector).within(() => cy.get('.euiFormControlLayoutClearButton').click()); - SAMPLE_RULE.logType = 'dns'; - YAML_RULE_LINES[2] = `product: ${SAMPLE_RULE.logType}`; - YAML_RULE_LINES[3] = `title: ${SAMPLE_RULE.name}`; - cy.get(logSelector).type(SAMPLE_RULE.logType).type('{enter}'); - cy.get(logSelector).contains(SAMPLE_RULE.logType, { - matchCase: false, - }); + YAML_RULE_LINES.forEach((line) => + cy.get('[data-test-subj="rule_yaml_editor"]').contains(line) + ); - const ruleDescriptionSelector = '[data-test-subj="rule_description_field"]'; - SAMPLE_RULE.description += ' edited'; - YAML_RULE_LINES[4] = `description: ${SAMPLE_RULE.description}`; - cy.get(ruleDescriptionSelector).clear(); - cy.get(ruleDescriptionSelector).type(SAMPLE_RULE.description); - cy.get(ruleDescriptionSelector).should('have.value', SAMPLE_RULE.description); + cy.intercept({ + url: '/rules', + }).as('getRules'); - cy.intercept({ - url: '/rules', - }).as('getRules'); + submitRule(); - // Click "create" button - cy.get('[data-test-subj="submit_rule_form_button"]').click({ - force: true, - }); + cy.wait('@getRules'); + + cy.waitForPageLoad('rules', { + contains: 'Detection rules', + }); - cy.waitForPageLoad('rules', { - contains: 'Detection rules', + checkRulesFlyout(); }); - cy.wait('@getRules'); + it('...can be edited', () => { + cy.waitForPageLoad('rules', { + contains: 'Detection rules', + }); - checkRulesFlyout(); - }); + cy.get(`input[placeholder="Search rules"]`).ospSearch(SAMPLE_RULE.name); + cy.get(`[data-test-subj="rule_link_${SAMPLE_RULE.name}"]`).click({ force: true }); + + cy.get(`[data-test-subj="rule_flyout_${SAMPLE_RULE.name}"]`) + .find('button') + .contains('Action') + .click({ force: true }) + .then(() => { + // Confirm arrival at detectors page + cy.get('.euiPopover__panel').find('button').contains('Edit').click(); + }); + + getNameField().clear(); + + SAMPLE_RULE.name += ' edited'; + getNameField().type(SAMPLE_RULE.name); + getNameField().should('have.value', SAMPLE_RULE.name); + + getLogTypeField().clearCombobox(); + SAMPLE_RULE.logType = 'dns'; + YAML_RULE_LINES[2] = `product: ${SAMPLE_RULE.logType}`; + YAML_RULE_LINES[3] = `title: ${SAMPLE_RULE.name}`; + getLogTypeField().type(SAMPLE_RULE.logType).type('{enter}'); + getLogTypeField().containsValue(SAMPLE_RULE.logType).contains(SAMPLE_RULE.logType); + + SAMPLE_RULE.description += ' edited'; + YAML_RULE_LINES[4] = `description: ${SAMPLE_RULE.description}`; + getDescriptionField().clear(); + getDescriptionField().type(SAMPLE_RULE.description); + getDescriptionField().should('have.value', SAMPLE_RULE.description); - it('...can be deleted', () => { - cy.intercept({ - url: '/rules', - }).as('deleteRule'); - - cy.intercept('POST', 'rules/_search?prePackaged=true', { - delay: 5000, - }).as('getPrePackagedRules'); - - cy.intercept('POST', 'rules/_search?prePackaged=false', { - delay: 5000, - }).as('getCustomRules'); - - cy.wait('@rulesSearch'); - cy.get(`input[placeholder="Search rules"]`).ospSearch(SAMPLE_RULE.name); - - // Click the rule link to open the details flyout - cy.get(`[data-test-subj="rule_link_${SAMPLE_RULE.name}"]`).click({ force: true }); - - cy.get(`[data-test-subj="rule_flyout_${SAMPLE_RULE.name}"]`) - .find('button') - .contains('Action') - .click({ force: true }) - .then(() => { - // Confirm arrival at detectors page - cy.get('.euiPopover__panel') - .find('button') - .contains('Delete') - .click() - .then(() => cy.get('.euiModalFooter > .euiButton').contains('Delete').click()); - - cy.wait('@deleteRule'); - cy.wait('@getCustomRules'); - cy.wait('@getPrePackagedRules'); - - // Search for sample_detector, presumably deleted - cy.wait(3000); - cy.get(`input[placeholder="Search rules"]`).ospSearch(SAMPLE_RULE.name); - // Click the rule link to open the details flyout - cy.get('tbody').contains(SAMPLE_RULE.name).should('not.exist'); + cy.intercept({ + url: '/rules', + }).as('getRules'); + + submitRule(); + + cy.waitForPageLoad('rules', { + contains: 'Detection rules', }); + + cy.wait('@getRules'); + + checkRulesFlyout(); + }); + + it('...can be deleted', () => { + cy.intercept({ + url: '/rules', + }).as('deleteRule'); + + cy.intercept('POST', 'rules/_search?prePackaged=true', { + delay: 5000, + }).as('getPrePackagedRules'); + + cy.intercept('POST', 'rules/_search?prePackaged=false', { + delay: 5000, + }).as('getCustomRules'); + + cy.wait('@rulesSearch'); + cy.get(`input[placeholder="Search rules"]`).ospSearch(SAMPLE_RULE.name); + + // Click the rule link to open the details flyout + cy.get(`[data-test-subj="rule_link_${SAMPLE_RULE.name}"]`).click({ force: true }); + + cy.get(`[data-test-subj="rule_flyout_${SAMPLE_RULE.name}"]`) + .find('button') + .contains('Action') + .click({ force: true }) + .then(() => { + // Confirm arrival at detectors page + cy.get('.euiPopover__panel') + .find('button') + .contains('Delete') + .click() + .then(() => cy.get('.euiModalFooter > .euiButton').contains('Delete').click()); + + cy.wait('@deleteRule'); + cy.wait('@getCustomRules'); + cy.wait('@getPrePackagedRules'); + + // Search for sample_detector, presumably deleted + cy.wait(3000); + cy.get(`input[placeholder="Search rules"]`).ospSearch(SAMPLE_RULE.name); + // Click the rule link to open the details flyout + cy.get('tbody').contains(SAMPLE_RULE.name).should('not.exist'); + }); + }); }); after(() => cy.cleanUpTests()); diff --git a/cypress/support/helpers.js b/cypress/support/helpers.js index ca572887e..c3b990892 100644 --- a/cypress/support/helpers.js +++ b/cypress/support/helpers.js @@ -4,7 +4,7 @@ */ import sample_detector from '../fixtures/integration_tests/detector/create_usb_detector_data.json'; -import { NODE_API, OPENSEARCH_DASHBOARDS_URL } from './constants'; +import { OPENSEARCH_DASHBOARDS_URL } from './constants'; import _ from 'lodash'; Cypress.Commands.add('getElementByText', (locator, text) => { @@ -62,17 +62,7 @@ Cypress.Commands.add( items = [items]; } Cypress.log({ message: `Select combobox items: ${items.join(' | ')}` }); - cy.wrap(subject) - .focus() - .click({ force: true }) - .then(() => { - items.map((item) => - cy.get('.euiComboBoxOptionsList__rowWrap').within(() => { - cy.get('button').contains(item).should('be.visible'); - cy.get('button').contains(item).click(); - }) - ); - }); + items.map((item) => cy.wrap(subject).type(item).type('{enter}')); } ); @@ -85,23 +75,70 @@ Cypress.Commands.add( Cypress.log({ message: `Clear combobox` }); return cy .wrap(subject) - .parents('.euiComboBox__inputWrap') - .find('.euiBadge') - .then(($badge) => { - let numberOfBadges = $badge.length; - Cypress.log({ - message: `Number of combo badges to clear: ${numberOfBadges}`, - }); - - cy.wrap(subject) - .parents('.euiComboBox__inputWrap') - .find('input') - .focus() - .pressBackspaceKey(numberOfBadges); - }); + .parents('.euiFormRow__fieldWrapper') + .find('[data-test-subj="comboBoxClearButton"]') + .click({ force: true }); } ); +Cypress.Commands.add( + 'containsValue', + { + prevSubject: true, + }, + (subject, value) => + cy.wrap(subject).parents('.euiFormRow__fieldWrapper').contains(value, { + matchCase: false, + }) +); + +Cypress.Commands.add( + 'clearValue', + { + prevSubject: true, + }, + (subject) => cy.wrap(subject).type('{selectall}').type('{backspace}') +); + +Cypress.Commands.add( + 'containsError', + { + prevSubject: true, + }, + (subject, errorText) => + cy + .wrap(subject) + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .contains(errorText) +); + +Cypress.Commands.add( + 'containsHelperText', + { + prevSubject: true, + }, + (subject, helperText) => + cy + .wrap(subject) + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormHelpText') + .contains(helperText) +); + +Cypress.Commands.add( + 'shouldNotHaveError', + { + prevSubject: true, + }, + (subject) => + cy + .wrap(subject) + .parents('.euiFormRow__fieldWrapper') + .find('.euiFormErrorText') + .should('not.exist') +); + Cypress.Commands.add('validateDetailsItem', (label, value) => { Cypress.log({ message: `Validate details item by label: ${label} and value: ${value}` }); return cy.getElementByText('.euiFlexItem label', label).parent().siblings().contains(value); diff --git a/cypress/support/index.d.ts b/cypress/support/index.d.ts index 2ffaed7e5..1225c9c59 100644 --- a/cypress/support/index.d.ts +++ b/cypress/support/index.d.ts @@ -99,6 +99,42 @@ declare namespace Cypress { */ validateDetailsItem(label: string, value: string): Chainable; + /** + * Should clear a field value (use with text and textarea fields) + * @example + * cy.getFieldByLabel('Rule name').clearValue() + */ + clearValue(): Chainable; + + /** + * Validates that field contains value + * Should be used with combobox or other fields that don't print its value in inputs + * @example + * cy.getFieldByLabel('Rule name').containsValue('Name') + */ + containsValue(value: string): Chainable; + + /** + * Validates that field has error text + * @example + * cy.getFieldByLabel('Rule name').containsError('This fields is invalid') + */ + containsError(errorText: string): Chainable; + + /** + * Validates that field has helper text + * @example + * cy.getFieldByLabel('Rule name').containsHelperText('Use this field for...') + */ + containsHelperText(helperText: string): Chainable; + + /** + * Should not have error text + * @example + * cy.getFieldByLabel('Rule name').shouldNotHaveError() + */ + shouldNotHaveError(): Chainable; + /** * Validates url path * @example diff --git a/package.json b/package.json index 0789a3bb3..e777af542 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,8 @@ "jest-environment-jsdom": "^27.5.1", "lint-staged": "^10.2.0", "ts-loader": "^6.2.1", - "@types/vis": "^4.21.21" + "@types/vis": "^4.21.21", + "string.prototype.replaceall": "1.0.7" }, "engines": { "yarn": "^1.21.1" diff --git a/public/app.scss b/public/app.scss index 52b92d7d5..d721e5522 100644 --- a/public/app.scss +++ b/public/app.scss @@ -20,6 +20,7 @@ $euiTextColor: $euiColorDarkestShade !default; @import "./pages/Findings/components/CorrelationsTable/CorrelationsTable.scss"; @import "./pages/Rules/components/RuleEditor/RuleEditorForm.scss"; @import "./pages/Rules/components/RuleEditor/DetectionVisualEditor.scss"; +@import "./pages/Rules/components/RuleEditor/components/SelectionExpField.scss"; .selected-radio-panel { background-color: tintOrShade($euiColorPrimary, 90%, 70%); @@ -135,3 +136,11 @@ $euiTextColor: $euiColorDarkestShade !default; .detailsFormRow { width: auto !important; } + +.empty-text-button { + vertical-align: baseline; + + .euiButtonEmpty__content { + padding: 0 !important; + } +} diff --git a/public/pages/Rules/components/DeleteModal/DeleteRuleModal.test.tsx b/public/pages/Rules/components/DeleteModal/DeleteRuleModal.test.tsx new file mode 100644 index 000000000..5108f60b8 --- /dev/null +++ b/public/pages/Rules/components/DeleteModal/DeleteRuleModal.test.tsx @@ -0,0 +1,16 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import DeleteRuleModalMock from '../../../../../test/mocks/Rules/components/DeleteModal/DeleteRuleModal.mock'; +import { DeleteRuleModal } from './DeleteRuleModal'; + +describe(' spec', () => { + it('renders the component', () => { + const tree = render(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/public/pages/Rules/components/DeleteModal/DeleteModal.tsx b/public/pages/Rules/components/DeleteModal/DeleteRuleModal.tsx similarity index 100% rename from public/pages/Rules/components/DeleteModal/DeleteModal.tsx rename to public/pages/Rules/components/DeleteModal/DeleteRuleModal.tsx diff --git a/public/pages/Rules/components/DeleteModal/__snapshots__/DeleteRuleModal.test.tsx.snap b/public/pages/Rules/components/DeleteModal/__snapshots__/DeleteRuleModal.test.tsx.snap new file mode 100644 index 000000000..e07727f3e --- /dev/null +++ b/public/pages/Rules/components/DeleteModal/__snapshots__/DeleteRuleModal.test.tsx.snap @@ -0,0 +1,168 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` spec renders the component 1`] = ` +Object { + "asFragment": [Function], + "baseElement": +