diff --git a/.github/workflows/cypress-workflow.yml b/.github/workflows/cypress-workflow.yml index 6dcef19f2..7904a4776 100644 --- a/.github/workflows/cypress-workflow.yml +++ b/.github/workflows/cypress-workflow.yml @@ -7,8 +7,8 @@ on: branches: - "*" env: - OPENSEARCH_DASHBOARDS_VERSION: '2.11.0' - SECURITY_ANALYTICS_BRANCH: '2.11.0.0' + OPENSEARCH_DASHBOARDS_VERSION: '2.11' + SECURITY_ANALYTICS_BRANCH: '2.11' jobs: tests: name: Run Cypress E2E tests diff --git a/cypress/integration/1_detectors.spec.js b/cypress/integration/1_detectors.spec.js index c1908f7b3..cf68e69ae 100644 --- a/cypress/integration/1_detectors.spec.js +++ b/cypress/integration/1_detectors.spec.js @@ -10,13 +10,11 @@ import dns_name_rule_data from '../fixtures/integration_tests/rule/create_dns_ru import dns_type_rule_data from '../fixtures/integration_tests/rule/create_dns_rule_with_type_selection.json'; import _ from 'lodash'; import { getMappingFields } from '../../public/pages/Detectors/utils/helpers'; -import { getLogTypeLabel } from '../../public/pages/LogTypes/utils/helpers'; const cypressIndexDns = 'cypress-index-dns'; const cypressIndexWindows = 'cypress-index-windows'; const detectorName = 'test detector'; const cypressLogTypeDns = 'dns'; -const sampleNotificationChannel = 'sample_chime_channel'; const cypressDNSRule = dns_name_rule_data.title; @@ -38,14 +36,10 @@ const dataSourceLabel = 'Select or input source indexes or index patterns'; const getDataSourceField = () => cy.getFieldByLabel(dataSourceLabel); -const logTypeLabel = 'Log type'; +const logTypeLabel = 'Select a log type you would like to detect'; const getLogTypeField = () => cy.getFieldByLabel(logTypeLabel); -const notificationLabel = 'Notification channel'; - -const getNotificationField = () => cy.getFieldByLabel(notificationLabel); - const openDetectorDetails = (detectorName) => { cy.getInputByPlaceholder('Search threat detectors').type(`${detectorName}`).pressEnterKey(); cy.getElementByText('.euiTableCellContent button', detectorName).click(); @@ -120,9 +114,9 @@ const validatePendingFieldMappingsPanel = (mappings) => { const fillDetailsForm = (detectorName, dataSource) => { getNameField().type(detectorName); getDataSourceField().selectComboboxItem(dataSource); - getDataSourceField().focus().blur(); - getLogTypeField().selectComboboxItem(getLogTypeLabel(cypressLogTypeDns)); - getLogTypeField().focus().blur(); + getDataSourceField().blur(); + getLogTypeField().selectComboboxItem(cypressLogTypeDns); + getLogTypeField().blur(); }; const createDetector = (detectorName, dataSource, expectFailure) => { @@ -130,9 +124,9 @@ const createDetector = (detectorName, dataSource, expectFailure) => { fillDetailsForm(detectorName, dataSource); - cy.getElementByText('.euiAccordion .euiTitle', 'Selected detection rules (14)') + cy.getElementByText('.euiAccordion .euiTitle', 'Detection rules (14 selected)') .click({ force: true, timeout: 5000 }) - .then(() => cy.contains('.euiTable .euiTableRow', getLogTypeLabel(cypressLogTypeDns))); + .then(() => cy.contains('.euiTable .euiTableRow', 'Dns')); cy.getElementByText('.euiAccordion .euiTitle', 'Field mapping - optional'); cy.get('[aria-controls="mappedTitleFieldsAccordion"]').then(($btn) => { @@ -155,7 +149,7 @@ const createDetector = (detectorName, dataSource, expectFailure) => { .focus() .blur(); - getNotificationField().selectComboboxItem(`[Channel] ${sampleNotificationChannel}`); + cy.getFieldByLabel('Specify alert severity').selectComboboxItem('1 (Highest)'); cy.intercept('POST', '/_plugins/_security_analytics/mappings').as('createMappingsRequest'); cy.intercept('POST', '/_plugins/_security_analytics/detectors').as('createDetectorRequest'); @@ -173,6 +167,8 @@ const createDetector = (detectorName, dataSource, expectFailure) => { cy.url() .should('contain', detectorId) .then(() => { + cy.getElementByText('.euiCallOut', `Detector created successfully: ${detectorName}`); + // Confirm detector state cy.getElementByText('.euiTitle', detectorName); cy.getElementByText('.euiHealth', 'Active').then(() => { @@ -220,20 +216,6 @@ describe('Detectors', () => { cy.createRule(dns_name_rule_data); cy.createRule(dns_type_rule_data); - - cy.request('POST', 'http://localhost:9200/_plugins/_notifications/configs/', { - config_id: 'sa_notification-channel_id', - name: sampleNotificationChannel, - config: { - name: sampleNotificationChannel, - description: 'This is a sample chime channel', - config_type: 'chime', - is_enabled: true, - chime: { - url: 'https://sample-chime-webhook', - }, - }, - }).should('have.property', 'status', 200); }); describe('...should validate form fields', () => { @@ -349,6 +331,8 @@ describe('Detectors', () => { .find('.euiFormErrorText') .should('not.exist'); + getCreateDetectorButton().should('be.enabled'); + getTriggerNameField().type('{selectall}').type('{backspace}').focus().blur(); getCreateDetectorButton().should('be.disabled'); @@ -512,11 +496,5 @@ describe('Detectors', () => { }); }); - after(() => { - cy.cleanUpTests(); - cy.request( - 'DELETE', - 'http://localhost:9200/_plugins/_notifications/configs/sa_notification-channel_id' - ); - }); + after(() => cy.cleanUpTests()); }); diff --git a/cypress/integration/2_rules.spec.js b/cypress/integration/2_rules.spec.js index b09a9d385..89544f177 100644 --- a/cypress/integration/2_rules.spec.js +++ b/cypress/integration/2_rules.spec.js @@ -4,7 +4,6 @@ */ import { OPENSEARCH_DASHBOARDS_URL } from '../support/constants'; -import { getLogTypeLabel } from '../../public/pages/LogTypes/utils/helpers'; const uniqueId = Cypress._.random(0, 1e6); const SAMPLE_RULE = { @@ -56,9 +55,7 @@ const checkRulesFlyout = () => { cy.get('[data-test-subj="rule_flyout_rule_name"]').contains(SAMPLE_RULE.name); // Validate log type - cy.get('[data-test-subj="rule_flyout_rule_log_type"]').contains( - getLogTypeLabel(SAMPLE_RULE.logType) - ); + cy.get('[data-test-subj="rule_flyout_rule_log_type"]').contains(SAMPLE_RULE.logType); // Validate description cy.get('[data-test-subj="rule_flyout_rule_description"]').contains(SAMPLE_RULE.description); @@ -164,7 +161,7 @@ const fillCreateForm = () => { getAuthorField().type(`${SAMPLE_RULE.author}`); // rule details - getLogTypeField().selectComboboxItem(getLogTypeLabel(SAMPLE_RULE.logType)); + getLogTypeField().selectComboboxItem(SAMPLE_RULE.logType); getRuleLevelField().selectComboboxItem(SAMPLE_RULE.severity); // rule detection @@ -287,7 +284,7 @@ describe('Rules', () => { getLogTypeField().focus().blur(); getLogTypeField().containsError('Log type is required'); - getLogTypeField().selectComboboxItem(getLogTypeLabel(SAMPLE_RULE.logType)); + getLogTypeField().selectComboboxItem(SAMPLE_RULE.logType); getLogTypeField().focus().blur().shouldNotHaveError(); }); @@ -428,7 +425,7 @@ describe('Rules', () => { // log field getLogTypeField().clearCombobox(); toastShouldExist(); - getLogTypeField().selectComboboxItem(getLogTypeLabel(SAMPLE_RULE.logType)); + getLogTypeField().selectComboboxItem(SAMPLE_RULE.logType); // severity field getRuleLevelField().clearCombobox(); @@ -553,10 +550,8 @@ describe('Rules', () => { SAMPLE_RULE.logType = 'dns'; YAML_RULE_LINES[2] = `product: ${SAMPLE_RULE.logType}`; YAML_RULE_LINES[3] = `title: ${SAMPLE_RULE.name}`; - getLogTypeField().selectComboboxItem(getLogTypeLabel(SAMPLE_RULE.logType)); - getLogTypeField() - .containsValue(SAMPLE_RULE.logType) - .contains(getLogTypeLabel(SAMPLE_RULE.logType)); + getLogTypeField().selectComboboxItem(SAMPLE_RULE.logType); + getLogTypeField().containsValue(SAMPLE_RULE.logType).contains(SAMPLE_RULE.logType); SAMPLE_RULE.description += ' edited'; YAML_RULE_LINES[4] = `description: ${SAMPLE_RULE.description}`; diff --git a/cypress/integration/3_alerts.spec.js b/cypress/integration/3_alerts.spec.js index 23a752a1e..81341897f 100644 --- a/cypress/integration/3_alerts.spec.js +++ b/cypress/integration/3_alerts.spec.js @@ -10,7 +10,6 @@ import aliasMappings from '../fixtures/sample_alias_mappings.json'; import indexDoc from '../fixtures/sample_document.json'; import ruleSettings from '../fixtures/integration_tests/rule/create_windows_usb_rule.json'; import { createDetector } from '../support/helpers'; -import { getLogTypeLabel } from '../../public/pages/LogTypes/utils/helpers'; const indexName = 'test-index'; const detectorName = 'test-detector'; @@ -119,9 +118,7 @@ describe('Alerts', () => { expect($tr, `timestamp`).to.contain(date); expect($tr, `rule name`).to.contain('Cypress USB Rule'); expect($tr, `detector name`).to.contain(testDetector.name); - expect($tr, `log type`).to.contain( - `System Activity: ${getLogTypeLabel(testDetector.detector_type)}` - ); + expect($tr, `log type`).to.contain('System Activity: Windows'); }); // Close the flyout @@ -165,10 +162,8 @@ describe('Alerts', () => { // Confirm finding timestamp cy.get('[data-test-subj="finding-details-flyout-timestamp"]').contains(date); - // Confirm finding detection type - cy.get('[data-test-subj="finding-details-flyout-detection-type"]').contains( - 'Detection rules' - ); + // Confirm finding detector name + cy.get('[data-test-subj="finding-details-flyout-detector-link"]').contains(testDetector.name); // Confirm there's only 1 rule details accordion cy.get('[data-test-subj="finding-details-flyout-rule-accordion-1"]').should('not.exist'); @@ -194,9 +189,7 @@ describe('Alerts', () => { cy.get('[data-test-subj="finding-details-flyout-rule-severity"]').contains('High'); // Confirm the rule category - cy.get('[data-test-subj="finding-details-flyout-rule-category"]').contains( - getLogTypeLabel(testDetector.detector_type) - ); + cy.get('[data-test-subj="finding-details-flyout-rule-category"]').contains('Windows'); // Confirm the rule description cy.get('[data-test-subj="finding-details-flyout-rule-description"]').contains( @@ -360,5 +353,40 @@ describe('Alerts', () => { }); }); + it('detector name hyperlink on finding details flyout redirects to the detector details page', () => { + // Open first alert details flyout + cy.get('tbody > tr') + .first() + .within(() => { + // Click the "View details" button for the first alert + cy.get('[aria-label="View details"]').click({ force: true }); + }); + + cy.get('[data-test-subj="alert-details-flyout"]').within(() => { + // Wait for findings table to finish loading + cy.contains('Cypress USB Rule'); + + // Click the details button for the first finding + cy.get('tbody > tr') + .first() + .within(() => { + cy.get('[data-test-subj="finding-details-flyout-button"]').click({ + force: true, + }); + }); + }); + + cy.get('[data-test-subj="finding-details-flyout"]').within(() => { + // Click the detector name hyperlink + cy.get('[data-test-subj="finding-details-flyout-detector-link"]') + // Removing the "target" attribute so the link won't open a new tab. Cypress wouldn't test the new tab. + .invoke('removeAttr', 'target') + .click({ force: true }); + }); + + // Confirm the detector details page is for the expected detector + cy.get('[data-test-subj="detector-details-detector-name"]').contains(testDetector.name); + }); + after(() => cy.cleanUpTests()); }); diff --git a/cypress/integration/4_findings.spec.js b/cypress/integration/4_findings.spec.js index 3fa12d608..a2a2787bb 100644 --- a/cypress/integration/4_findings.spec.js +++ b/cypress/integration/4_findings.spec.js @@ -9,7 +9,6 @@ import aliasMappings from '../fixtures/sample_alias_mappings.json'; import indexDoc from '../fixtures/sample_document.json'; import ruleSettings from '../fixtures/integration_tests/rule/create_windows_usb_rule.json'; import { createDetector } from '../support/helpers'; -import { getLogTypeLabel } from '../../public/pages/LogTypes/utils/helpers'; const indexName = 'test-index'; const detectorName = 'test-detector'; @@ -53,7 +52,7 @@ describe('Findings', () => { cy.contains('No items found').should('not.exist'); // Check for expected findings - cy.contains(`System Activity: ${getLogTypeLabel(testDetector.detector_type)}`); + cy.contains('System Activity: Windows'); cy.contains('High'); }); diff --git a/cypress/integration/5_integrations.spec.js b/cypress/integration/5_integrations.spec.js new file mode 100644 index 000000000..054295d7a --- /dev/null +++ b/cypress/integration/5_integrations.spec.js @@ -0,0 +1,142 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DETECTOR_TRIGGER_TIMEOUT, OPENSEARCH_DASHBOARDS_URL } from '../support/constants'; +import sample_index_settings from '../fixtures/sample_windows_index_settings.json'; +import sample_dns_settings from '../fixtures/integration_tests/index/create_dns_settings.json'; +import windows_usb_rule_data from '../fixtures/integration_tests/rule/create_windows_usb_rule.json'; +import dns_rule_data from '../fixtures/integration_tests/rule/create_dns_rule_with_name_selection.json'; +import usb_detector_data from '../fixtures/integration_tests/detector/create_usb_detector_data.json'; +import usb_detector_data_mappings from '../fixtures/integration_tests/detector/create_usb_detector_mappings_data.json'; +import dns_detector_data_mappings from '../fixtures/integration_tests/detector/create_dns_detector_mappings_data.json'; +import dns_detector_data from '../fixtures/integration_tests/detector/create_dns_detector_data.json'; +import add_windows_index_data from '../fixtures/integration_tests/index/add_windows_index_data.json'; +import add_dns_index_data from '../fixtures/integration_tests/index/add_dns_index_data.json'; + +describe('Integration tests', () => { + const indexName = 'cypress-index-windows'; + const dnsName = 'cypress-index-dns'; + + before(() => { + cy.cleanUpTests(); + + // Create custom rules + cy.createRule(windows_usb_rule_data).then((response) => { + usb_detector_data.inputs[0].detector_input.custom_rules[0].id = response.body.response._id; + usb_detector_data.triggers[0].ids.push(response.body.response._id); + }); + cy.createRule(dns_rule_data).then((response) => { + dns_detector_data.inputs[0].detector_input.custom_rules[0].id = response.body.response._id; + dns_detector_data.triggers[0].ids.push(response.body.response._id); + }); + + // Create test index + cy.createIndex(indexName, sample_index_settings); + cy.createIndex(dnsName, sample_dns_settings); + + // Create detectors + cy.createAliasMappings( + indexName, + usb_detector_data.detector_type, + usb_detector_data_mappings, + true + ).then(() => cy.createDetector(usb_detector_data)); + + cy.createAliasMappings( + dnsName, + dns_detector_data.detector_type, + dns_detector_data_mappings, + true + ).then(() => cy.createDetector(dns_detector_data)); + + // Ingest docs + cy.request( + 'POST', + `${Cypress.env('opensearch')}/${indexName}/_doc/101`, + add_windows_index_data + ); + cy.request('POST', `${Cypress.env('opensearch')}/${dnsName}/_doc/101`, add_dns_index_data); + + // Wait for detector interval to pass + cy.wait(DETECTOR_TRIGGER_TIMEOUT); + }); + + beforeEach(() => { + // Visit Detectors page + cy.visit(`${OPENSEARCH_DASHBOARDS_URL}/detectors`); + + // Wait for page to load + cy.waitForPageLoad('detectors', 'Threat detectors'); + }); + + xit('...can navigate to findings page', () => { + cy.intercept({ + method: 'GET', + pathname: '/_plugins/_security_analytics/findings/_search', + }).as('getFindings'); + + // Cypress USB Detector + cy.contains('Cypress USB Detector') + .click() + .then(() => { + cy.contains('View Findings') + .click() + .then(() => { + cy.hash() + .should('match', /findings\/.+$/) + .then((hash) => { + const detectorId = hash.replace('#/findings/', ''); + if (!detectorId) { + throw new Error('Navigating to findings page should contain detector ID'); + } else { + cy.wait('@getFindings').then((interception) => { + const url = new URL(interception.request.url); + // The request query param detectorId should match the hash param from the url + expect(url.searchParams.get('detectorId')).to.eq(detectorId); + }); + + // There should be only one call to the API + cy.get('@getFindings.all').should('have.length', 1); + } + }); + }); + }); + }); + xit('...can navigate to alerts page', () => { + cy.intercept({ + method: 'GET', + pathname: '/_plugins/_security_analytics/alerts', + }).as('getAlerts'); + + // Cypress USB Detector + cy.contains('Cypress USB Detector') + .click() + .then(() => { + cy.contains('View Alerts') + .click() + .then(() => { + cy.hash() + .should('match', /alerts\/.+$/) + .then((hash) => { + const detectorId = hash.replace('#/alerts/', ''); + if (!detectorId) { + throw new Error('Navigating to alerts page should contain detector ID'); + } else { + cy.wait('@getAlerts').then((interception) => { + const url = new URL(interception.request.url); + // The request query param detectorId should match the hash param from the url + expect(url.searchParams.get('detector_id')).to.eq(detectorId); + }); + + // There should be only one call to the API + cy.get('@getAlerts.all').should('have.length', 1); + } + }); + }); + }); + }); + + after(() => cy.cleanUpTests()); +}); diff --git a/models/interfaces.ts b/models/interfaces.ts index b2b5d0aa0..b55303ad6 100644 --- a/models/interfaces.ts +++ b/models/interfaces.ts @@ -54,8 +54,6 @@ export interface AlertCondition { // Alert related fields actions: TriggerAction[]; severity: string; - - detection_types: string[]; } export interface TriggerAction { diff --git a/public/pages/Alerts/components/AlertFlyout/AlertFlyout.tsx b/public/pages/Alerts/components/AlertFlyout/AlertFlyout.tsx index e2e3eb9bb..d9d98b280 100644 --- a/public/pages/Alerts/components/AlertFlyout/AlertFlyout.tsx +++ b/public/pages/Alerts/components/AlertFlyout/AlertFlyout.tsx @@ -20,7 +20,7 @@ import { import { AlertItem, RuleSource } from '../../../../../server/models/interfaces'; import React from 'react'; import { ContentPanel } from '../../../../components/ContentPanel'; -import { ALERT_STATE, DEFAULT_EMPTY_DATA, ROUTES } from '../../../../utils/constants'; +import { ALERT_STATE, DEFAULT_EMPTY_DATA } from '../../../../utils/constants'; import { capitalizeFirstLetter, createTextDetailsGroup, @@ -145,7 +145,7 @@ export class AlertFlyout extends React.Component + render: (id, finding) => ( { @@ -157,10 +157,7 @@ export class AlertFlyout extends React.Component detectionType || DEFAULT_EMPTY_DATA, + field: 'queries', + name: 'Rule name', + sortable: true, + render: (queries: any[]) => rules[queries[0]?.id]?.title || DEFAULT_EMPTY_DATA, }, { field: 'detector_id', @@ -257,12 +255,12 @@ export class AlertFlyout extends React.Component diff --git a/public/pages/Correlations/containers/CorrelationRules.tsx b/public/pages/Correlations/containers/CorrelationRules.tsx index abf398740..bec190dd7 100644 --- a/public/pages/Correlations/containers/CorrelationRules.tsx +++ b/public/pages/Correlations/containers/CorrelationRules.tsx @@ -21,26 +21,21 @@ import { getCorrelationRulesTableColumns, getCorrelationRulesTableSearchConfig, } from '../utils/helpers'; -import { CorrelationRule, CorrelationRuleTableItem } from '../../../../types'; +import { CorrelationRule } from '../../../../types'; import { RouteComponentProps } from 'react-router-dom'; import { DeleteCorrelationRuleModal } from '../components/DeleteModal'; export const CorrelationRules: React.FC = (props: RouteComponentProps) => { const context = useContext(CoreServicesContext); - const [allRules, setAllRules] = useState([]); + const [allRules, setAllRules] = useState([]); + const [filteredRules, setFilteredRules] = useState([]); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const [selectedRule, setSelectedRule] = useState(undefined); const getCorrelationRules = useCallback(async () => { const allRuleItems: CorrelationRule[] = await DataStore.correlations.getCorrelationRules(); - const allRuleTableItems: CorrelationRuleTableItem[] = allRuleItems.map((item) => { - const logTypes = item.queries.map((q) => q.logType).join(', '); - return { - ...item, - logTypes, - }; - }); - setAllRules(allRuleTableItems); + setAllRules(allRuleItems); + setFilteredRules(allRuleItems); }, [DataStore.correlations.getCorrelationRules]); useEffect(() => { @@ -66,6 +61,22 @@ export const CorrelationRules: React.FC = (props: RouteComp [] ); + const onLogTypeFilterChange = useCallback( + (logTypes?: string[]) => { + if (!logTypes) { + setFilteredRules(allRules); + return; + } + + const logTypesSet = new Set(logTypes); + const filteredRules = allRules.filter((rule) => { + return rule.queries.some((query) => logTypesSet.has(query.logType)); + }); + setFilteredRules(filteredRules); + }, + [allRules] + ); + const onRuleNameClick = useCallback((rule: CorrelationRule) => { props.history.push({ pathname: `${ROUTES.CORRELATION_RULE_EDIT}/${rule.id}`, @@ -132,10 +143,10 @@ export const CorrelationRules: React.FC = (props: RouteComp setIsDeleteModalVisible(true); setSelectedRule(rule); })} - items={allRules} + items={filteredRules} pagination={true} sorting={true} - search={getCorrelationRulesTableSearchConfig()} + search={getCorrelationRulesTableSearchConfig(onLogTypeFilterChange)} /> ) : ( = ( label: ruleTypes.find( (logType) => - logType.value.toLowerCase() === query.logType.toLowerCase() + logType.label.toLowerCase() === query.logType.toLowerCase() )?.label || query.logType, }, ] diff --git a/public/pages/Correlations/utils/constants.tsx b/public/pages/Correlations/utils/constants.tsx index f54889285..8c140ee62 100644 --- a/public/pages/Correlations/utils/constants.tsx +++ b/public/pages/Correlations/utils/constants.tsx @@ -9,7 +9,7 @@ import { ruleSeverity, ruleTypes } from '../../Rules/utils/constants'; import { FilterItem } from '../components/FilterGroup'; import { EuiIcon, EuiTitle } from '@elastic/eui'; import { logTypeCategories, logTypesByCategories } from '../../../utils/constants'; -import { getLogTypeLabel } from '../../LogTypes/utils/helpers'; +import _ from 'lodash'; export const graphRenderOptions = { nodes: { @@ -67,7 +67,7 @@ export const getDefaultLogTypeFilterItemOptions: () => FilterItem[] = () => { logTypes.forEach(({ name }) => { options.push({ - name: getLogTypeLabel(name), + name: _.capitalize(name), id: name, checked: 'on', visible: true, diff --git a/public/pages/Correlations/utils/helpers.tsx b/public/pages/Correlations/utils/helpers.tsx index 7979673b5..4c1633f23 100644 --- a/public/pages/Correlations/utils/helpers.tsx +++ b/public/pages/Correlations/utils/helpers.tsx @@ -5,14 +5,20 @@ import React from 'react'; import { EuiBasicTableColumn, EuiBadge, EuiToolTip, EuiButtonIcon, EuiLink } from '@elastic/eui'; -import { CorrelationRule, CorrelationRuleQuery, CorrelationRuleTableItem } from '../../../../types'; +import { + ArgsWithError, + ArgsWithQuery, + CorrelationRule, + CorrelationRuleQuery, +} from '../../../../types'; import { Search } from '@opensearch-project/oui/src/eui_components/basic_table'; +import { FieldClause } from '@opensearch-project/oui/src/eui_components/search_bar/query/ast'; import { formatRuleType, getLogTypeFilterOptions } from '../../../utils/helpers'; export const getCorrelationRulesTableColumns = ( onRuleNameClick: (rule: CorrelationRule) => void, _refreshRules: (ruleItem: CorrelationRule) => void -): EuiBasicTableColumn[] => { +): EuiBasicTableColumn[] => { return [ { field: 'name', @@ -25,8 +31,7 @@ export const getCorrelationRulesTableColumns = ( }, { name: 'Log types', - field: 'logTypes', - render: (logTypes: string, ruleItem: CorrelationRule) => { + render: (ruleItem: CorrelationRule) => { const badges = [ ...new Set(ruleItem.queries?.map((query) => formatRuleType(query.logType))), ]; @@ -70,11 +75,33 @@ export const getCorrelationRulesTableColumns = ( ]; }; -export const getCorrelationRulesTableSearchConfig = (): Search => { +export const getCorrelationRulesTableSearchConfig = ( + onLogTypeFilterChange: (logTypes?: string[]) => void +): Search => { return { box: { placeholder: 'Search by rule name, log type', - schema: true, + }, + onChange: (args: ArgsWithQuery | ArgsWithError) => { + if (!args.error) { + const logTypeFieldClauseIdx = args.query.ast.clauses.findIndex( + (clause) => clause.type === 'field' && clause.field === 'logTypes' + ); + const logTypeFieldClause = + logTypeFieldClauseIdx > -1 ? args.query.ast.clauses[logTypeFieldClauseIdx] : undefined; + + if (logTypeFieldClause) { + const logTypes = (logTypeFieldClause as FieldClause).value as string[]; + // Need to remove the logTypes clause so that in built search doesn't try to apply it because that will return 0 results, + // since it requires custom logic we implemented in the `onLogTypeFilterChange` callback + args.query.ast.removeOrFieldClauses('logTypes'); + onLogTypeFilterChange(logTypes); + } else if (args.query.ast.clauses.length === 0) { + onLogTypeFilterChange(undefined); + } + } + + return true; }, filters: [ { diff --git a/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx b/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx index 9e8087586..9025bd3e9 100644 --- a/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx +++ b/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx @@ -8,7 +8,6 @@ import { RouteComponentProps } from 'react-router-dom'; import { EuiAccordion, EuiButton, - EuiCheckbox, EuiComboBox, EuiComboBoxOptionOption, EuiFieldText, @@ -16,10 +15,8 @@ import { EuiFlexItem, EuiFormRow, EuiSpacer, - EuiSwitch, EuiText, EuiTextArea, - EuiTitle, } from '@elastic/eui'; import { AlertCondition } from '../../../../../../../models/interfaces'; import { @@ -53,9 +50,6 @@ interface AlertConditionPanelState { nameIsInvalid: boolean; previewToggle: boolean; selectedNames: EuiComboBoxOptionOption[]; - showNotificationDetails: boolean; - detectionRulesTriggerEnabled: boolean; - threatIntelTriggerEnabled: boolean; } export default class AlertConditionPanel extends Component< @@ -69,9 +63,6 @@ export default class AlertConditionPanel extends Component< nameIsInvalid: false, previewToggle: false, selectedNames: [], - showNotificationDetails: true, - detectionRulesTriggerEnabled: props.alertCondition.detection_types.includes('rules'), - threatIntelTriggerEnabled: props.alertCondition.detection_types.includes('threat_intel'), }; } @@ -79,14 +70,6 @@ export default class AlertConditionPanel extends Component< this.prepareMessage(); } - onDetectionTypeChange(detectionType: 'rules' | 'threat_intel', enabled: boolean) { - const detectionTypes = new Set(this.props.alertCondition.detection_types); - enabled ? detectionTypes.add(detectionType) : detectionTypes.delete(detectionType); - this.updateTrigger({ - detection_types: Array.from(detectionTypes), - }); - } - prepareMessage = (updateMessage: boolean = false) => { const { alertCondition, detector } = this.props; const detectorInput = detector.inputs[0].detector_input; @@ -207,11 +190,7 @@ export default class AlertConditionPanel extends Component< } = this.props; const actions = alertCondition.actions; - if (selectedOptions.length > 0) { - actions[0].destination_id = selectedOptions[0].value!; - } else { - actions[0].destination_id = ''; - } + actions[0].destination_id = selectedOptions.length > 0 ? selectedOptions[0].value! : ''; triggers.splice(indexNum, 1, { ...alertCondition, @@ -274,21 +253,13 @@ export default class AlertConditionPanel extends Component< const { alertCondition = getEmptyAlertCondition(), allNotificationChannels, - detector: { threat_intel_enabled: threatIntelEnabledInDetector }, indexNum, loadingNotifications, refreshNotificationChannels, rulesOptions, hasNotificationPlugin, } = this.props; - const { - nameFieldTouched, - nameIsInvalid, - selectedNames, - showNotificationDetails, - detectionRulesTriggerEnabled, - threatIntelTriggerEnabled, - } = this.state; + const { nameFieldTouched, nameIsInvalid, selectedNames } = this.state; const { name, sev_levels: ruleSeverityLevels, tags, severity } = alertCondition; const uniqueTagsOptions = new Set( rulesOptions.map((option) => option.tags).reduce((prev, current) => prev.concat(current), []) @@ -344,302 +315,247 @@ export default class AlertConditionPanel extends Component< return (
- -

