From 225e8982204471eaed7606ada243699c785b8f60 Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Tue, 12 Oct 2021 09:40:54 -0500 Subject: [PATCH] [Security Solution][Detection Alerts] Fixes follow-up alert refresh bugs (#112169) --- .../detection_alerts/acknowledged.spec.ts | 11 +- .../detection_alerts/closing.spec.ts | 69 +- .../detection_alerts/opening.spec.ts | 15 +- .../cypress/screens/alerts.ts | 6 + .../timeline_actions/alert_context_menu.tsx | 7 +- .../components/take_action_dropdown/index.tsx | 43 +- .../__snapshots__/index.test.tsx.snap | 788 +++++++++--------- .../side_panel/event_details/footer.tsx | 46 +- .../public/container/use_update_alerts.ts | 8 +- .../hooks/use_status_bulk_action_items.tsx | 2 +- 10 files changed, 549 insertions(+), 446 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/acknowledged.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/acknowledged.spec.ts index 5d72105178b69..2dad11ac7e937 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/acknowledged.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/acknowledged.spec.ts @@ -6,7 +6,11 @@ */ import { getNewRule } from '../../objects/rule'; -import { ALERTS_COUNT, TAKE_ACTION_POPOVER_BTN } from '../../screens/alerts'; +import { + ALERTS_COUNT, + TAKE_ACTION_POPOVER_BTN, + ALERT_COUNT_TABLE_FIRST_ROW_COUNT, +} from '../../screens/alerts'; import { selectNumberOfAlerts, @@ -50,11 +54,16 @@ describe('Marking alerts as acknowledged', () => { markAcknowledgedFirstAlert(); const expectedNumberOfAlerts = +numberOfAlerts - numberOfAlertsToBeMarkedAcknowledged; cy.get(ALERTS_COUNT).should('have.text', `${expectedNumberOfAlerts} alerts`); + cy.get(ALERT_COUNT_TABLE_FIRST_ROW_COUNT).should('have.text', `${expectedNumberOfAlerts}`); goToAcknowledgedAlerts(); waitForAlerts(); cy.get(ALERTS_COUNT).should('have.text', `${numberOfAlertsToBeMarkedAcknowledged} alert`); + cy.get(ALERT_COUNT_TABLE_FIRST_ROW_COUNT).should( + 'have.text', + `${numberOfAlertsToBeMarkedAcknowledged}` + ); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts index 602619b056244..860a4e6089a27 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts @@ -6,7 +6,13 @@ */ import { getNewRule } from '../../objects/rule'; -import { ALERTS_COUNT, SELECTED_ALERTS, TAKE_ACTION_POPOVER_BTN } from '../../screens/alerts'; +import { + ALERTS_COUNT, + SELECTED_ALERTS, + TAKE_ACTION_POPOVER_BTN, + ALERT_COUNT_TABLE_FIRST_ROW_COUNT, + ALERTS_TREND_SIGNAL_RULE_NAME_PANEL, +} from '../../screens/alerts'; import { closeFirstAlert, @@ -46,6 +52,7 @@ describe('Closing alerts', () => { .then((alertNumberString) => { const numberOfAlerts = alertNumberString.split(' ')[0]; cy.get(ALERTS_COUNT).should('have.text', `${numberOfAlerts} alerts`); + cy.get(ALERT_COUNT_TABLE_FIRST_ROW_COUNT).should('have.text', `${numberOfAlerts}`); selectNumberOfAlerts(numberOfAlertsToBeClosed); @@ -56,6 +63,10 @@ describe('Closing alerts', () => { const expectedNumberOfAlertsAfterClosing = +numberOfAlerts - numberOfAlertsToBeClosed; cy.get(ALERTS_COUNT).should('have.text', `${expectedNumberOfAlertsAfterClosing} alerts`); + cy.get(ALERT_COUNT_TABLE_FIRST_ROW_COUNT).should( + 'have.text', + `${expectedNumberOfAlertsAfterClosing}` + ); goToClosedAlerts(); waitForAlerts(); @@ -75,6 +86,10 @@ describe('Closing alerts', () => { 'have.text', `${expectedNumberOfClosedAlertsAfterOpened} alerts` ); + cy.get(ALERT_COUNT_TABLE_FIRST_ROW_COUNT).should( + 'have.text', + `${expectedNumberOfClosedAlertsAfterOpened}` + ); goToOpenedAlerts(); waitForAlerts(); @@ -83,6 +98,10 @@ describe('Closing alerts', () => { +numberOfAlerts - expectedNumberOfClosedAlertsAfterOpened; cy.get(ALERTS_COUNT).should('have.text', `${expectedNumberOfOpenedAlerts} alerts`); + cy.get(ALERT_COUNT_TABLE_FIRST_ROW_COUNT).should( + 'have.text', + `${expectedNumberOfOpenedAlerts}` + ); }); }); @@ -103,11 +122,59 @@ describe('Closing alerts', () => { const expectedNumberOfAlerts = +numberOfAlerts - numberOfAlertsToBeClosed; cy.get(ALERTS_COUNT).should('have.text', `${expectedNumberOfAlerts} alerts`); + cy.get(ALERT_COUNT_TABLE_FIRST_ROW_COUNT).should('have.text', `${expectedNumberOfAlerts}`); + + goToClosedAlerts(); + waitForAlerts(); + + cy.get(ALERTS_COUNT).should('have.text', `${numberOfAlertsToBeClosed} alert`); + cy.get(ALERT_COUNT_TABLE_FIRST_ROW_COUNT).should( + 'have.text', + `${numberOfAlertsToBeClosed}` + ); + }); + }); + + it('Updates trend histogram whenever alert status is updated in table', () => { + const numberOfAlertsToBeClosed = 1; + cy.get(ALERTS_COUNT) + .invoke('text') + .then((alertNumberString) => { + const numberOfAlerts = alertNumberString.split(' ')[0]; + cy.get(ALERTS_COUNT).should('have.text', `${numberOfAlerts} alerts`); + cy.get(ALERT_COUNT_TABLE_FIRST_ROW_COUNT).should('have.text', `${numberOfAlerts}`); + + selectNumberOfAlerts(numberOfAlertsToBeClosed); + + cy.get(SELECTED_ALERTS).should('have.text', `Selected ${numberOfAlertsToBeClosed} alert`); + + closeAlerts(); + waitForAlerts(); + + const expectedNumberOfAlertsAfterClosing = +numberOfAlerts - numberOfAlertsToBeClosed; + cy.get(ALERTS_COUNT).should('have.text', `${expectedNumberOfAlertsAfterClosing} alerts`); + cy.get(ALERT_COUNT_TABLE_FIRST_ROW_COUNT).should( + 'have.text', + `${expectedNumberOfAlertsAfterClosing}` + ); goToClosedAlerts(); waitForAlerts(); cy.get(ALERTS_COUNT).should('have.text', `${numberOfAlertsToBeClosed} alert`); + + const numberOfAlertsToBeOpened = 1; + selectNumberOfAlerts(numberOfAlertsToBeOpened); + + cy.get(SELECTED_ALERTS).should('have.text', `Selected ${numberOfAlertsToBeOpened} alert`); + cy.get(ALERTS_TREND_SIGNAL_RULE_NAME_PANEL).should('exist'); + + openAlerts(); + waitForAlerts(); + + cy.get(ALERTS_COUNT).should('not.exist'); + cy.get(ALERT_COUNT_TABLE_FIRST_ROW_COUNT).should('not.exist'); + cy.get(ALERTS_TREND_SIGNAL_RULE_NAME_PANEL).should('not.exist'); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/opening.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/opening.spec.ts index 645abfed8ac0e..87cef27b5b346 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/opening.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/opening.spec.ts @@ -6,7 +6,12 @@ */ import { getNewRule } from '../../objects/rule'; -import { ALERTS_COUNT, SELECTED_ALERTS, TAKE_ACTION_POPOVER_BTN } from '../../screens/alerts'; +import { + ALERTS_COUNT, + SELECTED_ALERTS, + TAKE_ACTION_POPOVER_BTN, + ALERT_COUNT_TABLE_FIRST_ROW_COUNT, +} from '../../screens/alerts'; import { closeAlerts, @@ -74,6 +79,10 @@ describe('Opening alerts', () => { const expectedNumberOfAlerts = +numberOfAlerts - numberOfAlertsToBeOpened; cy.get(ALERTS_COUNT).should('have.text', `${expectedNumberOfAlerts} alerts`); + cy.get(ALERT_COUNT_TABLE_FIRST_ROW_COUNT).should( + 'have.text', + `${expectedNumberOfAlerts}` + ); goToOpenedAlerts(); waitForAlerts(); @@ -82,6 +91,10 @@ describe('Opening alerts', () => { 'have.text', `${numberOfOpenedAlerts + numberOfAlertsToBeOpened} alerts`.toString() ); + cy.get(ALERT_COUNT_TABLE_FIRST_ROW_COUNT).should( + 'have.text', + `${numberOfOpenedAlerts + numberOfAlertsToBeOpened}` + ); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts.ts b/x-pack/plugins/security_solution/cypress/screens/alerts.ts index 675a25641a2bd..d18a8e1ba10ab 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts.ts @@ -70,3 +70,9 @@ export const TAKE_ACTION_POPOVER_BTN = '[data-test-subj="selectedShowBulkActions export const TIMELINE_CONTEXT_MENU_BTN = '[data-test-subj="timeline-context-menu-button"]'; export const ATTACH_ALERT_TO_CASE_BUTTON = '[data-test-subj="attach-alert-to-case-button"]'; + +export const ALERT_COUNT_TABLE_FIRST_ROW_COUNT = + '[data-test-subj="alertsCountTable"] tr:nth-child(1) td:nth-child(2) .euiTableCellContent__text'; + +export const ALERTS_TREND_SIGNAL_RULE_NAME_PANEL = + '[data-test-subj="render-content-signal.rule.name"]'; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index fc8dd4b024fd9..06d61b3f0b284 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -12,6 +12,7 @@ import { indexOf } from 'lodash'; import { connect, ConnectedProps } from 'react-redux'; import { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { get } from 'lodash/fp'; +import { useRouteSpy } from '../../../../common/utils/route/use_route_spy'; import { buildGetAlertByIdQuery } from '../../../../common/components/exceptions/helpers'; import { EventsTdContent } from '../../../../timelines/components/timeline/styles'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../../../../timelines/components/timeline/helpers'; @@ -63,6 +64,7 @@ const AlertContextMenuComponent: React.FC { const [isPopoverOpen, setPopover] = useState(false); + const [routeProps] = useRouteSpy(); const afterItemSelection = useCallback(() => { setPopover(false); @@ -112,10 +114,13 @@ const AlertContextMenuComponent: React.FC { if (timelineId === TimelineId.active) { refetchQuery([timelineQuery]); + if (routeProps.pageName === 'alerts') { + refetchQuery(globalQuery); + } } else { refetchQuery(globalQuery); } - }, [timelineId, globalQuery, timelineQuery]); + }, [timelineId, globalQuery, timelineQuery, routeProps]); const { exceptionModalType, diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx index 200b21bbecc4b..f7d65d1a3f3f4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx @@ -9,8 +9,7 @@ import React, { useState, useCallback, useMemo } from 'react'; import { EuiContextMenuPanel, EuiButton, EuiPopover } from '@elastic/eui'; import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { isEmpty } from 'lodash/fp'; -import { connect, ConnectedProps } from 'react-redux'; -import { TimelineEventsDetailsItem, TimelineId } from '../../../../common'; +import { TimelineEventsDetailsItem } from '../../../../common'; import { TAKE_ACTION } from '../alerts_table/alerts_utility_bar/translations'; import { useExceptionActions } from '../alerts_table/timeline_actions/use_add_exception_actions'; import { useAlertsActions } from '../alerts_table/timeline_actions/use_alerts_actions'; @@ -24,8 +23,6 @@ import { Status } from '../../../../common/detection_engine/schemas/common/schem import { isAlertFromEndpointAlert } from '../../../common/utils/endpoint_alert_check'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { useAddToCaseActions } from '../alerts_table/timeline_actions/use_add_to_case_actions'; -import { inputsModel, inputsSelectors, State } from '../../../common/store'; - interface ActionsData { alertStatus: Status; eventId: string; @@ -48,7 +45,7 @@ export interface TakeActionDropdownProps { timelineId: string; } -export const TakeActionDropdownComponent = React.memo( +export const TakeActionDropdown = React.memo( ({ detailsData, ecsData, @@ -61,9 +58,7 @@ export const TakeActionDropdownComponent = React.memo( onAddIsolationStatusClick, refetch, timelineId, - globalQuery, - timelineQuery, - }: TakeActionDropdownProps & PropsFromRedux) => { + }: TakeActionDropdownProps) => { const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); const [isPopoverOpen, setIsPopoverOpen] = useState(false); @@ -146,24 +141,12 @@ export const TakeActionDropdownComponent = React.memo( closePopoverHandler(); }, [closePopoverHandler]); - const refetchQuery = (newQueries: inputsModel.GlobalQuery[]) => { - newQueries.forEach((q) => q.refetch && (q.refetch as inputsModel.Refetch)()); - }; - - const refetchAll = useCallback(() => { - if (timelineId === TimelineId.active) { - refetchQuery([timelineQuery]); - } else { - refetchQuery(globalQuery); - } - }, [timelineId, globalQuery, timelineQuery]); - const { actionItems: statusActionItems } = useAlertsActions({ alertStatus: actionsData.alertStatus, closePopover: closePopoverAndFlyout, eventId: actionsData.eventId, indexName, - refetch: refetchAll, + refetch, timelineId, }); @@ -233,21 +216,3 @@ export const TakeActionDropdownComponent = React.memo( ) : null; } ); - -const makeMapStateToProps = () => { - const getGlobalQueries = inputsSelectors.globalQuery(); - const getTimelineQuery = inputsSelectors.timelineQueryByIdSelector(); - const mapStateToProps = (state: State, { timelineId }: TakeActionDropdownProps) => { - return { - globalQuery: getGlobalQueries(state), - timelineQuery: getTimelineQuery(state, timelineId), - }; - }; - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps; - -export const TakeActionDropdown = connector(React.memo(TakeActionDropdownComponent)); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap index 137d8d78bcdaa..2bb9da12e44ea 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap @@ -1056,7 +1056,7 @@ Array [ - - -
- + +
-
- -
- - -
-
-
- -
-
- +
+ + + + +
+
+ , @@ -2093,7 +2095,7 @@ Array [ - - -
- -
- -
- + +
+ +
+ +
- -
-
-
-
-
-
- +
+
+
+
+
+
+
+ , ] diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx index 32c3f5a885346..4ddcd710e0406 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx @@ -5,11 +5,12 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { EuiFlyoutFooter, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { find, get, isEmpty } from 'lodash/fp'; +import { connect, ConnectedProps } from 'react-redux'; import { TakeActionDropdown } from '../../../../detections/components/take_action_dropdown'; -import type { TimelineEventsDetailsItem } from '../../../../../common'; +import { TimelineEventsDetailsItem, TimelineId } from '../../../../../common'; import { useExceptionModal } from '../../../../detections/components/alerts_table/timeline_actions/use_add_exception_modal'; import { AddExceptionModalWrapper } from '../../../../detections/components/alerts_table/timeline_actions/alert_context_menu'; import { EventFiltersModal } from '../../../../management/pages/event_filters/view/components/modal'; @@ -18,6 +19,7 @@ import { getFieldValue } from '../../../../detections/components/host_isolation/ import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; import { Ecs } from '../../../../../common/ecs'; import { useFetchEcsAlertsData } from '../../../../detections/containers/detection_engine/alerts/use_fetch_ecs_alerts_data'; +import { inputsModel, inputsSelectors, State } from '../../../../common/store'; interface EventDetailsFooterProps { detailsData: TimelineEventsDetailsItem[] | null; @@ -41,7 +43,7 @@ interface AddExceptionModalWrapperData { ruleName: string; } -export const EventDetailsFooter = React.memo( +export const EventDetailsFooterComponent = React.memo( ({ detailsData, expandedEvent, @@ -50,7 +52,9 @@ export const EventDetailsFooter = React.memo( loadingEventDetails, onAddIsolationStatusClick, timelineId, - }: EventDetailsFooterProps) => { + globalQuery, + timelineQuery, + }: EventDetailsFooterProps & PropsFromRedux) => { const ruleIndex = useMemo( () => find({ category: 'signal', field: 'signal.rule.index' }, detailsData)?.values, [detailsData] @@ -78,6 +82,18 @@ export const EventDetailsFooter = React.memo( [expandedEvent?.eventId] ); + const refetchQuery = (newQueries: inputsModel.GlobalQuery[]) => { + newQueries.forEach((q) => q.refetch && (q.refetch as inputsModel.Refetch)()); + }; + + const refetchAll = useCallback(() => { + if (timelineId === TimelineId.active) { + refetchQuery([timelineQuery]); + } else { + refetchQuery(globalQuery); + } + }, [timelineId, globalQuery, timelineQuery]); + const { exceptionModalType, onAddExceptionTypeClick, @@ -86,7 +102,7 @@ export const EventDetailsFooter = React.memo( ruleIndices, } = useExceptionModal({ ruleIndex, - refetch: expandedEvent?.refetch, + refetch: refetchAll, timelineId, }); const { closeAddEventFilterModal, isAddEventFilterModalOpen, onAddEventFilterClick } = @@ -113,7 +129,7 @@ export const EventDetailsFooter = React.memo( onAddEventFilterClick={onAddEventFilterClick} onAddExceptionTypeClick={onAddExceptionTypeClick} onAddIsolationStatusClick={onAddIsolationStatusClick} - refetch={expandedEvent?.refetch} + refetch={refetchAll} indexName={expandedEvent.indexName} timelineId={timelineId} /> @@ -142,3 +158,21 @@ export const EventDetailsFooter = React.memo( ); } ); + +const makeMapStateToProps = () => { + const getGlobalQueries = inputsSelectors.globalQuery(); + const getTimelineQuery = inputsSelectors.timelineQueryByIdSelector(); + const mapStateToProps = (state: State, { timelineId }: EventDetailsFooterProps) => { + return { + globalQuery: getGlobalQueries(state), + timelineQuery: getTimelineQuery(state, timelineId), + }; + }; + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps; + +export const EventDetailsFooter = connector(React.memo(EventDetailsFooterComponent)); diff --git a/x-pack/plugins/timelines/public/container/use_update_alerts.ts b/x-pack/plugins/timelines/public/container/use_update_alerts.ts index 7f42ddc6e8211..b38c3b9a71fef 100644 --- a/x-pack/plugins/timelines/public/container/use_update_alerts.ts +++ b/x-pack/plugins/timelines/public/container/use_update_alerts.ts @@ -17,6 +17,8 @@ import { /** * Update alert status by query + * + * @param useDetectionEngine logic flag for using the regular Detection Engine URL or the RAC URL * * @param status to update to('open' / 'closed' / 'acknowledged') * @param index index to be updated @@ -26,7 +28,7 @@ import { * @throws An error if response is not OK */ export const useUpdateAlertsStatus = ( - timelineId: string + useDetectionEngine: boolean = false ): { updateAlertStatus: (params: { status: AlertStatus; @@ -37,7 +39,7 @@ export const useUpdateAlertsStatus = ( const { http } = useKibana().services; return { updateAlertStatus: async ({ status, index, query }) => { - if (['detections-page', 'detections-rules-details-page'].includes(timelineId)) { + if (useDetectionEngine) { return http!.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, { method: 'POST', body: JSON.stringify({ status, query }), @@ -51,5 +53,3 @@ export const useUpdateAlertsStatus = ( }, }; }; - -// diff --git a/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx b/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx index c9269436646ea..c6e0e13c4dcb4 100644 --- a/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx +++ b/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx @@ -28,7 +28,7 @@ export const useStatusBulkActionItems = ({ onUpdateFailure, timelineId, }: StatusBulkActionsProps) => { - const { updateAlertStatus } = useUpdateAlertsStatus(timelineId ?? ''); + const { updateAlertStatus } = useUpdateAlertsStatus(timelineId != null); const { addSuccess, addError, addWarning } = useAppToasts(); const onAlertStatusUpdateSuccess = useCallback(