diff --git a/packages/kbn-cell-actions/src/components/cell_action_item.tsx b/packages/kbn-cell-actions/src/components/cell_action_item.tsx index b002afb35d83f..8b1419b20531b 100644 --- a/packages/kbn-cell-actions/src/components/cell_action_item.tsx +++ b/packages/kbn-cell-actions/src/components/cell_action_item.tsx @@ -15,19 +15,24 @@ export const ActionItem = ({ action, actionContext, showTooltip, + onClick, }: { action: CellAction; actionContext: CellActionExecutionContext; showTooltip: boolean; + onClick?: () => void; }) => { const actionProps = useMemo( () => ({ iconType: action.getIconType(actionContext) as IconType, - onClick: () => action.execute(actionContext), + onClick: () => { + action.execute(actionContext); + if (onClick) onClick(); + }, 'data-test-subj': `actionItem-${action.id}`, 'aria-label': action.getDisplayName(actionContext), }), - [action, actionContext] + [action, actionContext, onClick] ); if (!actionProps.iconType) return null; diff --git a/packages/kbn-cell-actions/src/components/hover_actions_popover.tsx b/packages/kbn-cell-actions/src/components/hover_actions_popover.tsx index dd087aa755307..0487c27b06e52 100644 --- a/packages/kbn-cell-actions/src/components/hover_actions_popover.tsx +++ b/packages/kbn-cell-actions/src/components/hover_actions_popover.tsx @@ -149,6 +149,7 @@ export const HoverActionsPopover: React.FC = ({ action={action} actionContext={actionContext} showTooltip={showActionTooltips} + onClick={closePopover} /> ))} {extraActions.length > 0 && ( diff --git a/x-pack/plugins/security_solution/cypress/e2e/dashboards/detection_response.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/dashboards/detection_response.cy.ts new file mode 100644 index 0000000000000..9a8dc763de24d --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/e2e/dashboards/detection_response.cy.ts @@ -0,0 +1,262 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { getNewRule } from '../../objects/rule'; +import { ALERTS_COUNT } from '../../screens/alerts'; +import { + CONTROL_FRAMES, + OPTION_LIST_LABELS, + OPTION_LIST_VALUES, +} from '../../screens/common/filter_group'; + +import { + HOST_TABLE_HOST_NAME_BTN, + HOST_TABLE_ROW_SEV, + HOST_TABLE_ROW_TOTAL_ALERTS, + RULE_TABLE_ROW_RULE_NAME_BTN, + RULE_TABLE_ROW_TOTAL_ALERTS, + RULE_TABLE_VIEW_ALL_OPEN_ALERTS_BTN, + USER_TABLE_ROW_SEV, + USER_TABLE_ROW_TOTAL_ALERTS, + USER_TABLE_USER_NAME_BTN, +} from '../../screens/detection_response'; +import { DETECTION_RESPONSE } from '../../screens/security_header'; +import { QUERY_TAB_BUTTON, TIMELINE_DATA_PROVIDERS_CONTAINER } from '../../screens/timeline'; +import { waitForAlerts } from '../../tasks/alerts'; +import { createRule } from '../../tasks/api_calls/rules'; +import { cleanKibana } from '../../tasks/common'; +import { investigateDashboardItemInTimeline } from '../../tasks/dashboards/common'; +import { waitToNavigateAwayFrom } from '../../tasks/kibana_navigation'; +import { login, visit } from '../../tasks/login'; +import { navigateFromHeaderTo } from '../../tasks/security_header'; +import { closeTimeline } from '../../tasks/timeline'; +import { ALERTS_URL, DASHBOARDS_URL, DETECTIONS_RESPONSE_URL } from '../../urls/navigation'; + +describe('Detection response view', () => { + before(() => { + cleanKibana(); + login(); + createRule(getNewRule()); + visit(DETECTIONS_RESPONSE_URL); + }); + context('Open in timeline', { testIsolation: false }, () => { + afterEach(() => { + closeTimeline(); + }); + + it(`opens timeline with correct query count for hosts by alert severity table`, () => { + cy.get(HOST_TABLE_ROW_TOTAL_ALERTS) + .first() + .then((sub) => { + const alertCount = sub.text(); + cy.get(HOST_TABLE_HOST_NAME_BTN) + .first() + .then((hostNameEl) => { + const hostName = hostNameEl.text(); + investigateDashboardItemInTimeline(HOST_TABLE_ROW_TOTAL_ALERTS); + cy.get(QUERY_TAB_BUTTON).should('be.visible').should('contain.text', alertCount); + cy.get(TIMELINE_DATA_PROVIDERS_CONTAINER) + .should('be.visible') + .should( + 'contain.text', + `host.name: "${hostName}"ANDkibana.alert.workflow_status: "open"` + ); + }); + }); + }); + it(`opens timeline with correct query count for users by alert severity table`, () => { + cy.get(USER_TABLE_ROW_TOTAL_ALERTS) + .first() + .then((sub) => { + const alertCount = sub.text(); + cy.get(USER_TABLE_USER_NAME_BTN) + .first() + .then((userNameEl) => { + const userName = userNameEl.text(); + investigateDashboardItemInTimeline(USER_TABLE_ROW_TOTAL_ALERTS); + cy.get(QUERY_TAB_BUTTON).should('contain.text', alertCount); + cy.get(TIMELINE_DATA_PROVIDERS_CONTAINER) + .should('be.visible') + .should( + 'contain.text', + `user.name: "${userName}"ANDkibana.alert.workflow_status: "open"` + ); + }); + }); + }); + it(`opens timeline with correct query count for open alerts by rule table`, () => { + cy.get(RULE_TABLE_ROW_TOTAL_ALERTS) + .first() + .then((sub) => { + const alertCount = sub.text(); + cy.get(RULE_TABLE_ROW_RULE_NAME_BTN) + .first() + .then((ruleNameEl) => { + const ruleName = ruleNameEl.text(); + investigateDashboardItemInTimeline(RULE_TABLE_ROW_TOTAL_ALERTS); + cy.get(QUERY_TAB_BUTTON).should('contain.text', alertCount); + cy.get(TIMELINE_DATA_PROVIDERS_CONTAINER) + .should('be.visible') + .should( + 'contain.text', + `kibana.alert.rule.name: "${ruleName}"ANDkibana.alert.workflow_status: "open"` + ); + }); + }); + }); + }); + + context('Redirection to AlertPage', { testIsolation: false }, () => { + afterEach(() => { + navigateFromHeaderTo(DETECTION_RESPONSE); + }); + + it('should redirect to alert page with host and status as the filters', () => { + cy.get(HOST_TABLE_ROW_TOTAL_ALERTS) + .first() + .should('be.visible') + .then((sub) => { + const alertCount = sub.text(); + cy.get(HOST_TABLE_HOST_NAME_BTN) + .first() + .should('be.visible') + .then((hostNameEl) => { + const hostName = hostNameEl.text(); + sub.trigger('click'); + waitToNavigateAwayFrom(DASHBOARDS_URL); + cy.url().should((urlString) => { + const url = new URL(urlString); + expect(url.pathname.endsWith(ALERTS_URL)).eq(true); + }); + waitForAlerts(); + cy.get(ALERTS_COUNT).should('be.visible').should('have.text', `${alertCount} alerts`); + cy.get(CONTROL_FRAMES).should('have.length', 2); + cy.get(OPTION_LIST_LABELS).eq(0).should('have.text', `Status`); + cy.get(OPTION_LIST_VALUES(0)).should('have.text', 'open1'); + cy.get(OPTION_LIST_LABELS).eq(1).should('have.text', `Host name`); + cy.get(OPTION_LIST_VALUES(1)).should('have.text', `${hostName}1`); + }); + }); + }); + + it('should redirect to alert page with host, status and severity as the filters', () => { + const severityVal = 'high'; + cy.get(HOST_TABLE_ROW_SEV(severityVal)) + .first() + .should('be.visible') + .then((sub) => { + const alertCount = sub.text(); + cy.get(HOST_TABLE_HOST_NAME_BTN) + .first() + .should('be.visible') + .then((hostNameEl) => { + cy.get(HOST_TABLE_ROW_SEV(severityVal)).first().trigger('click'); + waitToNavigateAwayFrom(DASHBOARDS_URL); + const hostName = hostNameEl.text(); + waitForAlerts(); + cy.get(ALERTS_COUNT).should('be.visible').should('have.text', `${alertCount} alerts`); + cy.get(CONTROL_FRAMES).should('have.length', 3); + cy.get(OPTION_LIST_LABELS).eq(0).should('have.text', `Status`); + cy.get(OPTION_LIST_VALUES(0)).should('have.text', 'open1'); + cy.get(OPTION_LIST_LABELS).eq(1).should('have.text', 'Host name'); + cy.get(OPTION_LIST_VALUES(1)).should('have.text', `${hostName}1`); + cy.get(OPTION_LIST_LABELS).eq(2).should('have.text', 'Severity'); + cy.get(OPTION_LIST_VALUES(2)).should('have.text', `${severityVal}1`); + }); + }); + }); + it('should redirect to alert page with user and status as the filters', () => { + cy.get(USER_TABLE_ROW_TOTAL_ALERTS) + .first() + .should('be.visible') + .then((sub) => { + const alertCount = sub.text(); + cy.get(USER_TABLE_USER_NAME_BTN) + .first() + .should('be.visible') + .then((userNameEl) => { + const userName = userNameEl.text(); + sub.trigger('click'); + waitToNavigateAwayFrom(DASHBOARDS_URL); + cy.url().should((urlString) => { + const url = new URL(urlString); + expect(url.pathname.endsWith(ALERTS_URL)).eq(true); + }); + waitForAlerts(); + cy.get(ALERTS_COUNT).should('be.visible').should('have.text', `${alertCount} alerts`); + cy.get(CONTROL_FRAMES).should('have.length', 2); + cy.get(OPTION_LIST_LABELS).eq(0).should('have.text', `Status`); + cy.get(OPTION_LIST_VALUES(0)).should('have.text', 'open1'); + cy.get(OPTION_LIST_LABELS).eq(1).should('have.text', `Username`); + cy.get(OPTION_LIST_VALUES(1)).should('have.text', `${userName}1`); + }); + }); + }); + + it('should redirect to alert page with user, status and severity as the filters', () => { + const severityVal = 'high'; + cy.get(USER_TABLE_ROW_SEV(severityVal)) + .first() + .should('be.visible') + .then((sub) => { + const alertCount = sub.text(); + cy.get(USER_TABLE_USER_NAME_BTN) + .first() + .should('be.visible') + .then((userNameEl) => { + const userName = userNameEl.text(); + cy.get(USER_TABLE_ROW_SEV(severityVal)).trigger('click'); + waitToNavigateAwayFrom(DASHBOARDS_URL); + waitForAlerts(); + cy.get(ALERTS_COUNT).should('be.visible').should('have.text', `${alertCount} alerts`); + cy.get(CONTROL_FRAMES).should('have.length', 3); + cy.get(OPTION_LIST_LABELS).eq(0).should('have.text', `Status`); + cy.get(OPTION_LIST_VALUES(0)).should('have.text', 'open1'); + cy.get(OPTION_LIST_LABELS).eq(1).should('have.text', 'Username'); + cy.get(OPTION_LIST_VALUES(1)).should('have.text', `${userName}1`); + cy.get(OPTION_LIST_LABELS).eq(2).should('have.text', 'Severity'); + cy.get(OPTION_LIST_VALUES(2)).should('have.text', `${severityVal}1`); + }); + }); + }); + it('should redirect to alert page with rule name & status as filters', () => { + cy.get(RULE_TABLE_ROW_TOTAL_ALERTS) + .first() + .should('be.visible') + .then((sub) => { + const alertCount = sub.text(); + cy.get(RULE_TABLE_ROW_RULE_NAME_BTN) + .first() + .should('be.visible') + .then((ruleNameEl) => { + sub.trigger('click'); + waitToNavigateAwayFrom(DASHBOARDS_URL); + const ruleName = ruleNameEl.text(); + waitForAlerts(); + cy.get(ALERTS_COUNT).should('be.visible').should('have.text', `${alertCount} alerts`); + cy.get(CONTROL_FRAMES).should('have.length', 2); + cy.get(OPTION_LIST_LABELS).eq(0).should('have.text', `Status`); + cy.get(OPTION_LIST_VALUES(0)).should('have.text', 'open1'); + cy.get(OPTION_LIST_LABELS).eq(1).should('have.text', 'Rule name'); + cy.get(OPTION_LIST_VALUES(1)).should('have.text', `${ruleName}1`); + }); + }); + }); + it('should redirect to "View Open Alerts" correctly', () => { + cy.get(RULE_TABLE_VIEW_ALL_OPEN_ALERTS_BTN) + .first() + .should('be.visible') + .then((sub) => { + sub.trigger('click'); + waitToNavigateAwayFrom(DASHBOARDS_URL); + waitForAlerts(); + cy.get(CONTROL_FRAMES).should('have.length', 1); + cy.get(OPTION_LIST_LABELS).eq(0).should('have.text', `Status`); + cy.get(OPTION_LIST_VALUES(0)).should('have.text', 'open1'); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_response/open_alerts_in_timeline.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_response/open_alerts_in_timeline.cy.ts deleted file mode 100644 index b52b2d12d5181..0000000000000 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_response/open_alerts_in_timeline.cy.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { getNewRule } from '../../objects/rule'; - -import { - HOST_TABLE_ROW_TOTAL_ALERTS, - RULE_TABLE_ROW_TOTAL_ALERTS, - USER_TABLE_ROW_TOTAL_ALERTS, -} from '../../screens/detection_response'; -import { QUERY_TAB_BUTTON } from '../../screens/timeline'; -import { createRule } from '../../tasks/api_calls/rules'; -import { cleanKibana } from '../../tasks/common'; -import { login, visit } from '../../tasks/login'; -import { closeTimeline } from '../../tasks/timeline'; -import { DETECTIONS_RESPONSE_URL } from '../../urls/navigation'; - -const ALERT_COUNT = 1; - -describe.skip('Detection response view', () => { - context('Open in timeline', () => { - before(() => { - cleanKibana(); - login(); - createRule(getNewRule()); - visit(DETECTIONS_RESPONSE_URL); - }); - - afterEach(() => { - closeTimeline(); - }); - - it(`opens timeline with correct query count for hosts by alert severity table`, () => { - cy.get(HOST_TABLE_ROW_TOTAL_ALERTS).click(); - cy.get(QUERY_TAB_BUTTON).should('contain.text', ALERT_COUNT); - }); - it(`opens timeline with correct query count for users by alert severity table`, () => { - cy.get(USER_TABLE_ROW_TOTAL_ALERTS).click(); - cy.get(QUERY_TAB_BUTTON).should('contain.text', ALERT_COUNT); - }); - it(`opens timeline with correct query count for open alerts by rule table`, () => { - cy.get(RULE_TABLE_ROW_TOTAL_ALERTS).click(); - cy.get(QUERY_TAB_BUTTON).should('contain.text', ALERT_COUNT); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/cypress/screens/dashboards/common.ts b/x-pack/plugins/security_solution/cypress/screens/dashboards/common.ts new file mode 100644 index 0000000000000..451b6db5f7dac --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/screens/dashboards/common.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getDataTestSubjectSelector } from '../../helpers/common'; + +export const DASHBOARD_INVESTIGATE_IN_TIMELINE_CELL_ACTION = getDataTestSubjectSelector( + 'actionItem-security-alertsCount-cellActions-investigateInNewTimeline' +); diff --git a/x-pack/plugins/security_solution/cypress/screens/detection_response.ts b/x-pack/plugins/security_solution/cypress/screens/detection_response.ts index 5227e91b1bba1..6c9b0dbe265c0 100644 --- a/x-pack/plugins/security_solution/cypress/screens/detection_response.ts +++ b/x-pack/plugins/security_solution/cypress/screens/detection_response.ts @@ -5,11 +5,37 @@ * 2.0. */ +import { getDataTestSubjectSelector } from '../helpers/common'; + export const HOST_TABLE_ROW_TOTAL_ALERTS = '[data-test-subj="hostSeverityAlertsTable-totalAlerts"] button'; +export const HOST_TABLE_HOST_NAME_BTN = getDataTestSubjectSelector('host-details-button'); + +export const HOST_TABLE_ROW_SEV = (sev: string) => ` +${getDataTestSubjectSelector( + `hostSeverityAlertsTable-${sev.toLowerCase()}` +)} ${getDataTestSubjectSelector(`cellActions-renderContent-host.name`)} +`; + export const USER_TABLE_ROW_TOTAL_ALERTS = '[data-test-subj="userSeverityAlertsTable-totalAlerts"] button'; +export const USER_TABLE_USER_NAME_BTN = getDataTestSubjectSelector('users-link-anchor'); + +export const USER_TABLE_ROW_SEV = (sev: string) => ` +${getDataTestSubjectSelector( + `userSeverityAlertsTable-${sev.toLowerCase()}` +)} ${getDataTestSubjectSelector(`cellActions-renderContent-user.name`)} +`; + export const RULE_TABLE_ROW_TOTAL_ALERTS = '[data-test-subj="severityRuleAlertsTable-alertCount"] button'; + +export const RULE_TABLE_VIEW_ALL_OPEN_ALERTS_BTN = getDataTestSubjectSelector( + 'severityRuleAlertsButton' +); + +export const RULE_TABLE_ROW_RULE_NAME_BTN = getDataTestSubjectSelector( + 'severityRuleAlertsTable-name' +); diff --git a/x-pack/plugins/security_solution/cypress/tasks/dashboards/common.ts b/x-pack/plugins/security_solution/cypress/tasks/dashboards/common.ts new file mode 100644 index 0000000000000..a09e0cb76b2ca --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/tasks/dashboards/common.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DASHBOARD_INVESTIGATE_IN_TIMELINE_CELL_ACTION } from '../../screens/dashboards/common'; + +export const investigateDashboardItemInTimeline = (selector: string, itemIndex: number = 0) => { + cy.get(selector).eq(itemIndex).trigger('mouseover'); + cy.get(DASHBOARD_INVESTIGATE_IN_TIMELINE_CELL_ACTION).should('be.visible').trigger('click'); +}; diff --git a/x-pack/plugins/security_solution/cypress/tasks/kibana_navigation.ts b/x-pack/plugins/security_solution/cypress/tasks/kibana_navigation.ts index 3b3fc0c6da4e4..cdb88a030499e 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/kibana_navigation.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/kibana_navigation.ts @@ -14,3 +14,25 @@ export const navigateFromKibanaCollapsibleTo = (page: string) => { export const openKibanaNavigation = () => { cy.get(KIBANA_NAVIGATION_TOGGLE).click(); }; + +/** + * + * @param pathname Path from which you are navigating away + * + * @description + * Function waits until given pathname is no longer available + * + * */ +export const waitToNavigateAwayFrom = (pathName: string) => { + cy.waitUntil( + () => + cy.url().then((urlString) => { + const url = new URL(urlString); + return url.pathname !== pathName; + }), + { + timeout: 2000, + interval: 300, + } + ); +}; diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.test.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.test.tsx index 9750b8d34b9de..6e15e195a52a4 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.test.tsx @@ -8,7 +8,7 @@ import moment from 'moment'; import React from 'react'; -import { fireEvent, render } from '@testing-library/react'; +import { fireEvent, render, waitFor } from '@testing-library/react'; import { SecurityPageName } from '../../../../../common/constants'; import { TestProviders } from '../../../../common/mock'; @@ -169,4 +169,25 @@ describe('RuleAlertsTable', () => { title: 'Rule name', }); }); + + it('should render `View all open alerts` button which opens alert page with only status filter', async () => { + mockUseRuleAlertsItemsReturn({ items }); + const { getByTestId } = render( + + + + ); + + expect(getByTestId('severityRuleAlertsButton')).toBeInTheDocument(); + + fireEvent.click(getByTestId('severityRuleAlertsButton')); + + await waitFor(() => { + expect(mockNavigateToAlertsPageWithFilters).toHaveBeenCalledWith({ + fieldName: 'kibana.alert.workflow_status', + title: 'Status', + selectedOptions: ['open'], + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.tsx index 6c60bfc727b46..38f7836e26a42 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.tsx @@ -20,7 +20,7 @@ import { } from '@elastic/eui'; import { FormattedRelative } from '@kbn/i18n-react'; import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; -import { ALERT_RULE_NAME } from '@kbn/rule-data-utils'; +import { ALERT_RULE_NAME, ALERT_WORKFLOW_STATUS } from '@kbn/rule-data-utils'; import { CellActionsMode } from '@kbn/cell-actions'; import { SecurityCellActionsTrigger } from '../../../../actions/constants'; import { useNavigateToAlertsPageWithFilters } from '../../../../common/hooks/use_navigate_to_alerts_page_with_filters'; @@ -153,8 +153,12 @@ export const RuleAlertsTable = React.memo(({ signalIndexNa ); const navigateToAlerts = useCallback(() => { - navigateTo({ deepLinkId: SecurityPageName.alerts }); - }, [navigateTo]); + openAlertsPageWithFilter({ + title: i18n.OPEN_IN_ALERTS_TITLE_STATUS, + selectedOptions: ['open'], + fieldName: ALERT_WORKFLOW_STATUS, + }); + }, [openAlertsPageWithFilter]); const columns = useMemo( () => getTableColumns({ getAppUrl, navigateTo, openRuleInAlertsPage }),