Trigger name

- + + Trigger details and condition + + {triggerDetailsSubheading} + +
} - isInvalid={nameFieldTouched && nameIsInvalid} - error={getNameErrorMessage(name, nameIsInvalid, nameFieldTouched)} > - - - + +

Trigger name

+ + } + isInvalid={nameFieldTouched && nameIsInvalid} + error={getNameErrorMessage(name, nameIsInvalid, nameFieldTouched)} + > + +
- -

Detection type

-
- - {threatIntelEnabledInDetector ? ( - { - this.setState({ detectionRulesTriggerEnabled: e.target.checked }); - this.onDetectionTypeChange('rules', e.target.checked); - }} - /> - ) : ( - -

Detection rules

+ + +

If a detection rule matches

- )} - - - - {detectionRulesTriggerEnabled && ( - <> - - Trigger condition - - {triggerDetailsSubheading} - - + + + +

Rule names

+
+ } + > + + + + + +

Rule Severities

+ + } + > + +
+ + + +

Tags

+ + } + > + +
+ + + + + +

Notification

+
+ + + + Notification + + {`Configure notification to receive alerts when the trigger condition is met.`} + + + } + > + +

Specify alert severity

+ + } + > + + onChange={this.onAlertSeverityChange} + singleSelection={{ asPlainText: true }} + isClearable={false} + data-test-subj={'security-levels-combo-box'} + /> +
+ + + + + -

