From 4fb0ca47f9ebae78709aafd54a41436d48b783c3 Mon Sep 17 00:00:00 2001 From: PhilippeOberti Date: Wed, 9 Oct 2024 19:03:26 -0500 Subject: [PATCH 1/3] [Security Solution][Notes] - update cases alerts table action column width to show analyzer and session view icons --- .../header_actions/actions.test.tsx | 111 ++++++++++++++++++ .../components/header_actions/actions.tsx | 31 ++++- .../use_actions_column.tsx | 13 +- 3 files changed, 149 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/header_actions/actions.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_actions/actions.test.tsx index bc1ae98fe1be0..090e479ba3c40 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_actions/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_actions/actions.test.tsx @@ -20,6 +20,7 @@ import { Actions } from './actions'; import { initialUserPrivilegesState as mockInitialUserPrivilegesState } from '../user_privileges/user_privileges_context'; import { useUserPrivileges } from '../user_privileges'; import { useHiddenByFlyout } from '../guided_onboarding_tour/use_hidden_by_flyout'; +// import { useUiSetting$ } from '../../lib/kibana'; const useHiddenByFlyoutMock = useHiddenByFlyout as jest.Mock; jest.mock('../guided_onboarding_tour/use_hidden_by_flyout', () => ({ @@ -27,6 +28,7 @@ jest.mock('../guided_onboarding_tour/use_hidden_by_flyout', () => ({ })); jest.mock('../guided_onboarding_tour'); jest.mock('../user_privileges'); +jest.mock('../user_privileges'); jest.mock('../../../detections/components/user_info', () => ({ useUserData: jest.fn().mockReturnValue([{ canUserCRUD: true, hasIndexWrite: true }]), })); @@ -45,6 +47,15 @@ jest.mock( }) ); +const mockUseUiSetting = jest.fn().mockReturnValue([true]); +jest.mock('@kbn/kibana-react-plugin/public', () => { + const original = jest.requireActual('@kbn/kibana-react-plugin/public'); + return { + ...original, + useUiSetting$: () => mockUseUiSetting(), + }; +}); + jest.mock('./add_note_icon_item', () => { return { AddEventNoteAction: jest.fn(() =>
), @@ -143,6 +154,7 @@ describe('Actions', () => { incrementStep: incrementStepMock, isTourShown: () => true, }); + // (useUiSetting$ as jest.Mock).mockReturnValue([true]); jest.clearAllMocks(); }); @@ -249,6 +261,7 @@ describe('Actions', () => { expect(wrapper.find(GuidedOnboardingTourStep).exists()).toEqual(true); expect(wrapper.find(SecurityTourStep).exists()).toEqual(false); }); + describe.each(Object.keys(isTourAnchorConditions))('tour condition true: %s', (key: string) => { it('Single condition does not make tour step exist', () => { const wrapper = mount( @@ -281,6 +294,7 @@ describe('Actions', () => { wrapper.find('[data-test-subj="timeline-context-menu-button"]').first().prop('isDisabled') ).toBe(true); }); + test('it enables for eventType=signal', () => { const ecsData = { ...mockTimelineData[0].ecs, @@ -296,6 +310,7 @@ describe('Actions', () => { wrapper.find('[data-test-subj="timeline-context-menu-button"]').first().prop('isDisabled') ).toBe(false); }); + test('it disables for event.kind: undefined and agent.type: endpoint', () => { const ecsData = { ...mockTimelineData[0].ecs, @@ -311,6 +326,7 @@ describe('Actions', () => { wrapper.find('[data-test-subj="timeline-context-menu-button"]').first().prop('isDisabled') ).toBe(true); }); + test('it enables for event.kind: event and agent.type: endpoint', () => { const ecsData = { ...mockTimelineData[0].ecs, @@ -327,6 +343,7 @@ describe('Actions', () => { wrapper.find('[data-test-subj="timeline-context-menu-button"]').first().prop('isDisabled') ).toBe(false); }); + test('it disables for event.kind: alert and agent.type: endpoint', () => { const ecsData = { ...mockTimelineData[0].ecs, @@ -343,6 +360,7 @@ describe('Actions', () => { wrapper.find('[data-test-subj="timeline-context-menu-button"]').first().prop('isDisabled') ).toBe(true); }); + test('it shows the analyze event button when the event is from an endpoint', () => { const ecsData = { ...mockTimelineData[0].ecs, @@ -358,6 +376,7 @@ describe('Actions', () => { expect(wrapper.find('[data-test-subj="view-in-analyzer"]').exists()).toBe(true); }); + test('it does not render the analyze event button when the event is from an unsupported source', () => { const ecsData = { ...mockTimelineData[0].ecs, @@ -373,6 +392,60 @@ describe('Actions', () => { expect(wrapper.find('[data-test-subj="view-in-analyzer"]').exists()).toBe(false); }); + test('it does not render the analyze event button on the cases alerts table with advanced settings disabled', () => { + const ecsData = { + ...mockTimelineData[0].ecs, + event: { kind: ['alert'] }, + agent: { type: ['endpoint'] }, + process: { entity_id: ['1'] }, + }; + mockUseUiSetting.mockReturnValue([false]); + + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="view-in-analyzer"]').exists()).toBe(false); + }); + + test('it does render the analyze event button on the cases alerts table with advanced settings enabled', () => { + const ecsData = { + ...mockTimelineData[0].ecs, + event: { kind: ['alert'] }, + agent: { type: ['endpoint'] }, + process: { entity_id: ['1'] }, + }; + mockUseUiSetting.mockReturnValue([true]); + + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="view-in-analyzer"]').exists()).toBe(true); + }); + + test('it does render the analyze event button on the alerts page alerts table even with advanced settings disabled', () => { + const ecsData = { + ...mockTimelineData[0].ecs, + event: { kind: ['alert'] }, + agent: { type: ['endpoint'] }, + process: { entity_id: ['1'] }, + }; + mockUseUiSetting.mockReturnValue([false]); + + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="view-in-analyzer"]').exists()).toBe(true); + }); + test('it should not show session view button on action tabs for basic users', () => { const ecsData = { ...mockTimelineData[0].ecs, @@ -434,6 +507,44 @@ describe('Actions', () => { expect(wrapper.find('[data-test-subj="session-view-button"]').exists()).toEqual(true); }); + + test('it does not render the session view button on the cases alerts table with advanced settings disabled', () => { + const ecsData = { + ...mockTimelineData[0].ecs, + event: { kind: ['alert'] }, + agent: { type: ['endpoint'] }, + process: { entry_leader: { entity_id: ['test_id'], start: ['2022-05-08T13:44:00.13Z'] } }, + _index: '.ds-logs-endpoint.events.process-default', + }; + mockUseUiSetting.mockReturnValue([false]); + + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="session-view-button"]').exists()).toBe(false); + }); + + test('it does render the session view button on the cases alerts table with advanced settings enabled', () => { + const ecsData = { + ...mockTimelineData[0].ecs, + event: { kind: ['alert'] }, + agent: { type: ['endpoint'] }, + process: { entry_leader: { entity_id: ['test_id'], start: ['2022-05-08T13:44:00.13Z'] } }, + _index: '.ds-logs-endpoint.events.process-default', + }; + mockUseUiSetting.mockReturnValue([true]); + + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="session-view-button"]').exists()).toBe(true); + }); }); describe('Show notes action', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/header_actions/actions.tsx b/x-pack/plugins/security_solution/public/common/components/header_actions/actions.tsx index 2a40c4edd7d2c..e010a8df94016 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_actions/actions.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_actions/actions.tsx @@ -82,8 +82,6 @@ const ActionsComponent: React.FC = ({ const { startTransaction } = useStartTransaction(); - const isEnterprisePlus = useLicense().isEnterprise(); - const onPinEvent: OnPinEvent = useCallback( (evtId) => dispatch(timelineActions.pinEvent({ id: timelineId, eventId: evtId })), [dispatch, timelineId] @@ -292,6 +290,30 @@ const ActionsComponent: React.FC = ({ [documentBasedNotes, timelineNoteIds, securitySolutionNotesEnabled] ); + // we hide the analyzer icon if the data is not available for the resolver + // or if we are on the cases alerts table and the the visualization in flyout advanced setting is disabled + const ecsHasDataForAnalyzer = useIsInvestigateInResolverActionEnabled(ecsData); + const showAnalyzerIcon = useMemo(() => { + return ( + ecsHasDataForAnalyzer && + (timelineId !== TableId.alertsOnCasePage || + (timelineId === TableId.alertsOnCasePage && visualizationInFlyoutEnabled)) + ); + }, [ecsHasDataForAnalyzer, timelineId, visualizationInFlyoutEnabled]); + + // we hide the session view icon if the session view is not available + // or if we are on the cases alerts table and the the visualization in flyout advanced setting is disabled + // or if the user is not on an enterprise license or on the kubernetes page + const isEnterprisePlus = useLicense().isEnterprise(); + const showSessionViewIcon = useMemo(() => { + return ( + sessionViewConfig !== null && + (isEnterprisePlus || timelineId === TableId.kubernetesPageSessions) && + (timelineId !== TableId.alertsOnCasePage || + (timelineId === TableId.alertsOnCasePage && visualizationInFlyoutEnabled)) + ); + }, [sessionViewConfig, isEnterprisePlus, timelineId, visualizationInFlyoutEnabled]); + return ( <> @@ -359,7 +381,7 @@ const ActionsComponent: React.FC = ({ onRuleChange={onRuleChange} refetch={refetch} /> - {isDisabled === false ? ( + {showAnalyzerIcon ? (
= ({
) : null} - {sessionViewConfig !== null && - (isEnterprisePlus || timelineId === TableId.kubernetesPageSessions) ? ( + {showSessionViewIcon ? (
diff --git a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_actions_column.tsx b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_actions_column.tsx index 59e3cf9f07f9c..38686a0c58a25 100644 --- a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_actions_column.tsx +++ b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_actions_column.tsx @@ -13,6 +13,8 @@ import type { RenderCustomActionsRowArgs, } from '@kbn/triggers-actions-ui-plugin/public/types'; import { TableId } from '@kbn/securitysolution-data-table'; +import { useUiSetting$ } from '@kbn/kibana-react-plugin/public'; +import { ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING } from '../../../../common/constants'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { StatefulEventContext } from '../../../common/components/events_viewer/stateful_event_context'; import { eventsViewerSelector } from '../../../common/components/events_viewer/selectors'; @@ -28,7 +30,7 @@ export const getUseActionColumnHook = () => { const license = useLicense(); const isEnterprisePlus = license.isEnterprise(); - let ACTION_BUTTON_COUNT = tableId === TableId.alertsOnCasePage ? 4 : isEnterprisePlus ? 6 : 5; + let ACTION_BUTTON_COUNT = isEnterprisePlus ? 6 : 5; // we only want to show the note icon if the expandable flyout and the new notes system are enabled // TODO delete most likely in 8.16 @@ -39,6 +41,15 @@ export const getUseActionColumnHook = ACTION_BUTTON_COUNT--; } + // we do not show the analyzer graph and session view icons on the cases alerts tab alerts table + // if the visualization in flyout advanced settings is disabled because these aren't supported inside the table + const [visualizationInFlyoutEnabled] = useUiSetting$( + ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING + ); + if (!visualizationInFlyoutEnabled && tableId === TableId.alertsOnCasePage) { + ACTION_BUTTON_COUNT -= 2; + } + const eventContext = useContext(StatefulEventContext); const leadingControlColumn = useMemo( From 562c3ae307907d8e54ae582c7c757f44c680071d Mon Sep 17 00:00:00 2001 From: PhilippeOberti Date: Thu, 10 Oct 2024 12:57:04 -0500 Subject: [PATCH 2/3] cleanup of commented code --- .../public/common/components/header_actions/actions.test.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/header_actions/actions.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_actions/actions.test.tsx index 090e479ba3c40..818b56556e768 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_actions/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_actions/actions.test.tsx @@ -20,7 +20,6 @@ import { Actions } from './actions'; import { initialUserPrivilegesState as mockInitialUserPrivilegesState } from '../user_privileges/user_privileges_context'; import { useUserPrivileges } from '../user_privileges'; import { useHiddenByFlyout } from '../guided_onboarding_tour/use_hidden_by_flyout'; -// import { useUiSetting$ } from '../../lib/kibana'; const useHiddenByFlyoutMock = useHiddenByFlyout as jest.Mock; jest.mock('../guided_onboarding_tour/use_hidden_by_flyout', () => ({ @@ -154,7 +153,6 @@ describe('Actions', () => { incrementStep: incrementStepMock, isTourShown: () => true, }); - // (useUiSetting$ as jest.Mock).mockReturnValue([true]); jest.clearAllMocks(); }); From cb46c19f5f62f0469e4513694562542bc7e36399 Mon Sep 17 00:00:00 2001 From: PhilippeOberti Date: Fri, 11 Oct 2024 11:22:28 -0500 Subject: [PATCH 3/3] handling isEnterprise plus on cases page --- .../components/header_actions/actions.tsx | 1 - .../use_actions_column.tsx | 33 ++++++++++++++----- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/header_actions/actions.tsx b/x-pack/plugins/security_solution/public/common/components/header_actions/actions.tsx index e010a8df94016..2f27b1c8d8d9f 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_actions/actions.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_actions/actions.tsx @@ -130,7 +130,6 @@ const ActionsComponent: React.FC = ({ scopeId: timelineId, }); - const isDisabled = !useIsInvestigateInResolverActionEnabled(ecsData); const { setGlobalFullScreen } = useGlobalFullScreen(); const { setTimelineFullScreen } = useTimelineFullScreen(); const handleClick = useCallback(() => { diff --git a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_actions_column.tsx b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_actions_column.tsx index 38686a0c58a25..39b8d7cad3f60 100644 --- a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_actions_column.tsx +++ b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_actions_column.tsx @@ -25,15 +25,28 @@ import { getAlertsDefaultModel } from '../../components/alerts_table/default_con import type { State } from '../../../common/store'; import { RowAction } from '../../../common/components/control_columns/row_action'; +// we show a maximum of 6 action buttons +// - open flyout +// - investigate in timeline +// - 3-dot menu for more actions +// - add new note +// - session view +// - analyzer graph +const MAX_ACTION_BUTTON_COUNT = 6; + export const getUseActionColumnHook = (tableId: TableId): AlertsTableConfigurationRegistry['useActionsColumn'] => () => { + let ACTION_BUTTON_COUNT = MAX_ACTION_BUTTON_COUNT; + + // hiding the session view icon for users without enterprise plus license const license = useLicense(); const isEnterprisePlus = license.isEnterprise(); - let ACTION_BUTTON_COUNT = isEnterprisePlus ? 6 : 5; + if (!isEnterprisePlus) { + ACTION_BUTTON_COUNT--; + } - // we only want to show the note icon if the expandable flyout and the new notes system are enabled - // TODO delete most likely in 8.16 + // we only want to show the note icon if the new notes system feature flag is enabled const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled( 'securitySolutionNotesEnabled' ); @@ -43,11 +56,15 @@ export const getUseActionColumnHook = // we do not show the analyzer graph and session view icons on the cases alerts tab alerts table // if the visualization in flyout advanced settings is disabled because these aren't supported inside the table - const [visualizationInFlyoutEnabled] = useUiSetting$( - ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING - ); - if (!visualizationInFlyoutEnabled && tableId === TableId.alertsOnCasePage) { - ACTION_BUTTON_COUNT -= 2; + if (tableId === TableId.alertsOnCasePage) { + const [visualizationInFlyoutEnabled] = useUiSetting$( + ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING + ); + if (!isEnterprisePlus && !visualizationInFlyoutEnabled) { + ACTION_BUTTON_COUNT -= 1; + } else if (isEnterprisePlus && !visualizationInFlyoutEnabled) { + ACTION_BUTTON_COUNT -= 2; + } } const eventContext = useContext(StatefulEventContext);