Rule names

+ +

Select channel to notify

} > []} + selectedOptions={ + selectedNotificationChannelOption as EuiComboBoxOptionOption[] + } + onChange={this.onNotificationChannelsChange} + singleSelection={{ asPlainText: true }} + onBlur={refreshNotificationChannels} + isDisabled={!hasNotificationPlugin} />
- +
+ + + Manage channels + + +
+ + {!hasNotificationPlugin && ( + <> + + + + )} +
+ + + +

Notification message

+ + } + paddingSize={'l'} + initialIsOpen={false} + > + + -

Rule Severities

+ +

Message subject

} + fullWidth={true} > - this.onMessageSubjectChange(e.target.value)} + required={true} + fullWidth={true} />
- +
+ -

Tags

+

Message body

} + fullWidth={true} > - this.onMessageBodyChange(e.target.value)} + required={true} + fullWidth={true} />
-
- - - - )} - - {threatIntelEnabledInDetector && ( - <> - { - this.setState({ threatIntelTriggerEnabled: e.target.checked }); - this.onDetectionTypeChange('threat_intel', e.target.checked); - }} - /> + - {threatIntelTriggerEnabled && ( - <> - - -

- An alert will be generated when any match is found by the threat intelligence - feed. -

-
- - - )} - - )} - - - - {!detectionRulesTriggerEnabled && !threatIntelTriggerEnabled && ( - <> - -

Select detection type for the trigger

-
- - - )} - - - this.setState({ showNotificationDetails: e.target.checked })} - /> - - - - {showNotificationDetails && ( - <> - -

Alert severity

- - } - > - -
- - - - - - -

Notification channel

- - } - > - []} - selectedOptions={ - selectedNotificationChannelOption as EuiComboBoxOptionOption[] - } - onChange={this.onNotificationChannelsChange} - singleSelection={{ asPlainText: true }} - onFocus={refreshNotificationChannels} - isDisabled={!hasNotificationPlugin} - /> -
-
- - - Manage channels + + + this.prepareMessage(true)}> + Generate message - -
- - {!hasNotificationPlugin && ( - <> - - - - )} - - - - -

Notification message

- - } - paddingSize={'l'} - initialIsOpen={false} - > - - - -

Message subject

- - } - fullWidth={true} - > - this.onMessageSubjectChange(e.target.value)} - required={true} - fullWidth={true} - /> -
-
- - - -

Message body

- - } - fullWidth={true} - > - this.onMessageBodyChange(e.target.value)} - required={true} - fullWidth={true} - /> -
-
- - - - this.prepareMessage(true)}> - Generate message - - - -
-
- - - - )} + + + + + + ); } diff --git a/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/__snapshots__/AlertConditionPanel.test.tsx.snap b/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/__snapshots__/AlertConditionPanel.test.tsx.snap index 03b992412..c0a13a9ee 100644 --- a/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/__snapshots__/AlertConditionPanel.test.tsx.snap +++ b/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/__snapshots__/AlertConditionPanel.test.tsx.snap @@ -6,66 +6,6 @@ Object { "baseElement":
-
-
- -
-
-
-
- -
-
-
-
-
-

- Detection type -

-
-

- Detection rules -

-
-
@@ -93,7 +33,7 @@ Object {
- Trigger condition + Trigger details and condition
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+

+ If a detection rule matches +

+
+
-
-
-
-
- - - Send notification - -
-
-
-
- -
-
- -
-
-
-
- -
-
- -
-
- +

+ Notification +

@@ -701,7 +480,7 @@ Object {
@@ -710,22 +489,266 @@ Object { class="euiAccordion__padding--l" >
-
-
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+