From d0bc10f896a30c4b08c5fc302cd98c661d6847e3 Mon Sep 17 00:00:00 2001 From: Ashokaditya Date: Tue, 19 Oct 2021 05:27:12 +0200 Subject: [PATCH 1/6] [Security Solution][Endpoint]Activity Log API/UX changes (#114905) * rename legacy actions/responses fixes elastic/security-team/issues/1702 * use correct name for responses index refs elastic/kibana/pull/113621 * extract helper method to utils * append endpoint responses docs to activity log * Show completed responses on activity log fixes elastic/security-team/issues/1703 * remove width restriction on date picker * add a simple test to verify endpoint responses fixes elastic/security-team/issues/1702 * find unique action_ids from `.fleet-actions` and `.logs-endpoint.actions-default` indices fixes elastic/security-team/issues/1702 * do not filter out endpoint only actions/responses that did not make it to Fleet review comments * use a constant to manage various doc types review comments * refactor `getActivityLog` Simplify `getActivityLog` so it is easier to reason with. review comments * skip this for now will mock this better in a new PR * improve types * display endpoint actions similar to fleet actions, but with success icon color * Correctly do mocks for tests * Include only errored endpoint actions, remove successful duplicates fixes elastic/security-team/issues/1703 * Update tests to use non duplicate action_ids review comments fixes elastic/security-team/issues/1703 * show correct action title review fixes * statusCode constant review change * rename review changes * Update translations.ts refs https://github.com/elastic/kibana/pull/114905/commits/74a8340b5eb2e31faba67a4fbe656f74fe52d0a2 Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/endpoint/constants.ts | 4 +- .../common/endpoint/types/actions.ts | 33 ++- .../activity_log_date_range_picker/index.tsx | 1 - .../view/details/components/log_entry.tsx | 101 ++++++- .../components/log_entry_timeline_icon.tsx | 21 +- .../view/details/endpoints.stories.tsx | 14 +- .../pages/endpoint_hosts/view/index.test.tsx | 95 +++++-- .../pages/endpoint_hosts/view/translations.ts | 36 +++ .../endpoint/routes/actions/audit_log.test.ts | 213 ++++++++++++-- .../endpoint/routes/actions/isolation.ts | 29 +- .../server/endpoint/routes/actions/mocks.ts | 32 +++ .../server/endpoint/services/actions.ts | 146 ++++------ .../endpoint/utils/audit_log_helpers.ts | 266 ++++++++++++++++++ .../server/endpoint/utils/index.ts | 2 + .../endpoint/utils/yes_no_data_stream.test.ts | 100 +++++++ .../endpoint/utils/yes_no_data_stream.ts | 59 ++++ 16 files changed, 949 insertions(+), 203 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/endpoint/utils/audit_log_helpers.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.test.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index 6e9123da2dd9b..178a2b68a4aab 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -10,7 +10,7 @@ export const ENDPOINT_ACTIONS_DS = '.logs-endpoint.actions'; export const ENDPOINT_ACTIONS_INDEX = `${ENDPOINT_ACTIONS_DS}-default`; export const ENDPOINT_ACTION_RESPONSES_DS = '.logs-endpoint.action.responses'; -export const ENDPOINT_ACTION_RESPONSES_INDEX = `${ENDPOINT_ACTIONS_DS}-default`; +export const ENDPOINT_ACTION_RESPONSES_INDEX = `${ENDPOINT_ACTION_RESPONSES_DS}-default`; export const eventsIndexPattern = 'logs-endpoint.events.*'; export const alertsIndexPattern = 'logs-endpoint.alerts-*'; @@ -60,3 +60,5 @@ export const UNISOLATE_HOST_ROUTE = `${BASE_ENDPOINT_ROUTE}/unisolate`; /** Endpoint Actions Log Routes */ export const ENDPOINT_ACTION_LOG_ROUTE = `/api/endpoint/action_log/{agent_id}`; export const ACTION_STATUS_ROUTE = `/api/endpoint/action_status`; + +export const failedFleetActionErrorCode = '424'; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index bc46ca2f5b451..fb29297eb5929 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -10,6 +10,13 @@ import { ActionStatusRequestSchema, HostIsolationRequestSchema } from '../schema export type ISOLATION_ACTIONS = 'isolate' | 'unisolate'; +export const ActivityLogItemTypes = { + ACTION: 'action' as const, + RESPONSE: 'response' as const, + FLEET_ACTION: 'fleetAction' as const, + FLEET_RESPONSE: 'fleetResponse' as const, +}; + interface EcsError { code?: string; id?: string; @@ -87,8 +94,24 @@ export interface EndpointActionResponse { action_data: EndpointActionData; } +export interface EndpointActivityLogAction { + type: typeof ActivityLogItemTypes.ACTION; + item: { + id: string; + data: LogsEndpointAction; + }; +} + +export interface EndpointActivityLogActionResponse { + type: typeof ActivityLogItemTypes.RESPONSE; + item: { + id: string; + data: LogsEndpointActionResponse; + }; +} + export interface ActivityLogAction { - type: 'action'; + type: typeof ActivityLogItemTypes.FLEET_ACTION; item: { // document _id id: string; @@ -97,7 +120,7 @@ export interface ActivityLogAction { }; } export interface ActivityLogActionResponse { - type: 'response'; + type: typeof ActivityLogItemTypes.FLEET_RESPONSE; item: { // document id id: string; @@ -105,7 +128,11 @@ export interface ActivityLogActionResponse { data: EndpointActionResponse; }; } -export type ActivityLogEntry = ActivityLogAction | ActivityLogActionResponse; +export type ActivityLogEntry = + | ActivityLogAction + | ActivityLogActionResponse + | EndpointActivityLogAction + | EndpointActivityLogActionResponse; export interface ActivityLog { page: number; pageSize: number; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx index 05887d82cacad..a57fa8d8e4ce5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx @@ -32,7 +32,6 @@ interface Range { const DatePickerWrapper = styled.div` width: ${(props) => props.theme.eui.fractions.single.percentage}; - max-width: 350px; `; const StickyFlexItem = styled(EuiFlexItem)` background: ${(props) => `${props.theme.eui.euiHeaderBackgroundColor}`}; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx index bbe0a6f3afcd1..79af2ecb354fd 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx @@ -9,24 +9,34 @@ import React, { memo, useMemo } from 'react'; import styled from 'styled-components'; import { EuiComment, EuiText, EuiAvatarProps, EuiCommentProps, IconType } from '@elastic/eui'; -import { Immutable, ActivityLogEntry } from '../../../../../../../common/endpoint/types'; +import { + Immutable, + ActivityLogEntry, + ActivityLogItemTypes, +} from '../../../../../../../common/endpoint/types'; import { FormattedRelativePreferenceDate } from '../../../../../../common/components/formatted_date'; import { LogEntryTimelineIcon } from './log_entry_timeline_icon'; +import { useEuiTheme } from '../../../../../../common/lib/theme/use_eui_theme'; import * as i18 from '../../translations'; const useLogEntryUIProps = ( - logEntry: Immutable + logEntry: Immutable, + theme: ReturnType ): { actionEventTitle: string; + avatarColor: EuiAvatarProps['color']; + avatarIconColor: EuiAvatarProps['iconColor']; avatarSize: EuiAvatarProps['size']; commentText: string; commentType: EuiCommentProps['type']; displayComment: boolean; displayResponseEvent: boolean; + failedActionEventTitle: string; iconType: IconType; isResponseEvent: boolean; isSuccessful: boolean; + isCompleted: boolean; responseEventTitle: string; username: string | React.ReactNode; } => { @@ -34,15 +44,19 @@ const useLogEntryUIProps = ( let iconType: IconType = 'dot'; let commentType: EuiCommentProps['type'] = 'update'; let commentText: string = ''; + let avatarColor: EuiAvatarProps['color'] = theme.euiColorLightestShade; + let avatarIconColor: EuiAvatarProps['iconColor']; let avatarSize: EuiAvatarProps['size'] = 's'; + let failedActionEventTitle: string = ''; let isIsolateAction: boolean = false; let isResponseEvent: boolean = false; let isSuccessful: boolean = false; + let isCompleted: boolean = false; let displayComment: boolean = false; let displayResponseEvent: boolean = true; let username: EuiCommentProps['username'] = ''; - if (logEntry.type === 'action') { + if (logEntry.type === ActivityLogItemTypes.FLEET_ACTION) { avatarSize = 'm'; commentType = 'regular'; commentText = logEntry.item.data.data.comment?.trim() ?? ''; @@ -59,13 +73,51 @@ const useLogEntryUIProps = ( displayComment = true; } } - } else if (logEntry.type === 'response') { + } + if (logEntry.type === ActivityLogItemTypes.ACTION) { + avatarSize = 'm'; + commentType = 'regular'; + commentText = logEntry.item.data.EndpointActions.data.comment?.trim() ?? ''; + displayResponseEvent = false; + iconType = 'lockOpen'; + username = logEntry.item.data.user.id; + avatarIconColor = theme.euiColorVis9_behindText; + failedActionEventTitle = i18.ACTIVITY_LOG.LogEntry.action.failedEndpointReleaseAction; + if (logEntry.item.data.EndpointActions.data) { + const data = logEntry.item.data.EndpointActions.data; + if (data.command === 'isolate') { + iconType = 'lock'; + failedActionEventTitle = i18.ACTIVITY_LOG.LogEntry.action.failedEndpointIsolateAction; + } + if (commentText) { + displayComment = true; + } + } + } else if (logEntry.type === ActivityLogItemTypes.FLEET_RESPONSE) { isResponseEvent = true; if (logEntry.item.data.action_data.command === 'isolate') { isIsolateAction = true; } if (!!logEntry.item.data.completed_at && !logEntry.item.data.error) { isSuccessful = true; + } else { + avatarColor = theme.euiColorVis9_behindText; + } + } else if (logEntry.type === ActivityLogItemTypes.RESPONSE) { + iconType = 'check'; + isResponseEvent = true; + if (logEntry.item.data.EndpointActions.data.command === 'isolate') { + isIsolateAction = true; + } + if (logEntry.item.data.EndpointActions.completed_at) { + isCompleted = true; + if (!logEntry.item.data.error) { + isSuccessful = true; + avatarColor = theme.euiColorVis0_behindText; + } else { + isSuccessful = false; + avatarColor = theme.euiColorVis9_behindText; + } } } @@ -75,13 +127,23 @@ const useLogEntryUIProps = ( const getResponseEventTitle = () => { if (isIsolateAction) { - if (isSuccessful) { + if (isCompleted) { + if (isSuccessful) { + return i18.ACTIVITY_LOG.LogEntry.response.unisolationCompletedAndSuccessful; + } + return i18.ACTIVITY_LOG.LogEntry.response.unisolationCompletedAndUnsuccessful; + } else if (isSuccessful) { return i18.ACTIVITY_LOG.LogEntry.response.isolationSuccessful; } else { return i18.ACTIVITY_LOG.LogEntry.response.isolationFailed; } } else { - if (isSuccessful) { + if (isCompleted) { + if (isSuccessful) { + return i18.ACTIVITY_LOG.LogEntry.response.unisolationCompletedAndSuccessful; + } + return i18.ACTIVITY_LOG.LogEntry.response.unisolationCompletedAndUnsuccessful; + } else if (isSuccessful) { return i18.ACTIVITY_LOG.LogEntry.response.unisolationSuccessful; } else { return i18.ACTIVITY_LOG.LogEntry.response.unisolationFailed; @@ -91,18 +153,22 @@ const useLogEntryUIProps = ( return { actionEventTitle, + avatarColor, + avatarIconColor, avatarSize, commentText, commentType, displayComment, displayResponseEvent, + failedActionEventTitle, iconType, isResponseEvent, isSuccessful, + isCompleted, responseEventTitle: getResponseEventTitle(), username, }; - }, [logEntry]); + }, [logEntry, theme]); }; const StyledEuiComment = styled(EuiComment)` @@ -126,28 +192,41 @@ const StyledEuiComment = styled(EuiComment)` `; export const LogEntry = memo(({ logEntry }: { logEntry: Immutable }) => { + const theme = useEuiTheme(); const { actionEventTitle, + avatarColor, + avatarIconColor, avatarSize, commentText, commentType, displayComment, displayResponseEvent, + failedActionEventTitle, iconType, isResponseEvent, - isSuccessful, responseEventTitle, username, - } = useLogEntryUIProps(logEntry); + } = useLogEntryUIProps(logEntry, theme); return ( } - event={{displayResponseEvent ? responseEventTitle : actionEventTitle}} + event={ + + {displayResponseEvent + ? responseEventTitle + : failedActionEventTitle + ? failedActionEventTitle + : actionEventTitle} + + } timelineIcon={ - + } data-test-subj="timelineEntry" > diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry_timeline_icon.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry_timeline_icon.tsx index 3ff311cd8a139..25e7c7d2c4a49 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry_timeline_icon.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry_timeline_icon.tsx @@ -7,32 +7,27 @@ import React, { memo } from 'react'; import { EuiAvatar, EuiAvatarProps } from '@elastic/eui'; -import { useEuiTheme } from '../../../../../../common/lib/theme/use_eui_theme'; export const LogEntryTimelineIcon = memo( ({ + avatarColor, + avatarIconColor, avatarSize, - isResponseEvent, - isSuccessful, iconType, + isResponseEvent, }: { + avatarColor: EuiAvatarProps['color']; + avatarIconColor?: EuiAvatarProps['iconColor']; avatarSize: EuiAvatarProps['size']; - isResponseEvent: boolean; - isSuccessful: boolean; iconType: EuiAvatarProps['iconType']; + isResponseEvent: boolean; }) => { - const euiTheme = useEuiTheme(); - return ( ); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx index 123a51e5a52bd..717368a1ff3a0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx @@ -8,7 +8,11 @@ import React, { ComponentType } from 'react'; import moment from 'moment'; -import { ActivityLog, Immutable } from '../../../../../../common/endpoint/types'; +import { + ActivityLog, + Immutable, + ActivityLogItemTypes, +} from '../../../../../../common/endpoint/types'; import { EndpointDetailsFlyoutTabs } from './components/endpoint_details_tabs'; import { EndpointActivityLog } from './endpoint_activity_log'; import { EndpointDetailsFlyout } from '.'; @@ -26,7 +30,7 @@ export const dummyEndpointActivityLog = ( endDate: moment().toString(), data: [ { - type: 'action', + type: ActivityLogItemTypes.FLEET_ACTION, item: { id: '', data: { @@ -44,7 +48,7 @@ export const dummyEndpointActivityLog = ( }, }, { - type: 'action', + type: ActivityLogItemTypes.FLEET_ACTION, item: { id: '', data: { @@ -63,7 +67,7 @@ export const dummyEndpointActivityLog = ( }, }, { - type: 'action', + type: ActivityLogItemTypes.FLEET_ACTION, item: { id: '', data: { @@ -82,7 +86,7 @@ export const dummyEndpointActivityLog = ( }, }, { - type: 'action', + type: ActivityLogItemTypes.FLEET_ACTION, item: { id: '', data: { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index b2c438659b771..727c2e8a35024 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -42,6 +42,7 @@ import { import { getCurrentIsolationRequestState } from '../store/selectors'; import { licenseService } from '../../../../common/hooks/use_license'; import { FleetActionGenerator } from '../../../../../common/endpoint/data_generators/fleet_action_generator'; +import { EndpointActionGenerator } from '../../../../../common/endpoint/data_generators/endpoint_action_generator'; import { APP_PATH, MANAGEMENT_PATH, @@ -807,7 +808,7 @@ describe('when on the endpoint list page', () => { let renderResult: ReturnType; const agentId = 'some_agent_id'; - let getMockData: () => ActivityLog; + let getMockData: (option?: { hasLogsEndpointActionResponses?: boolean }) => ActivityLog; beforeEach(async () => { window.IntersectionObserver = jest.fn(() => ({ root: null, @@ -828,10 +829,15 @@ describe('when on the endpoint list page', () => { }); const fleetActionGenerator = new FleetActionGenerator('seed'); - const responseData = fleetActionGenerator.generateResponse({ + const endpointActionGenerator = new EndpointActionGenerator('seed'); + const endpointResponseData = endpointActionGenerator.generateResponse({ + agent: { id: agentId }, + }); + const fleetResponseData = fleetActionGenerator.generateResponse({ agent_id: agentId, }); - const actionData = fleetActionGenerator.generate({ + + const fleetActionData = fleetActionGenerator.generate({ agents: [agentId], data: { comment: 'some comment', @@ -844,35 +850,49 @@ describe('when on the endpoint list page', () => { }, }); - getMockData = () => ({ - page: 1, - pageSize: 50, - startDate: 'now-1d', - endDate: 'now', - data: [ - { - type: 'response', - item: { - id: 'some_id_0', - data: responseData, + getMockData = (hasLogsEndpointActionResponses?: { + hasLogsEndpointActionResponses?: boolean; + }) => { + const response: ActivityLog = { + page: 1, + pageSize: 50, + startDate: 'now-1d', + endDate: 'now', + data: [ + { + type: 'fleetResponse', + item: { + id: 'some_id_1', + data: fleetResponseData, + }, }, - }, - { - type: 'action', - item: { - id: 'some_id_1', - data: actionData, + { + type: 'fleetAction', + item: { + id: 'some_id_2', + data: fleetActionData, + }, }, - }, - { - type: 'action', + { + type: 'fleetAction', + item: { + id: 'some_id_3', + data: isolatedActionData, + }, + }, + ], + }; + if (hasLogsEndpointActionResponses) { + response.data.unshift({ + type: 'response', item: { - id: 'some_id_3', - data: isolatedActionData, + id: 'some_id_0', + data: endpointResponseData, }, - }, - ], - }); + }); + } + return response; + }; renderResult = render(); await reactTestingLibrary.act(async () => { @@ -912,6 +932,25 @@ describe('when on the endpoint list page', () => { expect(`${logEntries[1]} .euiCommentTimeline__icon--regular`).not.toBe(null); }); + it('should display log accurately with endpoint responses', async () => { + const activityLogTab = await renderResult.findByTestId('activity_log'); + reactTestingLibrary.act(() => { + reactTestingLibrary.fireEvent.click(activityLogTab); + }); + await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged'); + reactTestingLibrary.act(() => { + dispatchEndpointDetailsActivityLogChanged( + 'success', + getMockData({ hasLogsEndpointActionResponses: true }) + ); + }); + const logEntries = await renderResult.queryAllByTestId('timelineEntry'); + expect(logEntries.length).toEqual(4); + expect(`${logEntries[0]} .euiCommentTimeline__icon--update`).not.toBe(null); + expect(`${logEntries[1]} .euiCommentTimeline__icon--update`).not.toBe(null); + expect(`${logEntries[2]} .euiCommentTimeline__icon--regular`).not.toBe(null); + }); + it('should display empty state when API call has failed', async () => { const activityLogTab = await renderResult.findByTestId('activity_log'); reactTestingLibrary.act(() => { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts index c8a29eed3fda7..9cd55a70005ec 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts @@ -56,8 +56,44 @@ export const ACTIVITY_LOG = { defaultMessage: 'submitted request: Release host', } ), + failedEndpointReleaseAction: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.action.failedEndpointReleaseAction', + { + defaultMessage: 'failed to submit request: Release host', + } + ), + failedEndpointIsolateAction: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.action.failedEndpointIsolateAction', + { + defaultMessage: 'failed to submit request: Isolate host', + } + ), }, response: { + isolationCompletedAndSuccessful: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.isolationCompletedAndSuccessful', + { + defaultMessage: 'Host isolation request completed by Endpoint', + } + ), + isolationCompletedAndUnsuccessful: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.isolationCompletedAndUnsuccessful', + { + defaultMessage: 'Host isolation request completed by Endpoint with errors', + } + ), + unisolationCompletedAndSuccessful: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.unisolationCompletedAndSuccessful', + { + defaultMessage: 'Release request completed by Endpoint', + } + ), + unisolationCompletedAndUnsuccessful: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.unisolationCompletedAndUnsuccessful', + { + defaultMessage: 'Release request completed by Endpoint with errors', + } + ), isolationSuccessful: i18n.translate( 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.isolationSuccessful', { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts index 4bd63c83169e5..5ce7962000788 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts @@ -30,9 +30,15 @@ import { } from '../../mocks'; import { registerActionAuditLogRoutes } from './audit_log'; import uuid from 'uuid'; -import { aMockAction, aMockResponse, MockAction, mockSearchResult, MockResponse } from './mocks'; +import { mockAuditLogSearchResult, Results } from './mocks'; import { SecuritySolutionRequestHandlerContext } from '../../../types'; -import { ActivityLog } from '../../../../common/endpoint/types'; +import { + ActivityLog, + EndpointAction, + EndpointActionResponse, +} from '../../../../common/endpoint/types'; +import { FleetActionGenerator } from '../../../../common/endpoint/data_generators/fleet_action_generator'; +import { EndpointActionGenerator } from '../../../../common/endpoint/data_generators/endpoint_action_generator'; describe('Action Log API', () => { describe('schema', () => { @@ -93,17 +99,30 @@ describe('Action Log API', () => { }); describe('response', () => { - const mockID = 'XYZABC-000'; - const actionID = 'some-known-actionid'; + const mockAgentID = 'XYZABC-000'; let endpointAppContextService: EndpointAppContextService; + const fleetActionGenerator = new FleetActionGenerator('seed'); + const endpointActionGenerator = new EndpointActionGenerator('seed'); // convenience for calling the route and handler for audit log let getActivityLog: ( params: EndpointActionLogRequestParams, query?: EndpointActionLogRequestQuery ) => Promise>; - // convenience for injecting mock responses for actions index and responses - let havingActionsAndResponses: (actions: MockAction[], responses: MockResponse[]) => void; + + // convenience for injecting mock action requests and responses + // for .logs-endpoint and .fleet indices + let mockActions: ({ + numActions, + hasFleetActions, + hasFleetResponses, + hasResponses, + }: { + numActions: number; + hasFleetActions?: boolean; + hasFleetResponses?: boolean; + hasResponses?: boolean; + }) => void; let havingErrors: () => void; @@ -149,12 +168,113 @@ describe('Action Log API', () => { return mockResponse; }; - havingActionsAndResponses = (actions: MockAction[], responses: MockResponse[]) => { - esClientMock.asCurrentUser.search = jest.fn().mockImplementation((req) => { - const items: any[] = - req.index === '.fleet-actions' ? actions.splice(0, 50) : responses.splice(0, 1000); + // some arbitrary ids for needed actions + const getMockActionIds = (numAction: number): string[] => { + return [...Array(numAction).keys()].map(() => Math.random().toString(36).split('.')[1]); + }; + + // create as many actions as needed + const getEndpointActionsData = (actionIds: string[]) => { + const data = actionIds.map((actionId) => + endpointActionGenerator.generate({ + agent: { id: mockAgentID }, + EndpointActions: { + action_id: actionId, + }, + }) + ); + return data; + }; + // create as many responses as needed + const getEndpointResponseData = (actionIds: string[]) => { + const data = actionIds.map((actionId) => + endpointActionGenerator.generateResponse({ + agent: { id: mockAgentID }, + EndpointActions: { + action_id: actionId, + }, + }) + ); + return data; + }; + // create as many fleet actions as needed + const getFleetResponseData = (actionIds: string[]) => { + const data = actionIds.map((actionId) => + fleetActionGenerator.generateResponse({ + agent_id: mockAgentID, + action_id: actionId, + }) + ); + return data; + }; + // create as many fleet responses as needed + const getFleetActionData = (actionIds: string[]) => { + const data = actionIds.map((actionId) => + fleetActionGenerator.generate({ + agents: [mockAgentID], + action_id: actionId, + data: { + comment: 'some comment', + }, + }) + ); + return data; + }; + + // mock actions and responses results in a single response + mockActions = ({ + numActions, + hasFleetActions = false, + hasFleetResponses = false, + hasResponses = false, + }: { + numActions: number; + hasFleetActions?: boolean; + hasFleetResponses?: boolean; + hasResponses?: boolean; + }) => { + esClientMock.asCurrentUser.search = jest.fn().mockImplementationOnce(() => { + let actions: Results[] = []; + let fleetActions: Results[] = []; + let responses: Results[] = []; + let fleetResponses: Results[] = []; + + const actionIds = getMockActionIds(numActions); + + actions = getEndpointActionsData(actionIds).map((e) => ({ + _index: '.ds-.logs-endpoint.actions-default-2021.19.10-000001', + _source: e, + })); + + if (hasFleetActions) { + fleetActions = getFleetActionData(actionIds).map((e) => ({ + _index: '.fleet-actions-7', + _source: e, + })); + } - return Promise.resolve(mockSearchResult(items.map((x) => x.build()))); + if (hasFleetResponses) { + fleetResponses = getFleetResponseData(actionIds).map((e) => ({ + _index: '.ds-.fleet-actions-results-2021.19.10-000001', + _source: e, + })); + } + + if (hasResponses) { + responses = getEndpointResponseData(actionIds).map((e) => ({ + _index: '.ds-.logs-endpoint.action.responses-default-2021.19.10-000001', + _source: e, + })); + } + + const results = mockAuditLogSearchResult([ + ...actions, + ...fleetActions, + ...responses, + ...fleetResponses, + ]); + + return Promise.resolve(results); }); }; @@ -172,45 +292,80 @@ describe('Action Log API', () => { }); it('should return an empty array when nothing in audit log', async () => { - havingActionsAndResponses([], []); - const response = await getActivityLog({ agent_id: mockID }); + mockActions({ numActions: 0 }); + + const response = await getActivityLog({ agent_id: mockAgentID }); expect(response.ok).toBeCalled(); expect((response.ok.mock.calls[0][0]?.body as ActivityLog).data).toHaveLength(0); }); - it('should have actions and action responses', async () => { - havingActionsAndResponses( - [ - aMockAction().withAgent(mockID).withAction('isolate').withID(actionID), - aMockAction().withAgent(mockID).withAction('unisolate'), - ], - [aMockResponse(actionID, mockID).forAction(actionID).forAgent(mockID)] - ); - const response = await getActivityLog({ agent_id: mockID }); + it('should return fleet actions, fleet responses and endpoint responses', async () => { + mockActions({ + numActions: 2, + hasFleetActions: true, + hasFleetResponses: true, + hasResponses: true, + }); + + const response = await getActivityLog({ agent_id: mockAgentID }); + const responseBody = response.ok.mock.calls[0][0]?.body as ActivityLog; + expect(response.ok).toBeCalled(); + expect(responseBody.data).toHaveLength(6); + + expect( + responseBody.data.filter((e) => (e.item.data as EndpointActionResponse).completed_at) + ).toHaveLength(2); + expect( + responseBody.data.filter((e) => (e.item.data as EndpointAction).expiration) + ).toHaveLength(2); + }); + + it('should return only fleet actions and no responses', async () => { + mockActions({ numActions: 2, hasFleetActions: true }); + + const response = await getActivityLog({ agent_id: mockAgentID }); const responseBody = response.ok.mock.calls[0][0]?.body as ActivityLog; + expect(response.ok).toBeCalled(); + expect(responseBody.data).toHaveLength(2); + + expect( + responseBody.data.filter((e) => (e.item.data as EndpointAction).expiration) + ).toHaveLength(2); + }); + + it('should only have fleet data', async () => { + mockActions({ numActions: 2, hasFleetActions: true, hasFleetResponses: true }); + const response = await getActivityLog({ agent_id: mockAgentID }); + const responseBody = response.ok.mock.calls[0][0]?.body as ActivityLog; expect(response.ok).toBeCalled(); - expect(responseBody.data).toHaveLength(3); - expect(responseBody.data.filter((e) => e.type === 'response')).toHaveLength(1); - expect(responseBody.data.filter((e) => e.type === 'action')).toHaveLength(2); + expect(responseBody.data).toHaveLength(4); + + expect( + responseBody.data.filter((e) => (e.item.data as EndpointAction).expiration) + ).toHaveLength(2); + expect( + responseBody.data.filter((e) => (e.item.data as EndpointActionResponse).completed_at) + ).toHaveLength(2); }); it('should throw errors when no results for some agentID', async () => { havingErrors(); try { - await getActivityLog({ agent_id: mockID }); + await getActivityLog({ agent_id: mockAgentID }); } catch (error) { - expect(error.message).toEqual(`Error fetching actions log for agent_id ${mockID}`); + expect(error.message).toEqual(`Error fetching actions log for agent_id ${mockAgentID}`); } }); it('should return date ranges if present in the query', async () => { - havingActionsAndResponses([], []); + mockActions({ numActions: 0 }); + const startDate = new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(); const endDate = new Date().toISOString(); const response = await getActivityLog( - { agent_id: mockID }, + { agent_id: mockAgentID }, { page: 1, page_size: 50, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts index e12299bedbb34..02f0cb4867646 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts @@ -17,6 +17,7 @@ import { ENDPOINT_ACTION_RESPONSES_DS, ISOLATE_HOST_ROUTE, UNISOLATE_HOST_ROUTE, + failedFleetActionErrorCode, } from '../../../../common/endpoint/constants'; import { AGENT_ACTIONS_INDEX } from '../../../../../fleet/common'; import { @@ -33,6 +34,7 @@ import { getMetadataForEndpoints } from '../../services'; import { EndpointAppContext } from '../../types'; import { APP_ID } from '../../../../common/constants'; import { userCanIsolate } from '../../../../common/endpoint/actions'; +import { doLogsEndpointActionDsExists } from '../../utils'; /** * Registers the Host-(un-)isolation routes @@ -78,7 +80,7 @@ const createFailedActionResponseEntry = async ({ body: { ...doc, error: { - code: '424', + code: failedFleetActionErrorCode, message: 'Failed to deliver action request to fleet', }, }, @@ -88,31 +90,6 @@ const createFailedActionResponseEntry = async ({ } }; -const doLogsEndpointActionDsExists = async ({ - context, - logger, - dataStreamName, -}: { - context: SecuritySolutionRequestHandlerContext; - logger: Logger; - dataStreamName: string; -}): Promise => { - try { - const esClient = context.core.elasticsearch.client.asInternalUser; - const doesIndexTemplateExist = await esClient.indices.existsIndexTemplate({ - name: dataStreamName, - }); - return doesIndexTemplateExist.statusCode === 404 ? false : true; - } catch (error) { - const errorType = error?.type ?? ''; - if (errorType !== 'resource_not_found_exception') { - logger.error(error); - throw error; - } - return false; - } -}; - export const isolationRequestHandler = function ( endpointContext: EndpointAppContext, isolate: boolean diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/mocks.ts index 34f7d140a78de..b50d80a9bae71 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/mocks.ts @@ -13,11 +13,43 @@ import { ApiResponse } from '@elastic/elasticsearch'; import moment from 'moment'; import uuid from 'uuid'; import { + LogsEndpointAction, + LogsEndpointActionResponse, EndpointAction, EndpointActionResponse, ISOLATION_ACTIONS, } from '../../../../common/endpoint/types'; +export interface Results { + _index: string; + _source: + | LogsEndpointAction + | LogsEndpointActionResponse + | EndpointAction + | EndpointActionResponse; +} +export const mockAuditLogSearchResult = (results?: Results[]) => { + const response = { + body: { + hits: { + total: { value: results?.length ?? 0, relation: 'eq' }, + hits: + results?.map((a: Results) => ({ + _index: a._index, + _id: Math.random().toString(36).split('.')[1], + _score: 0.0, + _source: a._source, + })) ?? [], + }, + }, + statusCode: 200, + headers: {}, + warnings: [], + meta: {} as any, + }; + return response; +}; + export const mockSearchResult = (results: any = []): ApiResponse => { return { body: { diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions.ts index 711d78ba51b59..d59ecb674196c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions.ts @@ -6,15 +6,28 @@ */ import { ElasticsearchClient, Logger } from 'kibana/server'; +import { SearchHit, SearchResponse } from '@elastic/elasticsearch/api/types'; +import { ApiResponse } from '@elastic/elasticsearch'; import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '../../../../fleet/common'; import { SecuritySolutionRequestHandlerContext } from '../../types'; import { ActivityLog, + ActivityLogEntry, EndpointAction, + LogsEndpointAction, EndpointActionResponse, EndpointPendingActions, + LogsEndpointActionResponse, } from '../../../common/endpoint/types'; -import { catchAndWrapError } from '../utils'; +import { + catchAndWrapError, + categorizeActionResults, + categorizeResponseResults, + getActionRequestsResult, + getActionResponsesResult, + getTimeSortedData, + getUniqueLogData, +} from '../utils'; import { EndpointMetadataService } from './metadata'; const PENDING_ACTION_RESPONSE_MAX_LAPSED_TIME = 300000; // 300k ms === 5 minutes @@ -38,9 +51,9 @@ export const getAuditLogResponse = async ({ }): Promise => { const size = Math.floor(pageSize / 2); const from = page <= 1 ? 0 : page * size - size + 1; - const esClient = context.core.elasticsearch.client.asCurrentUser; + const data = await getActivityLog({ - esClient, + context, from, size, startDate, @@ -59,7 +72,7 @@ export const getAuditLogResponse = async ({ }; const getActivityLog = async ({ - esClient, + context, size, from, startDate, @@ -67,83 +80,39 @@ const getActivityLog = async ({ elasticAgentId, logger, }: { - esClient: ElasticsearchClient; + context: SecuritySolutionRequestHandlerContext; elasticAgentId: string; size: number; from: number; startDate: string; endDate: string; logger: Logger; -}) => { - const options = { - headers: { - 'X-elastic-product-origin': 'fleet', - }, - ignore: [404], - }; - - let actionsResult; - let responsesResult; - const dateFilters = [ - { range: { '@timestamp': { gte: startDate } } }, - { range: { '@timestamp': { lte: endDate } } }, - ]; +}): Promise => { + let actionsResult: ApiResponse, unknown>; + let responsesResult: ApiResponse, unknown>; try { // fetch actions with matching agent_id - const baseActionFilters = [ - { term: { agents: elasticAgentId } }, - { term: { input_type: 'endpoint' } }, - { term: { type: 'INPUT_ACTION' } }, - ]; - const actionsFilters = [...baseActionFilters, ...dateFilters]; - actionsResult = await esClient.search( - { - index: AGENT_ACTIONS_INDEX, - size, - from, - body: { - query: { - bool: { - // @ts-ignore - filter: actionsFilters, - }, - }, - sort: [ - { - '@timestamp': { - order: 'desc', - }, - }, - ], - }, - }, - options - ); - const actionIds = actionsResult?.body?.hits?.hits?.map( - (e) => (e._source as EndpointAction).action_id - ); + const { actionIds, actionRequests } = await getActionRequestsResult({ + context, + logger, + elasticAgentId, + startDate, + endDate, + size, + from, + }); + actionsResult = actionRequests; - // fetch responses with matching `action_id`s - const baseResponsesFilter = [ - { term: { agent_id: elasticAgentId } }, - { terms: { action_id: actionIds } }, - ]; - const responsesFilters = [...baseResponsesFilter, ...dateFilters]; - responsesResult = await esClient.search( - { - index: AGENT_ACTIONS_RESULTS_INDEX, - size: 1000, - body: { - query: { - bool: { - filter: responsesFilters, - }, - }, - }, - }, - options - ); + // fetch responses with matching unique set of `action_id`s + responsesResult = await getActionResponsesResult({ + actionIds: [...new Set(actionIds)], // de-dupe `action_id`s + context, + logger, + elasticAgentId, + startDate, + endDate, + }); } catch (error) { logger.error(error); throw error; @@ -153,21 +122,26 @@ const getActivityLog = async ({ throw new Error(`Error fetching actions log for agent_id ${elasticAgentId}`); } - const responses = responsesResult?.body?.hits?.hits?.length - ? responsesResult?.body?.hits?.hits?.map((e) => ({ - type: 'response', - item: { id: e._id, data: e._source }, - })) - : []; - const actions = actionsResult?.body?.hits?.hits?.length - ? actionsResult?.body?.hits?.hits?.map((e) => ({ - type: 'action', - item: { id: e._id, data: e._source }, - })) - : []; - const sortedData = ([...responses, ...actions] as ActivityLog['data']).sort((a, b) => - new Date(b.item.data['@timestamp']) > new Date(a.item.data['@timestamp']) ? 1 : -1 - ); + // label record as `action`, `fleetAction` + const responses = categorizeResponseResults({ + results: responsesResult?.body?.hits?.hits as Array< + SearchHit + >, + }); + + // label record as `response`, `fleetResponse` + const actions = categorizeActionResults({ + results: actionsResult?.body?.hits?.hits as Array< + SearchHit + >, + }); + + // filter out the duplicate endpoint actions that also have fleetActions + // include endpoint actions that have no fleet actions + const uniqueLogData = getUniqueLogData([...responses, ...actions]); + + // sort by @timestamp in desc order, newest first + const sortedData = getTimeSortedData(uniqueLogData); return sortedData; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/audit_log_helpers.ts b/x-pack/plugins/security_solution/server/endpoint/utils/audit_log_helpers.ts new file mode 100644 index 0000000000000..f75b265bf24d7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/utils/audit_log_helpers.ts @@ -0,0 +1,266 @@ +/* + * 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 { Logger } from 'kibana/server'; +import { SearchRequest } from 'src/plugins/data/public'; +import { SearchHit, SearchResponse } from '@elastic/elasticsearch/api/types'; +import { ApiResponse } from '@elastic/elasticsearch'; +import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '../../../../fleet/common'; +import { + ENDPOINT_ACTIONS_INDEX, + ENDPOINT_ACTION_RESPONSES_INDEX, + failedFleetActionErrorCode, +} from '../../../common/endpoint/constants'; +import { SecuritySolutionRequestHandlerContext } from '../../types'; +import { + ActivityLog, + ActivityLogAction, + EndpointActivityLogAction, + ActivityLogActionResponse, + EndpointActivityLogActionResponse, + ActivityLogItemTypes, + EndpointAction, + LogsEndpointAction, + EndpointActionResponse, + LogsEndpointActionResponse, + ActivityLogEntry, +} from '../../../common/endpoint/types'; +import { doesLogsEndpointActionsIndexExist } from '../utils'; + +const actionsIndices = [AGENT_ACTIONS_INDEX, ENDPOINT_ACTIONS_INDEX]; +const responseIndices = [AGENT_ACTIONS_RESULTS_INDEX, ENDPOINT_ACTION_RESPONSES_INDEX]; +export const logsEndpointActionsRegex = new RegExp(`(^\.ds-\.logs-endpoint\.actions-default-).+`); +export const logsEndpointResponsesRegex = new RegExp( + `(^\.ds-\.logs-endpoint\.action\.responses-default-).+` +); +const queryOptions = { + headers: { + 'X-elastic-product-origin': 'fleet', + }, + ignore: [404], +}; + +const getDateFilters = ({ startDate, endDate }: { startDate: string; endDate: string }) => { + return [ + { range: { '@timestamp': { gte: startDate } } }, + { range: { '@timestamp': { lte: endDate } } }, + ]; +}; + +export const getUniqueLogData = (activityLogEntries: ActivityLogEntry[]): ActivityLogEntry[] => { + // find the error responses for actions that didn't make it to fleet index + const onlyResponsesForFleetErrors = activityLogEntries + .filter( + (e) => + e.type === ActivityLogItemTypes.RESPONSE && + e.item.data.error?.code === failedFleetActionErrorCode + ) + .map( + (e: ActivityLogEntry) => (e.item.data as LogsEndpointActionResponse).EndpointActions.action_id + ); + + // all actions and responses minus endpoint actions. + const nonEndpointActionsDocs = activityLogEntries.filter( + (e) => e.type !== ActivityLogItemTypes.ACTION + ); + + // only endpoint actions that match the error responses + const onlyEndpointActionsDocWithoutFleetActions = activityLogEntries + .filter((e) => e.type === ActivityLogItemTypes.ACTION) + .filter((e: ActivityLogEntry) => + onlyResponsesForFleetErrors.includes( + (e.item.data as LogsEndpointAction).EndpointActions.action_id + ) + ); + + // join the error actions and the rest + return [...nonEndpointActionsDocs, ...onlyEndpointActionsDocWithoutFleetActions]; +}; + +export const categorizeResponseResults = ({ + results, +}: { + results: Array>; +}): Array => { + return results?.length + ? results?.map((e) => { + const isResponseDoc: boolean = matchesIndexPattern({ + regexPattern: logsEndpointResponsesRegex, + index: e._index, + }); + return isResponseDoc + ? { + type: ActivityLogItemTypes.RESPONSE, + item: { id: e._id, data: e._source as LogsEndpointActionResponse }, + } + : { + type: ActivityLogItemTypes.FLEET_RESPONSE, + item: { id: e._id, data: e._source as EndpointActionResponse }, + }; + }) + : []; +}; + +export const categorizeActionResults = ({ + results, +}: { + results: Array>; +}): Array => { + return results?.length + ? results?.map((e) => { + const isActionDoc: boolean = matchesIndexPattern({ + regexPattern: logsEndpointActionsRegex, + index: e._index, + }); + return isActionDoc + ? { + type: ActivityLogItemTypes.ACTION, + item: { id: e._id, data: e._source as LogsEndpointAction }, + } + : { + type: ActivityLogItemTypes.FLEET_ACTION, + item: { id: e._id, data: e._source as EndpointAction }, + }; + }) + : []; +}; + +export const getTimeSortedData = (data: ActivityLog['data']): ActivityLog['data'] => { + return data.sort((a, b) => + new Date(b.item.data['@timestamp']) > new Date(a.item.data['@timestamp']) ? 1 : -1 + ); +}; + +export const getActionRequestsResult = async ({ + context, + logger, + elasticAgentId, + startDate, + endDate, + size, + from, +}: { + context: SecuritySolutionRequestHandlerContext; + logger: Logger; + elasticAgentId: string; + startDate: string; + endDate: string; + size: number; + from: number; +}): Promise<{ + actionIds: string[]; + actionRequests: ApiResponse, unknown>; +}> => { + const dateFilters = getDateFilters({ startDate, endDate }); + const baseActionFilters = [ + { term: { agents: elasticAgentId } }, + { term: { input_type: 'endpoint' } }, + { term: { type: 'INPUT_ACTION' } }, + ]; + const actionsFilters = [...baseActionFilters, ...dateFilters]; + + const hasLogsEndpointActionsIndex = await doesLogsEndpointActionsIndexExist({ + context, + logger, + indexName: ENDPOINT_ACTIONS_INDEX, + }); + + const actionsSearchQuery: SearchRequest = { + index: hasLogsEndpointActionsIndex ? actionsIndices : AGENT_ACTIONS_INDEX, + size, + from, + body: { + query: { + bool: { + filter: actionsFilters, + }, + }, + sort: [ + { + '@timestamp': { + order: 'desc', + }, + }, + ], + }, + }; + + let actionRequests: ApiResponse, unknown>; + try { + const esClient = context.core.elasticsearch.client.asCurrentUser; + actionRequests = await esClient.search(actionsSearchQuery, queryOptions); + const actionIds = actionRequests?.body?.hits?.hits?.map((e) => { + return logsEndpointActionsRegex.test(e._index) + ? (e._source as LogsEndpointAction).EndpointActions.action_id + : (e._source as EndpointAction).action_id; + }); + + return { actionIds, actionRequests }; + } catch (error) { + logger.error(error); + throw error; + } +}; + +export const getActionResponsesResult = async ({ + context, + logger, + elasticAgentId, + actionIds, + startDate, + endDate, +}: { + context: SecuritySolutionRequestHandlerContext; + logger: Logger; + elasticAgentId: string; + actionIds: string[]; + startDate: string; + endDate: string; +}): Promise, unknown>> => { + const dateFilters = getDateFilters({ startDate, endDate }); + const baseResponsesFilter = [ + { term: { agent_id: elasticAgentId } }, + { terms: { action_id: actionIds } }, + ]; + const responsesFilters = [...baseResponsesFilter, ...dateFilters]; + + const hasLogsEndpointActionResponsesIndex = await doesLogsEndpointActionsIndexExist({ + context, + logger, + indexName: ENDPOINT_ACTION_RESPONSES_INDEX, + }); + + const responsesSearchQuery: SearchRequest = { + index: hasLogsEndpointActionResponsesIndex ? responseIndices : AGENT_ACTIONS_RESULTS_INDEX, + size: 1000, + body: { + query: { + bool: { + filter: responsesFilters, + }, + }, + }, + }; + + let actionResponses: ApiResponse, unknown>; + try { + const esClient = context.core.elasticsearch.client.asCurrentUser; + actionResponses = await esClient.search(responsesSearchQuery, queryOptions); + } catch (error) { + logger.error(error); + throw error; + } + return actionResponses; +}; + +const matchesIndexPattern = ({ + regexPattern, + index, +}: { + regexPattern: RegExp; + index: string; +}): boolean => regexPattern.test(index); diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/index.ts b/x-pack/plugins/security_solution/server/endpoint/utils/index.ts index 34cabf79aff0e..6c40073f8c654 100644 --- a/x-pack/plugins/security_solution/server/endpoint/utils/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/utils/index.ts @@ -7,3 +7,5 @@ export * from './fleet_agent_status_to_endpoint_host_status'; export * from './wrap_errors'; +export * from './audit_log_helpers'; +export * from './yes_no_data_stream'; diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.test.ts b/x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.test.ts new file mode 100644 index 0000000000000..d2894c8c64c14 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.test.ts @@ -0,0 +1,100 @@ +/* + * 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 { + elasticsearchServiceMock, + savedObjectsClientMock, + loggingSystemMock, +} from 'src/core/server/mocks'; +import { SecuritySolutionRequestHandlerContext } from '../../types'; +import { createRouteHandlerContext } from '../mocks'; +import { + doLogsEndpointActionDsExists, + doesLogsEndpointActionsIndexExist, +} from './yes_no_data_stream'; + +describe('Accurately answers if index template for data stream exists', () => { + let ctxt: jest.Mocked; + + beforeEach(() => { + ctxt = createRouteHandlerContext( + elasticsearchServiceMock.createScopedClusterClient(), + savedObjectsClientMock.create() + ); + }); + + const mockEsApiResponse = (response: { body: boolean; statusCode: number }) => { + return jest.fn().mockImplementationOnce(() => Promise.resolve(response)); + }; + + it('Returns FALSE for a non-existent data stream index template', async () => { + ctxt.core.elasticsearch.client.asInternalUser.indices.existsIndexTemplate = mockEsApiResponse({ + body: false, + statusCode: 404, + }); + const doesItExist = await doLogsEndpointActionDsExists({ + context: ctxt, + logger: loggingSystemMock.create().get('host-isolation'), + dataStreamName: '.test-stream.name', + }); + expect(doesItExist).toBeFalsy(); + }); + + it('Returns TRUE for an existing index', async () => { + ctxt.core.elasticsearch.client.asInternalUser.indices.existsIndexTemplate = mockEsApiResponse({ + body: true, + statusCode: 200, + }); + const doesItExist = await doLogsEndpointActionDsExists({ + context: ctxt, + logger: loggingSystemMock.create().get('host-isolation'), + dataStreamName: '.test-stream.name', + }); + expect(doesItExist).toBeTruthy(); + }); +}); + +describe('Accurately answers if index exists', () => { + let ctxt: jest.Mocked; + + beforeEach(() => { + ctxt = createRouteHandlerContext( + elasticsearchServiceMock.createScopedClusterClient(), + savedObjectsClientMock.create() + ); + }); + + const mockEsApiResponse = (response: { body: boolean; statusCode: number }) => { + return jest.fn().mockImplementationOnce(() => Promise.resolve(response)); + }; + + it('Returns FALSE for a non-existent index', async () => { + ctxt.core.elasticsearch.client.asInternalUser.indices.exists = mockEsApiResponse({ + body: false, + statusCode: 404, + }); + const doesItExist = await doesLogsEndpointActionsIndexExist({ + context: ctxt, + logger: loggingSystemMock.create().get('host-isolation'), + indexName: '.test-index.name-default', + }); + expect(doesItExist).toBeFalsy(); + }); + + it('Returns TRUE for an existing index', async () => { + ctxt.core.elasticsearch.client.asInternalUser.indices.exists = mockEsApiResponse({ + body: true, + statusCode: 200, + }); + const doesItExist = await doesLogsEndpointActionsIndexExist({ + context: ctxt, + logger: loggingSystemMock.create().get('host-isolation'), + indexName: '.test-index.name-default', + }); + expect(doesItExist).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.ts b/x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.ts new file mode 100644 index 0000000000000..dea2e46c3c258 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.ts @@ -0,0 +1,59 @@ +/* + * 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 { Logger } from 'src/core/server'; +import { SecuritySolutionRequestHandlerContext } from '../../types'; + +export const doLogsEndpointActionDsExists = async ({ + context, + logger, + dataStreamName, +}: { + context: SecuritySolutionRequestHandlerContext; + logger: Logger; + dataStreamName: string; +}): Promise => { + try { + const esClient = context.core.elasticsearch.client.asInternalUser; + const doesIndexTemplateExist = await esClient.indices.existsIndexTemplate({ + name: dataStreamName, + }); + return doesIndexTemplateExist.statusCode === 404 ? false : true; + } catch (error) { + const errorType = error?.type ?? ''; + if (errorType !== 'resource_not_found_exception') { + logger.error(error); + throw error; + } + return false; + } +}; + +export const doesLogsEndpointActionsIndexExist = async ({ + context, + logger, + indexName, +}: { + context: SecuritySolutionRequestHandlerContext; + logger: Logger; + indexName: string; +}): Promise => { + try { + const esClient = context.core.elasticsearch.client.asInternalUser; + const doesIndexExist = await esClient.indices.exists({ + index: indexName, + }); + return doesIndexExist.statusCode === 404 ? false : true; + } catch (error) { + const errorType = error?.type ?? ''; + if (errorType !== 'index_not_found_exception') { + logger.error(error); + throw error; + } + return false; + } +}; From ec3809658fdc0850106c6e2672ae46c3f6621d96 Mon Sep 17 00:00:00 2001 From: Caroline Horn <549577+cchaos@users.noreply.github.com> Date: Mon, 18 Oct 2021 23:56:00 -0400 Subject: [PATCH 2/6] [Unified Integrations] Clean up empty states, tutorial links and routing to prefer unified integrations (#114911) Cleans up the integrations view and redirects all links to the integration manager. --- .../chrome/ui/header/collapsible_nav.tsx | 2 +- .../__snapshots__/add_data.test.tsx.snap | 16 +- .../components/add_data/add_data.test.tsx | 4 +- .../components/add_data/add_data.tsx | 152 +++++++++--------- .../components/sample_data/index.tsx | 4 +- .../components/tutorial_directory.js | 56 +------ .../public/application/components/welcome.tsx | 3 +- src/plugins/home/public/index.ts | 1 - src/plugins/home/public/services/index.ts | 1 - .../home/public/services/tutorials/index.ts | 1 - .../tutorials/tutorial_service.mock.ts | 1 - .../tutorials/tutorial_service.test.tsx | 32 ---- .../services/tutorials/tutorial_service.ts | 18 --- .../empty_index_list_prompt.tsx | 2 +- .../__snapshots__/overview.test.tsx.snap | 110 +++---------- .../public/components/overview/overview.tsx | 12 +- .../public/assets/elastic_beats_card_dark.svg | 1 - .../assets/elastic_beats_card_light.svg | 1 - .../__snapshots__/no_data_page.test.tsx.snap | 4 +- .../elastic_agent_card.test.tsx.snap | 55 ++++++- .../elastic_beats_card.test.tsx.snap | 70 -------- .../no_data_card/elastic_agent_card.test.tsx | 10 +- .../no_data_card/elastic_agent_card.tsx | 44 ++++- .../no_data_card/elastic_beats_card.test.tsx | 45 ------ .../no_data_card/elastic_beats_card.tsx | 66 -------- .../no_data_page/no_data_card/index.ts | 1 - .../no_data_page/no_data_page.tsx | 14 +- .../components/app/RumDashboard/RumHome.tsx | 8 +- .../routing/templates/no_data_config.ts | 10 +- .../epm/components/package_list_grid.tsx | 2 +- .../components/home_integration/index.tsx | 8 - .../tutorial_directory_header_link.tsx | 16 +- .../tutorial_directory_notice.tsx | 147 ----------------- x-pack/plugins/fleet/public/plugin.ts | 7 +- .../infra/public/pages/logs/page_content.tsx | 2 +- .../infra/public/pages/logs/page_template.tsx | 6 +- .../logs/stream/page_no_indices_content.tsx | 4 +- .../infra/public/pages/metrics/index.tsx | 4 +- .../metric_detail/components/invalid_node.tsx | 4 +- .../public/pages/metrics/page_template.tsx | 9 +- .../components/app/header/header_menu.tsx | 2 +- .../public/utils/no_data_config.ts | 7 +- .../security_solution/common/constants.ts | 2 +- .../components/overview_empty/index.test.tsx | 12 +- .../components/overview_empty/index.tsx | 50 ++---- .../translations/translations/ja-JP.json | 11 -- .../translations/translations/zh-CN.json | 11 -- x-pack/test/accessibility/apps/home.ts | 27 ---- 48 files changed, 292 insertions(+), 783 deletions(-) delete mode 100644 src/plugins/kibana_react/public/assets/elastic_beats_card_dark.svg delete mode 100644 src/plugins/kibana_react/public/assets/elastic_beats_card_light.svg delete mode 100644 src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_beats_card.test.tsx.snap delete mode 100644 src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.test.tsx delete mode 100644 src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.tsx delete mode 100644 x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_notice.tsx diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx index ad590865b9e14..ccc0e17b655b1 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -362,7 +362,7 @@ export function CollapsibleNav({ iconType="plusInCircleFilled" > {i18n.translate('core.ui.primaryNav.addData', { - defaultMessage: 'Add data', + defaultMessage: 'Add integrations', })} diff --git a/src/plugins/home/public/application/components/add_data/__snapshots__/add_data.test.tsx.snap b/src/plugins/home/public/application/components/add_data/__snapshots__/add_data.test.tsx.snap index 26b5697f008b6..de6beab31247a 100644 --- a/src/plugins/home/public/application/components/add_data/__snapshots__/add_data.test.tsx.snap +++ b/src/plugins/home/public/application/components/add_data/__snapshots__/add_data.test.tsx.snap @@ -17,7 +17,7 @@ exports[`AddData render 1`] = ` id="homDataAdd__title" > @@ -43,17 +43,25 @@ exports[`AddData render 1`] = ` grow={false} > diff --git a/src/plugins/home/public/application/components/add_data/add_data.test.tsx b/src/plugins/home/public/application/components/add_data/add_data.test.tsx index 4018ae67c19ee..3aa51f89c7d67 100644 --- a/src/plugins/home/public/application/components/add_data/add_data.test.tsx +++ b/src/plugins/home/public/application/components/add_data/add_data.test.tsx @@ -27,7 +27,9 @@ beforeEach(() => { jest.clearAllMocks(); }); -const applicationStartMock = {} as unknown as ApplicationStart; +const applicationStartMock = { + capabilities: { navLinks: { integrations: true } }, +} as unknown as ApplicationStart; const addBasePathMock = jest.fn((path: string) => (path ? path : 'path')); diff --git a/src/plugins/home/public/application/components/add_data/add_data.tsx b/src/plugins/home/public/application/components/add_data/add_data.tsx index 97ba28a04a07e..50d6079dd8df3 100644 --- a/src/plugins/home/public/application/components/add_data/add_data.tsx +++ b/src/plugins/home/public/application/components/add_data/add_data.tsx @@ -22,8 +22,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { METRIC_TYPE } from '@kbn/analytics'; import { ApplicationStart } from 'kibana/public'; import { createAppNavigationHandler } from '../app_navigation_handler'; -// @ts-expect-error untyped component -import { Synopsis } from '../synopsis'; import { getServices } from '../../kibana_services'; import { RedirectAppLinks } from '../../../../../kibana_react/public'; @@ -35,87 +33,91 @@ interface Props { export const AddData: FC = ({ addBasePath, application, isDarkMode }) => { const { trackUiMetric } = getServices(); + const canAccessIntegrations = application.capabilities.navLinks.integrations; + if (canAccessIntegrations) { + return ( + <> +
+ + + +

+ +

+
- return ( - <> -
- - - -

- -

-
+ - + +

+ +

+
- -

- -

-
+ - + + + + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + { + trackUiMetric(METRIC_TYPE.CLICK, 'home_tutorial_directory'); + createAppNavigationHandler('/app/integrations/browse')(event); + }} + > + + + + - - - - {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} - { - trackUiMetric(METRIC_TYPE.CLICK, 'home_tutorial_directory'); - createAppNavigationHandler('/app/home#/tutorial_directory')(event); - }} + + - - - - - - - - - - -
+ + +
+ - - - - -
+ + + +
+
- - - ); + + + ); + } else { + return null; + } }; diff --git a/src/plugins/home/public/application/components/sample_data/index.tsx b/src/plugins/home/public/application/components/sample_data/index.tsx index d6b9328f57e9b..b65fbb5d002b0 100644 --- a/src/plugins/home/public/application/components/sample_data/index.tsx +++ b/src/plugins/home/public/application/components/sample_data/index.tsx @@ -40,7 +40,7 @@ export function SampleDataCard({ urlBasePath, onDecline, onConfirm }: Props) { image={cardGraphicURL} textAlign="left" title={ - + } description={ - + { - const notices = getServices().tutorialService.getDirectoryNotices(); - return notices.length ? ( - - {notices.map((DirectoryNotice, index) => ( - - - - ))} - - ) : null; - }; - renderHeaderLinks = () => { const headerLinks = getServices().tutorialService.getDirectoryHeaderLinks(); return headerLinks.length ? ( @@ -245,7 +203,6 @@ class TutorialDirectoryUi extends React.Component { render() { const headerLinks = this.renderHeaderLinks(); const tabs = this.getTabs(); - const notices = this.renderNotices(); return ( + ), tabs, rightSideItems: headerLinks ? [headerLinks] : [], }} > - {notices && ( - <> - {notices} - - - )} {this.renderTabContent()} ); diff --git a/src/plugins/home/public/application/components/welcome.tsx b/src/plugins/home/public/application/components/welcome.tsx index ca7e6874c75c2..03dff22c7b33f 100644 --- a/src/plugins/home/public/application/components/welcome.tsx +++ b/src/plugins/home/public/application/components/welcome.tsx @@ -48,8 +48,7 @@ export class Welcome extends React.Component { }; private redirecToAddData() { - const path = this.services.addBasePath('#/tutorial_directory'); - window.location.href = path; + this.services.application.navigateToApp('integrations', { path: '/browse' }); } private onSampleDataDecline = () => { diff --git a/src/plugins/home/public/index.ts b/src/plugins/home/public/index.ts index dd02bf65dd8b0..7abaf5d19f008 100644 --- a/src/plugins/home/public/index.ts +++ b/src/plugins/home/public/index.ts @@ -23,7 +23,6 @@ export type { FeatureCatalogueSolution, Environment, TutorialVariables, - TutorialDirectoryNoticeComponent, TutorialDirectoryHeaderLinkComponent, TutorialModuleNoticeComponent, } from './services'; diff --git a/src/plugins/home/public/services/index.ts b/src/plugins/home/public/services/index.ts index 65913df6310b1..2ee68a9eef0c2 100644 --- a/src/plugins/home/public/services/index.ts +++ b/src/plugins/home/public/services/index.ts @@ -22,7 +22,6 @@ export { TutorialService } from './tutorials'; export type { TutorialVariables, TutorialServiceSetup, - TutorialDirectoryNoticeComponent, TutorialDirectoryHeaderLinkComponent, TutorialModuleNoticeComponent, } from './tutorials'; diff --git a/src/plugins/home/public/services/tutorials/index.ts b/src/plugins/home/public/services/tutorials/index.ts index 8de12c31249d8..e007a5ea4d552 100644 --- a/src/plugins/home/public/services/tutorials/index.ts +++ b/src/plugins/home/public/services/tutorials/index.ts @@ -11,7 +11,6 @@ export { TutorialService } from './tutorial_service'; export type { TutorialVariables, TutorialServiceSetup, - TutorialDirectoryNoticeComponent, TutorialDirectoryHeaderLinkComponent, TutorialModuleNoticeComponent, } from './tutorial_service'; diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts b/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts index 0c109d61912ca..ab38a32a1a5b3 100644 --- a/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts +++ b/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts @@ -25,7 +25,6 @@ const createMock = (): jest.Mocked> => { const service = { setup: jest.fn(), getVariables: jest.fn(() => ({})), - getDirectoryNotices: jest.fn(() => []), getDirectoryHeaderLinks: jest.fn(() => []), getModuleNotices: jest.fn(() => []), getCustomStatusCheck: jest.fn(), diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx b/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx index a88cf526e3716..b90165aafb45f 100644 --- a/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx +++ b/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx @@ -27,22 +27,6 @@ describe('TutorialService', () => { }).toThrow(); }); - test('allows multiple register directory notice calls', () => { - const setup = new TutorialService().setup(); - expect(() => { - setup.registerDirectoryNotice('abc', () =>
); - setup.registerDirectoryNotice('def', () => ); - }).not.toThrow(); - }); - - test('throws when same directory notice is registered twice', () => { - const setup = new TutorialService().setup(); - expect(() => { - setup.registerDirectoryNotice('abc', () =>
); - setup.registerDirectoryNotice('abc', () => ); - }).toThrow(); - }); - test('allows multiple register directory header link calls', () => { const setup = new TutorialService().setup(); expect(() => { @@ -91,22 +75,6 @@ describe('TutorialService', () => { }); }); - describe('getDirectoryNotices', () => { - test('returns empty array', () => { - const service = new TutorialService(); - expect(service.getDirectoryNotices()).toEqual([]); - }); - - test('returns last state of register calls', () => { - const service = new TutorialService(); - const setup = service.setup(); - const notices = [() =>
, () => ]; - setup.registerDirectoryNotice('abc', notices[0]); - setup.registerDirectoryNotice('def', notices[1]); - expect(service.getDirectoryNotices()).toEqual(notices); - }); - }); - describe('getDirectoryHeaderLinks', () => { test('returns empty array', () => { const service = new TutorialService(); diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.ts b/src/plugins/home/public/services/tutorials/tutorial_service.ts index 839b0702a499e..81b6bbe72e3e9 100644 --- a/src/plugins/home/public/services/tutorials/tutorial_service.ts +++ b/src/plugins/home/public/services/tutorials/tutorial_service.ts @@ -11,9 +11,6 @@ import React from 'react'; /** @public */ export type TutorialVariables = Partial>; -/** @public */ -export type TutorialDirectoryNoticeComponent = React.FC; - /** @public */ export type TutorialDirectoryHeaderLinkComponent = React.FC; @@ -27,7 +24,6 @@ type CustomComponent = () => Promise; export class TutorialService { private tutorialVariables: TutorialVariables = {}; - private tutorialDirectoryNotices: { [key: string]: TutorialDirectoryNoticeComponent } = {}; private tutorialDirectoryHeaderLinks: { [key: string]: TutorialDirectoryHeaderLinkComponent; } = {}; @@ -47,16 +43,6 @@ export class TutorialService { this.tutorialVariables[key] = value; }, - /** - * Registers a component that will be rendered at the top of tutorial directory page. - */ - registerDirectoryNotice: (id: string, component: TutorialDirectoryNoticeComponent) => { - if (this.tutorialDirectoryNotices[id]) { - throw new Error(`directory notice ${id} already set`); - } - this.tutorialDirectoryNotices[id] = component; - }, - /** * Registers a component that will be rendered next to tutorial directory title/header area. */ @@ -94,10 +80,6 @@ export class TutorialService { return this.tutorialVariables; } - public getDirectoryNotices() { - return Object.values(this.tutorialDirectoryNotices); - } - public getDirectoryHeaderLinks() { return Object.values(this.tutorialDirectoryHeaderLinks); } diff --git a/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_index_list_prompt/empty_index_list_prompt.tsx b/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_index_list_prompt/empty_index_list_prompt.tsx index 1331eb9b7c4ac..d00f9e2368e21 100644 --- a/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_index_list_prompt/empty_index_list_prompt.tsx +++ b/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_index_list_prompt/empty_index_list_prompt.tsx @@ -91,7 +91,7 @@ export const EmptyIndexListPrompt = ({ { - navigateToApp('home', { path: '#/tutorial_directory' }); + navigateToApp('home', { path: '/app/integrations/browse' }); closeFlyout(); }} icon={} diff --git a/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap b/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap index 6da2f95fa394d..babcab15a4974 100644 --- a/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap +++ b/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap @@ -226,10 +226,7 @@ exports[`Overview render 1`] = ` [MockFunction] { "calls": Array [ Array [ - "/app/home#/tutorial_directory", - ], - Array [ - "home#/tutorial_directory", + "/app/integrations/browse", ], Array [ "kibana_landing_page", @@ -259,11 +256,7 @@ exports[`Overview render 1`] = ` "results": Array [ Object { "type": "return", - "value": "/app/home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "home#/tutorial_directory", + "value": "/app/integrations/browse", }, Object { "type": "return", @@ -533,10 +526,7 @@ exports[`Overview without features 1`] = ` [MockFunction] { "calls": Array [ Array [ - "/app/home#/tutorial_directory", - ], - Array [ - "home#/tutorial_directory", + "/app/integrations/browse", ], Array [ "kibana_landing_page", @@ -563,16 +553,10 @@ exports[`Overview without features 1`] = ` "/plugins/kibanaReact/assets/solutions_solution_4.svg", ], Array [ - "/app/home#/tutorial_directory", + "/app/integrations/browse", ], Array [ - "home#/tutorial_directory", - ], - Array [ - "/app/home#/tutorial_directory", - ], - Array [ - "home#/tutorial_directory", + "/app/integrations/browse", ], Array [ "kibana_landing_page", @@ -602,11 +586,7 @@ exports[`Overview without features 1`] = ` "results": Array [ Object { "type": "return", - "value": "/app/home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "home#/tutorial_directory", + "value": "/app/integrations/browse", }, Object { "type": "return", @@ -642,19 +622,11 @@ exports[`Overview without features 1`] = ` }, Object { "type": "return", - "value": "/app/home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "/app/home#/tutorial_directory", + "value": "/app/integrations/browse", }, Object { "type": "return", - "value": "home#/tutorial_directory", + "value": "/app/integrations/browse", }, Object { "type": "return", @@ -801,10 +773,7 @@ exports[`Overview without solutions 1`] = ` [MockFunction] { "calls": Array [ Array [ - "/app/home#/tutorial_directory", - ], - Array [ - "home#/tutorial_directory", + "/app/integrations/browse", ], Array [ "kibana_landing_page", @@ -831,20 +800,13 @@ exports[`Overview without solutions 1`] = ` "/plugins/kibanaReact/assets/solutions_solution_4.svg", ], Array [ - "/app/home#/tutorial_directory", - ], - Array [ - "home#/tutorial_directory", + "/app/integrations/browse", ], ], "results": Array [ Object { "type": "return", - "value": "/app/home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "home#/tutorial_directory", + "value": "/app/integrations/browse", }, Object { "type": "return", @@ -880,11 +842,7 @@ exports[`Overview without solutions 1`] = ` }, Object { "type": "return", - "value": "/app/home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "home#/tutorial_directory", + "value": "/app/integrations/browse", }, ], } @@ -898,10 +856,7 @@ exports[`Overview without solutions 1`] = ` [MockFunction] { "calls": Array [ Array [ - "/app/home#/tutorial_directory", - ], - Array [ - "home#/tutorial_directory", + "/app/integrations/browse", ], Array [ "kibana_landing_page", @@ -928,20 +883,13 @@ exports[`Overview without solutions 1`] = ` "/plugins/kibanaReact/assets/solutions_solution_4.svg", ], Array [ - "/app/home#/tutorial_directory", - ], - Array [ - "home#/tutorial_directory", + "/app/integrations/browse", ], ], "results": Array [ Object { "type": "return", - "value": "/app/home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "home#/tutorial_directory", + "value": "/app/integrations/browse", }, Object { "type": "return", @@ -977,11 +925,7 @@ exports[`Overview without solutions 1`] = ` }, Object { "type": "return", - "value": "/app/home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "home#/tutorial_directory", + "value": "/app/integrations/browse", }, ], } @@ -1001,10 +945,7 @@ exports[`Overview without solutions 1`] = ` [MockFunction] { "calls": Array [ Array [ - "/app/home#/tutorial_directory", - ], - Array [ - "home#/tutorial_directory", + "/app/integrations/browse", ], Array [ "kibana_landing_page", @@ -1031,20 +972,13 @@ exports[`Overview without solutions 1`] = ` "/plugins/kibanaReact/assets/solutions_solution_4.svg", ], Array [ - "/app/home#/tutorial_directory", - ], - Array [ - "home#/tutorial_directory", + "/app/integrations/browse", ], ], "results": Array [ Object { "type": "return", - "value": "/app/home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "home#/tutorial_directory", + "value": "/app/integrations/browse", }, Object { "type": "return", @@ -1080,11 +1014,7 @@ exports[`Overview without solutions 1`] = ` }, Object { "type": "return", - "value": "/app/home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "home#/tutorial_directory", + "value": "/app/integrations/browse", }, ], } diff --git a/src/plugins/kibana_overview/public/components/overview/overview.tsx b/src/plugins/kibana_overview/public/components/overview/overview.tsx index 07769e2f3c474..6a0279bd12465 100644 --- a/src/plugins/kibana_overview/public/components/overview/overview.tsx +++ b/src/plugins/kibana_overview/public/components/overview/overview.tsx @@ -61,7 +61,7 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => const IS_DARK_THEME = uiSettings.get('theme:darkMode'); // Home does not have a locator implemented, so hard-code it here. - const addDataHref = addBasePath('/app/home#/tutorial_directory'); + const addDataHref = addBasePath('/app/integrations/browse'); const devToolsHref = share.url.locators.get('CONSOLE_APP_LOCATOR')?.useUrl({}); const managementHref = share.url.locators .get('MANAGEMENT_APP_LOCATOR') @@ -86,8 +86,14 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => }), logo: 'logoKibana', actions: { - beats: { - href: addBasePath(`home#/tutorial_directory`), + elasticAgent: { + title: i18n.translate('kibanaOverview.noDataConfig.title', { + defaultMessage: 'Add integrations', + }), + description: i18n.translate('kibanaOverview.noDataConfig.description', { + defaultMessage: + 'Use Elastic Agent or Beats to collect data and build out Analytics solutions.', + }), }, }, docsLink: docLinks.links.kibana, diff --git a/src/plugins/kibana_react/public/assets/elastic_beats_card_dark.svg b/src/plugins/kibana_react/public/assets/elastic_beats_card_dark.svg deleted file mode 100644 index 8652d8d921506..0000000000000 --- a/src/plugins/kibana_react/public/assets/elastic_beats_card_dark.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/plugins/kibana_react/public/assets/elastic_beats_card_light.svg b/src/plugins/kibana_react/public/assets/elastic_beats_card_light.svg deleted file mode 100644 index f54786c1b950c..0000000000000 --- a/src/plugins/kibana_react/public/assets/elastic_beats_card_light.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap b/src/plugins/kibana_react/public/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap index d8bc5745ec8e5..8842a3c9f5842 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap +++ b/src/plugins/kibana_react/public/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap @@ -73,9 +73,9 @@ exports[`NoDataPage render 1`] = ` - diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap index 3f72ae5597a98..f66d05140b2e9 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap @@ -13,7 +13,36 @@ exports[`ElasticAgentCard props button 1`] = ` href="/app/integrations/browse" image="/plugins/kibanaReact/assets/elastic_agent_card.svg" paddingSize="l" - title="Add Elastic Agent" + title={ + + + Add Elastic Agent + + + } +/> +`; + +exports[`ElasticAgentCard props category 1`] = ` + + Add Elastic Agent + + } + href="/app/integrations/browse/custom" + image="/plugins/kibanaReact/assets/elastic_agent_card.svg" + paddingSize="l" + title={ + + + Add Elastic Agent + + + } /> `; @@ -30,7 +59,13 @@ exports[`ElasticAgentCard props href 1`] = ` href="#" image="/plugins/kibanaReact/assets/elastic_agent_card.svg" paddingSize="l" - title="Add Elastic Agent" + title={ + + + Add Elastic Agent + + + } /> `; @@ -48,7 +83,13 @@ exports[`ElasticAgentCard props recommended 1`] = ` href="/app/integrations/browse" image="/plugins/kibanaReact/assets/elastic_agent_card.svg" paddingSize="l" - title="Add Elastic Agent" + title={ + + + Add Elastic Agent + + + } /> `; @@ -65,6 +106,12 @@ exports[`ElasticAgentCard renders 1`] = ` href="/app/integrations/browse" image="/plugins/kibanaReact/assets/elastic_agent_card.svg" paddingSize="l" - title="Add Elastic Agent" + title={ + + + Add Elastic Agent + + + } /> `; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_beats_card.test.tsx.snap b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_beats_card.test.tsx.snap deleted file mode 100644 index af26f9e93ebac..0000000000000 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_beats_card.test.tsx.snap +++ /dev/null @@ -1,70 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ElasticBeatsCard props button 1`] = ` - - Button - - } - href="/app/home#/tutorial_directory" - image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg" - paddingSize="l" - title="Add data" -/> -`; - -exports[`ElasticBeatsCard props href 1`] = ` - - Button - - } - href="#" - image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg" - paddingSize="l" - title="Add data" -/> -`; - -exports[`ElasticBeatsCard props recommended 1`] = ` - - Add data - - } - href="/app/home#/tutorial_directory" - image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg" - paddingSize="l" - title="Add data" -/> -`; - -exports[`ElasticBeatsCard renders 1`] = ` - - Add data - - } - href="/app/home#/tutorial_directory" - image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg" - paddingSize="l" - title="Add data" -/> -`; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.test.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.test.tsx index 45cc32cae06d6..b971abf06a437 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.test.tsx +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.test.tsx @@ -14,7 +14,10 @@ jest.mock('../../../context', () => ({ ...jest.requireActual('../../../context'), useKibana: jest.fn().mockReturnValue({ services: { - http: { basePath: { prepend: jest.fn((path: string) => (path ? path : 'path')) } }, + http: { + basePath: { prepend: jest.fn((path: string) => (path ? path : 'path')) }, + }, + application: { capabilities: { navLinks: { integrations: true } } }, uiSettings: { get: jest.fn() }, }, }), @@ -41,5 +44,10 @@ describe('ElasticAgentCard', () => { const component = shallow(); expect(component).toMatchSnapshot(); }); + + test('category', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); }); }); diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx index f071bd9fab25a..5a91e568471d1 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx @@ -9,7 +9,7 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; import { CoreStart } from 'kibana/public'; -import { EuiButton, EuiCard } from '@elastic/eui'; +import { EuiButton, EuiCard, EuiTextColor, EuiScreenReaderOnly } from '@elastic/eui'; import { useKibana } from '../../../context'; import { NoDataPageActions, NO_DATA_RECOMMENDED } from '../no_data_page'; @@ -27,13 +27,40 @@ export const ElasticAgentCard: FunctionComponent = ({ href, button, layout, + category, ...cardRest }) => { const { - services: { http }, + services: { http, application }, } = useKibana(); const addBasePath = http.basePath.prepend; - const basePathUrl = '/plugins/kibanaReact/assets/'; + const image = addBasePath(`/plugins/kibanaReact/assets/elastic_agent_card.svg`); + const canAccessFleet = application.capabilities.navLinks.integrations; + const hasCategory = category ? `/${category}` : ''; + + if (!canAccessFleet) { + return ( + + {i18n.translate('kibana-react.noDataPage.elasticAgentCard.noPermission.title', { + defaultMessage: `Contact your administrator`, + })} + + } + description={ + + {i18n.translate('kibana-react.noDataPage.elasticAgentCard.noPermission.description', { + defaultMessage: `This integration is not yet enabled. Your administrator has the required permissions to turn it on.`, + })} + + } + isDisabled + /> + ); + } const defaultCTAtitle = i18n.translate('kibana-react.noDataPage.elasticAgentCard.title', { defaultMessage: 'Add Elastic Agent', @@ -51,12 +78,17 @@ export const ElasticAgentCard: FunctionComponent = ({ return ( + {defaultCTAtitle} + + } description={i18n.translate('kibana-react.noDataPage.elasticAgentCard.description', { defaultMessage: `Use Elastic Agent for a simple, unified way to collect data from your machines.`, })} - image={addBasePath(`${basePathUrl}elastic_agent_card.svg`)} betaBadgeLabel={recommended ? NO_DATA_RECOMMENDED : undefined} footer={footer} layout={layout as 'vertical' | undefined} diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.test.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.test.tsx deleted file mode 100644 index 6ea41bf6b3e1f..0000000000000 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.test.tsx +++ /dev/null @@ -1,45 +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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { shallow } from 'enzyme'; -import React from 'react'; -import { ElasticBeatsCard } from './elastic_beats_card'; - -jest.mock('../../../context', () => ({ - ...jest.requireActual('../../../context'), - useKibana: jest.fn().mockReturnValue({ - services: { - http: { basePath: { prepend: jest.fn((path: string) => (path ? path : 'path')) } }, - uiSettings: { get: jest.fn() }, - }, - }), -})); - -describe('ElasticBeatsCard', () => { - test('renders', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); - }); - - describe('props', () => { - test('recommended', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); - }); - - test('button', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); - }); - - test('href', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); - }); - }); -}); diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.tsx deleted file mode 100644 index 0372d12096489..0000000000000 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.tsx +++ /dev/null @@ -1,66 +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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { FunctionComponent } from 'react'; -import { i18n } from '@kbn/i18n'; -import { CoreStart } from 'kibana/public'; -import { EuiButton, EuiCard } from '@elastic/eui'; -import { useKibana } from '../../../context'; -import { NoDataPageActions, NO_DATA_RECOMMENDED } from '../no_data_page'; - -export type ElasticBeatsCardProps = NoDataPageActions & { - solution: string; -}; - -export const ElasticBeatsCard: FunctionComponent = ({ - recommended, - title, - button, - href, - solution, // unused for now - layout, - ...cardRest -}) => { - const { - services: { http, uiSettings }, - } = useKibana(); - const addBasePath = http.basePath.prepend; - const basePathUrl = '/plugins/kibanaReact/assets/'; - const IS_DARK_THEME = uiSettings.get('theme:darkMode'); - - const defaultCTAtitle = i18n.translate('kibana-react.noDataPage.elasticBeatsCard.title', { - defaultMessage: 'Add data', - }); - - const footer = - typeof button !== 'string' && typeof button !== 'undefined' ? ( - button - ) : ( - // The href and/or onClick are attached to the whole Card, so the button is just for show. - // Do not add the behavior here too or else it will propogate through - {button || title || defaultCTAtitle} - ); - - return ( - - ); -}; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/index.ts b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/index.ts index 3744239d9a472..e05d4d9675ca9 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/index.ts +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/index.ts @@ -7,5 +7,4 @@ */ export * from './elastic_agent_card'; -export * from './elastic_beats_card'; export * from './no_data_card'; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx index 56eb0f34617d6..b2d9ef6ca5008 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx @@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { KibanaPageTemplateProps } from '../page_template'; -import { ElasticAgentCard, ElasticBeatsCard, NoDataCard } from './no_data_card'; +import { ElasticAgentCard, NoDataCard } from './no_data_card'; import { KibanaPageTemplateSolutionNavAvatar } from '../solution_nav'; export const NO_DATA_PAGE_MAX_WIDTH = 950; @@ -55,6 +55,10 @@ export type NoDataPageActions = Partial & { * Remapping `onClick` to any element */ onClick?: MouseEventHandler; + /** + * Category to auto-select within Fleet + */ + category?: string; }; export type NoDataPageActionsProps = Record; @@ -107,18 +111,12 @@ export const NoDataPage: FunctionComponent = ({ const actionsKeys = Object.keys(sortedData); const renderActions = useMemo(() => { return Object.values(sortedData).map((action, i) => { - if (actionsKeys[i] === 'elasticAgent') { + if (actionsKeys[i] === 'elasticAgent' || actionsKeys[i] === 'beats') { return ( ); - } else if (actionsKeys[i] === 'beats') { - return ( - - - - ); } else { return ( ), discussForumLink: ( - + import('./tutorial_directory_notice')); -export const TutorialDirectoryNotice: TutorialDirectoryNoticeComponent = () => ( - }> - - -); - const TutorialDirectoryHeaderLinkLazy = React.lazy( () => import('./tutorial_directory_header_link') ); diff --git a/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_header_link.tsx b/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_header_link.tsx index 074a1c40bdb19..18fdd875c7379 100644 --- a/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_header_link.tsx +++ b/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_header_link.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { memo, useState, useEffect } from 'react'; +import React, { memo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty } from '@elastic/eui'; import type { TutorialDirectoryHeaderLinkComponent } from 'src/plugins/home/public'; @@ -13,25 +13,15 @@ import type { TutorialDirectoryHeaderLinkComponent } from 'src/plugins/home/publ import { RedirectAppLinks } from '../../../../../../src/plugins/kibana_react/public'; import { useLink, useCapabilities, useStartServices } from '../../hooks'; -import { tutorialDirectoryNoticeState$ } from './tutorial_directory_notice'; - const TutorialDirectoryHeaderLink: TutorialDirectoryHeaderLinkComponent = memo(() => { const { getHref } = useLink(); const { application } = useStartServices(); const { show: hasIngestManager } = useCapabilities(); - const [noticeState, setNoticeState] = useState({ + const [noticeState] = useState({ settingsDataLoaded: false, - hasSeenNotice: false, }); - useEffect(() => { - const subscription = tutorialDirectoryNoticeState$.subscribe((value) => setNoticeState(value)); - return () => { - subscription.unsubscribe(); - }; - }, []); - - return hasIngestManager && noticeState.settingsDataLoaded && noticeState.hasSeenNotice ? ( + return hasIngestManager && noticeState.settingsDataLoaded ? ( { - const { getHref } = useLink(); - const { application } = useStartServices(); - const { show: hasIngestManager } = useCapabilities(); - const { data: settingsData, isLoading } = useGetSettings(); - const [dismissedNotice, setDismissedNotice] = useState(false); - - const dismissNotice = useCallback(async () => { - setDismissedNotice(true); - await sendPutSettings({ - has_seen_add_data_notice: true, - }); - }, []); - - useEffect(() => { - tutorialDirectoryNoticeState$.next({ - settingsDataLoaded: !isLoading, - hasSeenNotice: Boolean(dismissedNotice || settingsData?.item?.has_seen_add_data_notice), - }); - }, [isLoading, settingsData, dismissedNotice]); - - const hasSeenNotice = - isLoading || settingsData?.item?.has_seen_add_data_notice || dismissedNotice; - - return hasIngestManager && !hasSeenNotice ? ( - <> - - - - ), - }} - /> - } - > -

- - - - ), - }} - /> -

- - -
- - - - - -
-
- -
- { - dismissNotice(); - }} - > - - -
-
-
-
- - - ) : null; -}); - -// Needed for React.lazy -// eslint-disable-next-line import/no-default-export -export default TutorialDirectoryNotice; diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts index e1f263b0763e8..4a2a6900cc78c 100644 --- a/x-pack/plugins/fleet/public/plugin.ts +++ b/x-pack/plugins/fleet/public/plugin.ts @@ -44,11 +44,7 @@ import { CUSTOM_LOGS_INTEGRATION_NAME, INTEGRATIONS_BASE_PATH } from './constant import { licenseService } from './hooks'; import { setHttpClient } from './hooks/use_request'; import { createPackageSearchProvider } from './search_provider'; -import { - TutorialDirectoryNotice, - TutorialDirectoryHeaderLink, - TutorialModuleNotice, -} from './components/home_integration'; +import { TutorialDirectoryHeaderLink, TutorialModuleNotice } from './components/home_integration'; import { createExtensionRegistrationCallback } from './services/ui_extensions'; import type { UIExtensionRegistrationCallback, UIExtensionsStorage } from './types'; import { LazyCustomLogsAssetsExtension } from './lazy_custom_logs_assets_extension'; @@ -197,7 +193,6 @@ export class FleetPlugin implements Plugin { diff --git a/x-pack/plugins/infra/public/pages/logs/page_template.tsx b/x-pack/plugins/infra/public/pages/logs/page_template.tsx index 7ee60ab84bf25..6de13b495f0ba 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_template.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_template.tsx @@ -44,13 +44,13 @@ export const LogsPageTemplate: React.FC = ({ actions: { beats: { title: i18n.translate('xpack.infra.logs.noDataConfig.beatsCard.title', { - defaultMessage: 'Add logs with Beats', + defaultMessage: 'Add a logging integration', }), description: i18n.translate('xpack.infra.logs.noDataConfig.beatsCard.description', { defaultMessage: - 'Use Beats to send logs to Elasticsearch. We make it easy with modules for many popular systems and apps.', + 'Use the Elastic Agent or Beats to send logs to Elasticsearch. We make it easy with integrations for many popular systems and apps.', }), - href: basePath + `/app/home#/tutorial_directory/logging`, + href: basePath + `/app/integrations/browse`, }, }, docsLink: docLinks.links.observability.guide, diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx index bc3bc22f3f1b2..2259a8d3528af 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx @@ -22,8 +22,8 @@ export const LogsPageNoIndicesContent = () => { const canConfigureSource = application?.capabilities?.logs?.configureSource ? true : false; const tutorialLinkProps = useLinkProps({ - app: 'home', - hash: '/tutorial_directory/logging', + app: 'integrations', + hash: '/browse', }); return ( diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index ae375dc504e7a..1a79cd996087d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -93,9 +93,7 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => { diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx index 2a436eac30b2c..17e6382ce65cc 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx @@ -18,8 +18,8 @@ interface InvalidNodeErrorProps { export const InvalidNodeError: React.FunctionComponent = ({ nodeName }) => { const tutorialLinkProps = useLinkProps({ - app: 'home', - hash: '/tutorial_directory/metrics', + app: 'integrations', + hash: '/browse', }); return ( diff --git a/x-pack/plugins/infra/public/pages/metrics/page_template.tsx b/x-pack/plugins/infra/public/pages/metrics/page_template.tsx index 41ea12c280841..4da671283644d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/page_template.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/page_template.tsx @@ -10,7 +10,6 @@ import { i18n } from '@kbn/i18n'; import { useKibanaContextForPlugin } from '../../hooks/use_kibana'; import type { LazyObservabilityPageTemplateProps } from '../../../../observability/public'; import { KibanaPageTemplateProps } from '../../../../../../src/plugins/kibana_react/public'; -import { useLinkProps } from '../../hooks/use_link_props'; interface MetricsPageTemplateProps extends LazyObservabilityPageTemplateProps { hasData?: boolean; @@ -30,11 +29,6 @@ export const MetricsPageTemplate: React.FC = ({ }, } = useKibanaContextForPlugin(); - const tutorialLinkProps = useLinkProps({ - app: 'home', - hash: '/tutorial_directory/metrics', - }); - const noDataConfig: KibanaPageTemplateProps['noDataConfig'] = hasData ? undefined : { @@ -44,13 +38,12 @@ export const MetricsPageTemplate: React.FC = ({ actions: { beats: { title: i18n.translate('xpack.infra.metrics.noDataConfig.beatsCard.title', { - defaultMessage: 'Add metrics with Beats', + defaultMessage: 'Add a metrics integration', }), description: i18n.translate('xpack.infra.metrics.noDataConfig.beatsCard.description', { defaultMessage: 'Use Beats to send metrics data to Elasticsearch. We make it easy with modules for many popular systems and apps.', }), - ...tutorialLinkProps, }, }, docsLink: docLinks.links.observability.guide, diff --git a/x-pack/plugins/observability/public/components/app/header/header_menu.tsx b/x-pack/plugins/observability/public/components/app/header/header_menu.tsx index 707cb241501fd..0ed01b7d3673e 100644 --- a/x-pack/plugins/observability/public/components/app/header/header_menu.tsx +++ b/x-pack/plugins/observability/public/components/app/header/header_menu.tsx @@ -26,7 +26,7 @@ export function ObservabilityHeaderMenu(): React.ReactElement | null { {addDataLinkText} diff --git a/x-pack/plugins/observability/public/utils/no_data_config.ts b/x-pack/plugins/observability/public/utils/no_data_config.ts index 1e16fb145bdce..2c87b1434a0b4 100644 --- a/x-pack/plugins/observability/public/utils/no_data_config.ts +++ b/x-pack/plugins/observability/public/utils/no_data_config.ts @@ -24,12 +24,15 @@ export function getNoDataConfig({ defaultMessage: 'Observability', }), actions: { - beats: { + elasticAgent: { + title: i18n.translate('xpack.observability.noDataConfig.beatsCard.title', { + defaultMessage: 'Add integrations', + }), description: i18n.translate('xpack.observability.noDataConfig.beatsCard.description', { defaultMessage: 'Use Beats and APM agents to send observability data to Elasticsearch. We make it easy with support for many popular systems, apps, and languages.', }), - href: basePath.prepend(`/app/home#/tutorial_directory/logging`), + href: basePath.prepend(`/app/integrations`), }, }, docsLink, diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 5c41e92661e58..5a7e19e2cdd05 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -16,7 +16,7 @@ export const APP_NAME = 'Security'; export const APP_ICON = 'securityAnalyticsApp'; export const APP_ICON_SOLUTION = 'logoSecurity'; export const APP_PATH = `/app/security`; -export const ADD_DATA_PATH = `/app/home#/tutorial_directory/security`; +export const ADD_DATA_PATH = `/app/integrations/browse/security`; export const DEFAULT_BYTES_FORMAT = 'format:bytes:defaultPattern'; export const DEFAULT_DATE_FORMAT = 'dateFormat'; export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz'; diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx index 5fa2725f9ee6f..61e9e66f1bb87 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx @@ -45,9 +45,10 @@ describe('OverviewEmpty', () => { expect(wrapper.find('[data-test-subj="empty-page"]').prop('noDataConfig')).toEqual({ actions: { elasticAgent: { + category: 'security', description: - 'Use Elastic Agent to collect security events and protect your endpoints from threats. Manage your agents in Fleet and add integrations with a single click.', - href: '/app/integrations/browse/security', + 'Use Elastic Agent to collect security events and protect your endpoints from threats.', + title: 'Add a Security integration', }, }, docsLink: 'https://www.elastic.co/guide/en/security/mocked-test-branch/index.html', @@ -68,8 +69,11 @@ describe('OverviewEmpty', () => { it('render with correct actions ', () => { expect(wrapper.find('[data-test-subj="empty-page"]').prop('noDataConfig')).toEqual({ actions: { - beats: { - href: '/app/home#/tutorial_directory/security', + elasticAgent: { + category: 'security', + description: + 'Use Elastic Agent to collect security events and protect your endpoints from threats.', + title: 'Add a Security integration', }, }, docsLink: 'https://www.elastic.co/guide/en/security/mocked-test-branch/index.html', diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx index bc76333943191..9b20c079002e6 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx @@ -5,13 +5,10 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../../../common/lib/kibana'; -import { ADD_DATA_PATH } from '../../../../common/constants'; -import { pagePathGetters } from '../../../../../fleet/public'; import { SOLUTION_NAME } from '../../../../public/common/translations'; -import { useUserPrivileges } from '../../../common/components/user_privileges'; import { KibanaPageTemplate, @@ -19,42 +16,27 @@ import { } from '../../../../../../../src/plugins/kibana_react/public'; const OverviewEmptyComponent: React.FC = () => { - const { http, docLinks } = useKibana().services; - const basePath = http.basePath.get(); - const canAccessFleet = useUserPrivileges().endpointPrivileges.canAccessFleet; - const integrationsPathComponents = pagePathGetters.integrations_all({ category: 'security' }); - - const agentAction: NoDataPageActionsProps = useMemo( - () => ({ - elasticAgent: { - href: `${basePath}${integrationsPathComponents[0]}${integrationsPathComponents[1]}`, - description: i18n.translate( - 'xpack.securitySolution.pages.emptyPage.beatsCard.description', - { - defaultMessage: - 'Use Elastic Agent to collect security events and protect your endpoints from threats. Manage your agents in Fleet and add integrations with a single click.', - } - ), - }, - }), - [basePath, integrationsPathComponents] - ); - - const beatsAction: NoDataPageActionsProps = useMemo( - () => ({ - beats: { - href: `${basePath}${ADD_DATA_PATH}`, - }, - }), - [basePath] - ); + const { docLinks } = useKibana().services; + + const agentAction: NoDataPageActionsProps = { + elasticAgent: { + category: 'security', + title: i18n.translate('xpack.securitySolution.pages.emptyPage.beatsCard.title', { + defaultMessage: 'Add a Security integration', + }), + description: i18n.translate('xpack.securitySolution.pages.emptyPage.beatsCard.description', { + defaultMessage: + 'Use Elastic Agent to collect security events and protect your endpoints from threats.', + }), + }, + }; return ( diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3df5e4ee6c48a..852b01977b78b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3009,11 +3009,7 @@ "home.tutorial.savedObject.unableToAddErrorMessage": "{savedObjectsLength} 件中 {errorsLength} 件の kibana オブジェクトが追加できません。エラー:{errorMessage}", "home.tutorial.selectionLegend": "デプロイタイプ", "home.tutorial.selfManagedButtonLabel": "自己管理", - "home.tutorial.tabs.allTitle": "すべて", - "home.tutorial.tabs.loggingTitle": "ログ", - "home.tutorial.tabs.metricsTitle": "メトリック", "home.tutorial.tabs.sampleDataTitle": "サンプルデータ", - "home.tutorial.tabs.securitySolutionTitle": "セキュリティ", "home.tutorial.unexpectedStatusCheckStateErrorDescription": "予期せぬステータス確認ステータス {statusCheckState}", "home.tutorial.unhandledInstructionTypeErrorDescription": "予期せぬ指示タイプ {visibleInstructions}", "home.tutorialDirectory.featureCatalogueDescription": "一般的なアプリやサービスからデータを取り込みます。", @@ -4204,8 +4200,6 @@ "kibana-react.noDataPage.cantDecide.link": "詳細については、ドキュメントをご確認ください。", "kibana-react.noDataPage.elasticAgentCard.description": "Elasticエージェントを使用すると、シンプルで統一された方法でコンピューターからデータを収集するできます。", "kibana-react.noDataPage.elasticAgentCard.title": "Elasticエージェントの追加", - "kibana-react.noDataPage.elasticBeatsCard.description": "Beatsを使用して、さまざまなシステムのデータをElasticsearchに追加します。", - "kibana-react.noDataPage.elasticBeatsCard.title": "データの追加", "kibana-react.noDataPage.intro": "データを追加して開始するか、{solution}については{link}をご覧ください。", "kibana-react.noDataPage.intro.link": "詳細", "kibana-react.noDataPage.noDataPage.recommended": "推奨", @@ -11076,12 +11070,7 @@ "xpack.fleet.fleetServerUpgradeModal.modalTitle": "エージェントをFleetサーバーに登録", "xpack.fleet.fleetServerUpgradeModal.onPremDescriptionMessage": "Fleetサーバーが使用できます。スケーラビリティとセキュリティが改善されています。{existingAgentsMessage} Fleetを使用し続けるには、Fleetサーバーと新しいバージョンのElasticエージェントを各ホストにインストールする必要があります。詳細については、{link}をご覧ください。", "xpack.fleet.genericActionsMenuText": "開く", - "xpack.fleet.homeIntegration.tutorialDirectory.dismissNoticeButtonText": "メッセージを消去", "xpack.fleet.homeIntegration.tutorialDirectory.fleetAppButtonText": "統合を試す", - "xpack.fleet.homeIntegration.tutorialDirectory.noticeText": "Elasticエージェント統合では、シンプルかつ統合された方法で、ログ、メトリック、他の種類のデータの監視をホストに追加することができます。複数のBeatsをインストールする必要はありません。このため、インフラストラクチャ全体でのポリシーのデプロイが簡単で高速になりました。詳細については、{blogPostLink}をお読みください。", - "xpack.fleet.homeIntegration.tutorialDirectory.noticeText.blogPostLink": "発表ブログ投稿", - "xpack.fleet.homeIntegration.tutorialDirectory.noticeTitle": "{newPrefix} Elasticエージェント統合", - "xpack.fleet.homeIntegration.tutorialDirectory.noticeTitle.newPrefix": "一般公開へ:", "xpack.fleet.homeIntegration.tutorialModule.noticeText": "{notePrefix}このモジュールの新しいバージョンは{availableAsIntegrationLink}です。統合と新しいElasticエージェントの詳細については、{blogPostLink}をお読みください。", "xpack.fleet.homeIntegration.tutorialModule.noticeText.blogPostLink": "発表ブログ投稿", "xpack.fleet.homeIntegration.tutorialModule.noticeText.integrationLink": "Elasticエージェント統合として提供", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d9af3edb8101d..9d88c757f1e58 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3038,11 +3038,7 @@ "home.tutorial.savedObject.unableToAddErrorMessage": "{savedObjectsLength} 个 kibana 对象中有 {errorsLength} 个无法添加,错误:{errorMessage}", "home.tutorial.selectionLegend": "部署类型", "home.tutorial.selfManagedButtonLabel": "自管型", - "home.tutorial.tabs.allTitle": "全部", - "home.tutorial.tabs.loggingTitle": "日志", - "home.tutorial.tabs.metricsTitle": "指标", "home.tutorial.tabs.sampleDataTitle": "样例数据", - "home.tutorial.tabs.securitySolutionTitle": "安全", "home.tutorial.unexpectedStatusCheckStateErrorDescription": "意外的状态检查状态 {statusCheckState}", "home.tutorial.unhandledInstructionTypeErrorDescription": "未处理的指令类型 {visibleInstructions}", "home.tutorialDirectory.featureCatalogueDescription": "从热门应用和服务中采集数据。", @@ -4244,8 +4240,6 @@ "kibana-react.noDataPage.cantDecide.link": "请参阅我们的文档以了解更多信息。", "kibana-react.noDataPage.elasticAgentCard.description": "使用 Elastic 代理以简单统一的方式从您的计算机中收集数据。", "kibana-react.noDataPage.elasticAgentCard.title": "添加 Elastic 代理", - "kibana-react.noDataPage.elasticBeatsCard.description": "使用 Beats 将各种系统的数据添加到 Elasticsearch。", - "kibana-react.noDataPage.elasticBeatsCard.title": "添加数据", "kibana-react.noDataPage.intro": "添加您的数据以开始,或{link}{solution}。", "kibana-react.noDataPage.intro.link": "了解详情", "kibana-react.noDataPage.noDataPage.recommended": "推荐", @@ -11191,12 +11185,7 @@ "xpack.fleet.fleetServerUpgradeModal.modalTitle": "将代理注册到 Fleet 服务器", "xpack.fleet.fleetServerUpgradeModal.onPremDescriptionMessage": "Fleet 服务器现在可用且提供改善的可扩展性和安全性。{existingAgentsMessage}要继续使用 Fleet,必须在各个主机上安装 Fleet 服务器和新版 Elastic 代理。详细了解我们的 {link}。", "xpack.fleet.genericActionsMenuText": "打开", - "xpack.fleet.homeIntegration.tutorialDirectory.dismissNoticeButtonText": "关闭消息", "xpack.fleet.homeIntegration.tutorialDirectory.fleetAppButtonText": "试用集成", - "xpack.fleet.homeIntegration.tutorialDirectory.noticeText": "通过 Elastic 代理集成,可以简单统一的方式将日志、指标和其他类型数据的监测添加到主机。不再需要安装多个 Beats,这样将策略部署到整个基础架构更容易也更快速。有关更多信息,请阅读我们的{blogPostLink}。", - "xpack.fleet.homeIntegration.tutorialDirectory.noticeText.blogPostLink": "公告博客", - "xpack.fleet.homeIntegration.tutorialDirectory.noticeTitle": "{newPrefix}Elastic 代理集成", - "xpack.fleet.homeIntegration.tutorialDirectory.noticeTitle.newPrefix": "已正式发布:", "xpack.fleet.homeIntegration.tutorialModule.noticeText": "{notePrefix}此模块的较新版本{availableAsIntegrationLink}。要详细了解集成和新 Elastic 代理,请阅读我们的{blogPostLink}。", "xpack.fleet.homeIntegration.tutorialModule.noticeText.blogPostLink": "公告博客", "xpack.fleet.homeIntegration.tutorialModule.noticeText.integrationLink": "将作为 Elastic 代理集成来提供", diff --git a/x-pack/test/accessibility/apps/home.ts b/x-pack/test/accessibility/apps/home.ts index a7158d9579b60..61297859c29f8 100644 --- a/x-pack/test/accessibility/apps/home.ts +++ b/x-pack/test/accessibility/apps/home.ts @@ -64,33 +64,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await a11y.testAppSnapshot(); }); - it('Add data page meets a11y requirements ', async () => { - await home.clickGoHome(); - await testSubjects.click('homeAddData'); - await a11y.testAppSnapshot(); - }); - - it('Sample data page meets a11y requirements ', async () => { - await testSubjects.click('homeTab-sampleData'); - await a11y.testAppSnapshot(); - }); - - it('click on Add logs panel to open all log examples page meets a11y requirements ', async () => { - await testSubjects.click('sampleDataSetCardlogs'); - await a11y.testAppSnapshot(); - }); - - it('click on ActiveMQ logs panel to open tutorial meets a11y requirements', async () => { - await testSubjects.click('homeTab-all'); - await testSubjects.click('homeSynopsisLinkactivemqlogs'); - await a11y.testAppSnapshot(); - }); - - it('click on cloud tutorial meets a11y requirements', async () => { - await testSubjects.click('onCloudTutorial'); - await a11y.testAppSnapshot(); - }); - it('passes with searchbox open', async () => { await testSubjects.click('nav-search-popover'); await a11y.testAppSnapshot(); From fd0fc770623b483a0eb7de48a4e3407446de9bd7 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Mon, 18 Oct 2021 22:24:20 -0600 Subject: [PATCH 3/6] Fixes console errors seen (#115448) ## Summary During testing I encountered this error message: ``` [2021-10-18T13:19:07.053-06:00][ERROR][plugins.securitySolution] The notification throttle "from" and/or "to" range values could not be constructed as valid. Tried to construct the values of "from": now-null "to": 2021-10-18T19:19:00.835Z. This will cause a reset of the notification throttle. Expect either missing alert notifications or alert notifications happening earlier than expected. ``` This error was happening whenever I had a rule that was using an immediately invoked action and was encountering an error such as a non ECS compliant signal. The root cause is that I was not checking everywhere to ensure we had a throttle rule to ensure scheduling. This fixes that by adding an `if` statement/guard around the areas of code. I also improve the error message by adding which ruleId the error is coming from. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- ...dule_throttle_notification_actions.test.ts | 2 +- .../schedule_throttle_notification_actions.ts | 1 + .../create_security_rule_type_wrapper.ts | 60 +++++++++-------- .../legacy_notifications/one_action.json | 2 +- .../signals/signal_rule_alert_type.test.ts | 64 ++++++++++++++++++- .../signals/signal_rule_alert_type.ts | 61 +++++++++--------- 6 files changed, 129 insertions(+), 61 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts index 81f229c636bd8..964df3c91eb08 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts @@ -278,7 +278,7 @@ describe('schedule_throttle_notification_actions', () => { }); expect(logger.error).toHaveBeenCalledWith( - 'The notification throttle "from" and/or "to" range values could not be constructed as valid. Tried to construct the values of "from": now-invalid "to": 2021-08-24T19:19:22.094Z. This will cause a reset of the notification throttle. Expect either missing alert notifications or alert notifications happening earlier than expected.' + 'The notification throttle "from" and/or "to" range values could not be constructed as valid. Tried to construct the values of "from": now-invalid "to": 2021-08-24T19:19:22.094Z. This will cause a reset of the notification throttle. Expect either missing alert notifications or alert notifications happening earlier than expected. Check your rule with ruleId: "rule-123" for data integrity issues' ); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts index 5bf18496e6375..7b4b314cc8911 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts @@ -145,6 +145,7 @@ export const scheduleThrottledNotificationActions = async ({ ` "from": now-${throttle}`, ` "to": ${startedAt.toISOString()}.`, ' This will cause a reset of the notification throttle. Expect either missing alert notifications or alert notifications happening earlier than expected.', + ` Check your rule with ruleId: "${ruleId}" for data integrity issues`, ].join('') ); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts index 0ad416e86e31a..0fe7cbdc9bd9f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts @@ -375,20 +375,22 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = ); } else { // NOTE: Since this is throttled we have to call it even on an error condition, otherwise it will "reset" the throttle and fire early - await scheduleThrottledNotificationActions({ - alertInstance: services.alertInstanceFactory(alertId), - throttle: ruleSO.attributes.throttle, - startedAt, - id: ruleSO.id, - kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) - ?.kibana_siem_app_url, - outputIndex: ruleDataClient.indexName, - ruleId, - esClient: services.scopedClusterClient.asCurrentUser, - notificationRuleParams, - signals: result.createdSignals, - logger, - }); + if (ruleSO.attributes.throttle != null) { + await scheduleThrottledNotificationActions({ + alertInstance: services.alertInstanceFactory(alertId), + throttle: ruleSO.attributes.throttle, + startedAt, + id: ruleSO.id, + kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) + ?.kibana_siem_app_url, + outputIndex: ruleDataClient.indexName, + ruleId, + esClient: services.scopedClusterClient.asCurrentUser, + notificationRuleParams, + signals: result.createdSignals, + logger, + }); + } const errorMessage = buildRuleMessage( 'Bulk Indexing of signals failed:', truncateMessageList(result.errors).join() @@ -407,20 +409,22 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = } } catch (error) { // NOTE: Since this is throttled we have to call it even on an error condition, otherwise it will "reset" the throttle and fire early - await scheduleThrottledNotificationActions({ - alertInstance: services.alertInstanceFactory(alertId), - throttle: ruleSO.attributes.throttle, - startedAt, - id: ruleSO.id, - kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) - ?.kibana_siem_app_url, - outputIndex: ruleDataClient.indexName, - ruleId, - esClient: services.scopedClusterClient.asCurrentUser, - notificationRuleParams, - signals: result.createdSignals, - logger, - }); + if (ruleSO.attributes.throttle != null) { + await scheduleThrottledNotificationActions({ + alertInstance: services.alertInstanceFactory(alertId), + throttle: ruleSO.attributes.throttle, + startedAt, + id: ruleSO.id, + kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) + ?.kibana_siem_app_url, + outputIndex: ruleDataClient.indexName, + ruleId, + esClient: services.scopedClusterClient.asCurrentUser, + notificationRuleParams, + signals: result.createdSignals, + logger, + }); + } const errorMessage = error.message ?? '(no error message given)'; const message = buildRuleMessage( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/legacy_notifications/one_action.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/legacy_notifications/one_action.json index 1966dcf5ff53c..bf980e370e3a3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/legacy_notifications/one_action.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/legacy_notifications/one_action.json @@ -3,7 +3,7 @@ "interval": "1m", "actions": [ { - "id": "42534430-2092-11ec-99a6-05d79563c01a", + "id": "1fa31c30-3046-11ec-8971-1f3f7bae65af", "group": "default", "params": { "message": "Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index 88b276358a705..6a84776ccee5d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -536,14 +536,74 @@ describe('signal_rule_alert_type', () => { errors: ['Error that bubbled up.'], }; (queryExecutor as jest.Mock).mockResolvedValue(result); - await alert.executor(payload); + const ruleAlert = getAlertMock(false, getQueryRuleParams()); + ruleAlert.throttle = '1h'; + const payLoadWithThrottle = getPayload( + ruleAlert, + alertServices + ) as jest.Mocked; + payLoadWithThrottle.rule.throttle = '1h'; + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + await alert.executor(payLoadWithThrottle); expect(scheduleThrottledNotificationActions).toHaveBeenCalledTimes(1); }); + it('should NOT call scheduleThrottledNotificationActions if result is false and the throttle is not set', async () => { + const result: SearchAfterAndBulkCreateReturnType = { + success: false, + warning: false, + searchAfterTimes: [], + bulkCreateTimes: [], + lastLookBackDate: null, + createdSignalsCount: 0, + createdSignals: [], + warningMessages: [], + errors: ['Error that bubbled up.'], + }; + (queryExecutor as jest.Mock).mockResolvedValue(result); + await alert.executor(payload); + expect(scheduleThrottledNotificationActions).toHaveBeenCalledTimes(0); + }); + it('should call scheduleThrottledNotificationActions if an error was thrown to prevent the throttle from being reset', async () => { (queryExecutor as jest.Mock).mockRejectedValue({}); - await alert.executor(payload); + const ruleAlert = getAlertMock(false, getQueryRuleParams()); + ruleAlert.throttle = '1h'; + const payLoadWithThrottle = getPayload( + ruleAlert, + alertServices + ) as jest.Mocked; + payLoadWithThrottle.rule.throttle = '1h'; + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + await alert.executor(payLoadWithThrottle); expect(scheduleThrottledNotificationActions).toHaveBeenCalledTimes(1); }); + + it('should NOT call scheduleThrottledNotificationActions if an error was thrown to prevent the throttle from being reset if throttle is not defined', async () => { + const result: SearchAfterAndBulkCreateReturnType = { + success: false, + warning: false, + searchAfterTimes: [], + bulkCreateTimes: [], + lastLookBackDate: null, + createdSignalsCount: 0, + createdSignals: [], + warningMessages: [], + errors: ['Error that bubbled up.'], + }; + (queryExecutor as jest.Mock).mockRejectedValue(result); + await alert.executor(payload); + expect(scheduleThrottledNotificationActions).toHaveBeenCalledTimes(0); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 4e98bee83aeb5..90220814fb928 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -474,21 +474,22 @@ export const signalRulesAlertType = ({ ); } else { // NOTE: Since this is throttled we have to call it even on an error condition, otherwise it will "reset" the throttle and fire early - await scheduleThrottledNotificationActions({ - alertInstance: services.alertInstanceFactory(alertId), - throttle: savedObject.attributes.throttle, - startedAt, - id: savedObject.id, - kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) - ?.kibana_siem_app_url, - outputIndex, - ruleId, - signals: result.createdSignals, - esClient: services.scopedClusterClient.asCurrentUser, - notificationRuleParams, - logger, - }); - + if (savedObject.attributes.throttle != null) { + await scheduleThrottledNotificationActions({ + alertInstance: services.alertInstanceFactory(alertId), + throttle: savedObject.attributes.throttle, + startedAt, + id: savedObject.id, + kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) + ?.kibana_siem_app_url, + outputIndex, + ruleId, + signals: result.createdSignals, + esClient: services.scopedClusterClient.asCurrentUser, + notificationRuleParams, + logger, + }); + } const errorMessage = buildRuleMessage( 'Bulk Indexing of signals failed:', truncateMessageList(result.errors).join() @@ -507,20 +508,22 @@ export const signalRulesAlertType = ({ } } catch (error) { // NOTE: Since this is throttled we have to call it even on an error condition, otherwise it will "reset" the throttle and fire early - await scheduleThrottledNotificationActions({ - alertInstance: services.alertInstanceFactory(alertId), - throttle: savedObject.attributes.throttle, - startedAt, - id: savedObject.id, - kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) - ?.kibana_siem_app_url, - outputIndex, - ruleId, - signals: result.createdSignals, - esClient: services.scopedClusterClient.asCurrentUser, - notificationRuleParams, - logger, - }); + if (savedObject.attributes.throttle != null) { + await scheduleThrottledNotificationActions({ + alertInstance: services.alertInstanceFactory(alertId), + throttle: savedObject.attributes.throttle, + startedAt, + id: savedObject.id, + kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) + ?.kibana_siem_app_url, + outputIndex, + ruleId, + signals: result.createdSignals, + esClient: services.scopedClusterClient.asCurrentUser, + notificationRuleParams, + logger, + }); + } const errorMessage = error.message ?? '(no error message given)'; const message = buildRuleMessage( 'An error occurred during rule execution:', From e53f4d2f28d127935bd7fe5dfa235a81e30b9460 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Mon, 18 Oct 2021 22:37:00 -0600 Subject: [PATCH 4/6] [Security Solutions] Makes legacy actions/notification system, legacy action status, and exception lists multiple space shareable (#115427) ## Summary See https://github.com/elastic/kibana/issues/114548 Makes the following saved objects multiple-isolated: * siem-detection-engine-rule-status * exception-list * siem-detection-engine-rule-actions ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- x-pack/plugins/lists/server/saved_objects/exception_list.ts | 3 ++- .../rule_actions/legacy_saved_object_mappings.ts | 3 ++- .../legacy_rule_status_saved_object_mappings.ts | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/lists/server/saved_objects/exception_list.ts b/x-pack/plugins/lists/server/saved_objects/exception_list.ts index 8354e64d64a6e..3d31bdd561140 100644 --- a/x-pack/plugins/lists/server/saved_objects/exception_list.ts +++ b/x-pack/plugins/lists/server/saved_objects/exception_list.ts @@ -177,11 +177,12 @@ const combinedMappings: SavedObjectsType['mappings'] = { }; export const exceptionListType: SavedObjectsType = { + convertToMultiNamespaceTypeVersion: '8.0.0', hidden: false, mappings: combinedMappings, migrations, name: exceptionListSavedObjectType, - namespaceType: 'single', + namespaceType: 'multiple-isolated', }; export const exceptionListAgnosticType: SavedObjectsType = { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_saved_object_mappings.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_saved_object_mappings.ts index 3d6a405225fe6..835ccd92f9cc4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_saved_object_mappings.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_saved_object_mappings.ts @@ -59,9 +59,10 @@ const legacyRuleActionsSavedObjectMappings: SavedObjectsType['mappings'] = { * @deprecated Remove this once we no longer need legacy migrations for rule actions (8.0.0) */ export const legacyType: SavedObjectsType = { + convertToMultiNamespaceTypeVersion: '8.0.0', name: legacyRuleActionsSavedObjectType, hidden: false, - namespaceType: 'single', + namespaceType: 'multiple-isolated', mappings: legacyRuleActionsSavedObjectMappings, migrations: legacyRuleActionsSavedObjectMigration, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_rule_status_saved_object_mappings.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_rule_status_saved_object_mappings.ts index 3fe3fc06cc7d6..298c75b8b7d51 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_rule_status_saved_object_mappings.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_rule_status_saved_object_mappings.ts @@ -65,9 +65,10 @@ export const ruleStatusSavedObjectMappings: SavedObjectsType['mappings'] = { * @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x) */ export const legacyRuleStatusType: SavedObjectsType = { + convertToMultiNamespaceTypeVersion: '8.0.0', name: legacyRuleStatusSavedObjectType, hidden: false, - namespaceType: 'single', + namespaceType: 'multiple-isolated', mappings: ruleStatusSavedObjectMappings, migrations: legacyRuleStatusSavedObjectMigration, }; From 57ff4a7172bb68067df9b809d592748deeb1114c Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Tue, 19 Oct 2021 00:43:12 -0400 Subject: [PATCH 5/6] [Security Solution][Endpoint] Adds additional endpoint privileges to the `useUserPrivileges()` hook (#115051) * Adds new `canIsolateHost` and `canCreateArtifactsByPolicy` privileges for endpoint * Refactor `useEndpointPrivileges` mocks to also provide a test function to return the full set of default privileges * refactor useEndpointPrivileges tests to be more resilient to future changes --- .../__mocks__/use_endpoint_privileges.ts | 18 ---- .../__mocks__/use_endpoint_privileges.ts | 12 +++ .../user_privileges/endpoint/index.ts | 9 ++ .../user_privileges/endpoint/mocks.ts | 29 ++++++ .../use_endpoint_privileges.test.ts | 89 ++++++++++--------- .../{ => endpoint}/use_endpoint_privileges.ts | 25 ++++-- .../user_privileges/endpoint/utils.ts | 19 ++++ .../components/user_privileges/index.tsx | 10 +-- .../components/user_info/index.test.tsx | 2 +- .../alerts/use_alerts_privileges.test.tsx | 6 +- .../alerts/use_signal_index.test.tsx | 2 +- .../search_exceptions.test.tsx | 17 ++-- .../search_exceptions/search_exceptions.tsx | 2 +- .../view/event_filters_list_page.test.tsx | 2 +- .../host_isolation_exceptions_list.test.tsx | 3 +- .../policy_trusted_apps_empty_unassigned.tsx | 2 +- .../policy_trusted_apps_flyout.test.tsx | 2 +- .../policy_trusted_apps_layout.test.tsx | 22 ++--- .../layout/policy_trusted_apps_layout.tsx | 2 +- .../list/policy_trusted_apps_list.test.tsx | 10 +-- .../list/policy_trusted_apps_list.tsx | 2 +- .../view/trusted_apps_page.test.tsx | 2 +- .../public/overview/pages/overview.test.tsx | 2 +- 23 files changed, 170 insertions(+), 119 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/use_endpoint_privileges.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/__mocks__/use_endpoint_privileges.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/index.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/mocks.ts rename x-pack/plugins/security_solution/public/common/components/user_privileges/{ => endpoint}/use_endpoint_privileges.test.ts (63%) rename x-pack/plugins/security_solution/public/common/components/user_privileges/{ => endpoint}/use_endpoint_privileges.ts (71%) create mode 100644 x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/utils.ts diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/use_endpoint_privileges.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/use_endpoint_privileges.ts deleted file mode 100644 index 80ca534534187..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/use_endpoint_privileges.ts +++ /dev/null @@ -1,18 +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 { EndpointPrivileges } from '../use_endpoint_privileges'; - -export const useEndpointPrivileges = jest.fn(() => { - const endpointPrivilegesMock: EndpointPrivileges = { - loading: false, - canAccessFleet: true, - canAccessEndpointManagement: true, - isPlatinumPlus: true, - }; - return endpointPrivilegesMock; -}); diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/__mocks__/use_endpoint_privileges.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/__mocks__/use_endpoint_privileges.ts new file mode 100644 index 0000000000000..ae9aacaf3d55b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/__mocks__/use_endpoint_privileges.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 { getEndpointPrivilegesInitialStateMock } from '../mocks'; + +export { getEndpointPrivilegesInitialState } from '../utils'; + +export const useEndpointPrivileges = jest.fn(getEndpointPrivilegesInitialStateMock); diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/index.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/index.ts new file mode 100644 index 0000000000000..adea89ce1a051 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export * from './use_endpoint_privileges'; +export { getEndpointPrivilegesInitialState } from './utils'; diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/mocks.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/mocks.ts new file mode 100644 index 0000000000000..2851c92816cea --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/mocks.ts @@ -0,0 +1,29 @@ +/* + * 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 type { EndpointPrivileges } from './use_endpoint_privileges'; +import { getEndpointPrivilegesInitialState } from './utils'; + +export const getEndpointPrivilegesInitialStateMock = ( + overrides: Partial = {} +): EndpointPrivileges => { + // Get the initial state and set all permissions to `true` (enabled) for testing + const endpointPrivilegesMock: EndpointPrivileges = { + ...( + Object.entries(getEndpointPrivilegesInitialState()) as Array< + [keyof EndpointPrivileges, boolean] + > + ).reduce((mockPrivileges, [key, value]) => { + mockPrivileges[key] = !value; + + return mockPrivileges; + }, {} as EndpointPrivileges), + ...overrides, + }; + + return endpointPrivilegesMock; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.test.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts similarity index 63% rename from x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.test.ts rename to x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts index 82443e913499b..d4ba29a4ef950 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts @@ -6,16 +6,17 @@ */ import { act, renderHook, RenderHookResult, RenderResult } from '@testing-library/react-hooks'; -import { useHttp, useCurrentUser } from '../../lib/kibana'; +import { useHttp, useCurrentUser } from '../../../lib/kibana'; import { EndpointPrivileges, useEndpointPrivileges } from './use_endpoint_privileges'; -import { securityMock } from '../../../../../security/public/mocks'; -import { appRoutesService } from '../../../../../fleet/common'; -import { AuthenticatedUser } from '../../../../../security/common'; -import { licenseService } from '../../hooks/use_license'; -import { fleetGetCheckPermissionsHttpMock } from '../../../management/pages/mocks'; - -jest.mock('../../lib/kibana'); -jest.mock('../../hooks/use_license', () => { +import { securityMock } from '../../../../../../security/public/mocks'; +import { appRoutesService } from '../../../../../../fleet/common'; +import { AuthenticatedUser } from '../../../../../../security/common'; +import { licenseService } from '../../../hooks/use_license'; +import { fleetGetCheckPermissionsHttpMock } from '../../../../management/pages/mocks'; +import { getEndpointPrivilegesInitialStateMock } from './mocks'; + +jest.mock('../../../lib/kibana'); +jest.mock('../../../hooks/use_license', () => { const licenseServiceInstance = { isPlatinumPlus: jest.fn(), }; @@ -27,6 +28,8 @@ jest.mock('../../hooks/use_license', () => { }; }); +const licenseServiceMock = licenseService as jest.Mocked; + describe('When using useEndpointPrivileges hook', () => { let authenticatedUser: AuthenticatedUser; let fleetApiMock: ReturnType; @@ -45,7 +48,7 @@ describe('When using useEndpointPrivileges hook', () => { fleetApiMock = fleetGetCheckPermissionsHttpMock( useHttp() as Parameters[0] ); - (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(true); + licenseServiceMock.isPlatinumPlus.mockReturnValue(true); render = () => { const hookRenderResponse = renderHook(() => useEndpointPrivileges()); @@ -69,34 +72,31 @@ describe('When using useEndpointPrivileges hook', () => { (useCurrentUser as jest.Mock).mockReturnValue(null); const { rerender } = render(); - expect(result.current).toEqual({ - canAccessEndpointManagement: false, - canAccessFleet: false, - loading: true, - isPlatinumPlus: true, - }); + expect(result.current).toEqual( + getEndpointPrivilegesInitialStateMock({ + canAccessEndpointManagement: false, + canAccessFleet: false, + loading: true, + }) + ); // Make user service available (useCurrentUser as jest.Mock).mockReturnValue(authenticatedUser); rerender(); - expect(result.current).toEqual({ - canAccessEndpointManagement: false, - canAccessFleet: false, - loading: true, - isPlatinumPlus: true, - }); + expect(result.current).toEqual( + getEndpointPrivilegesInitialStateMock({ + canAccessEndpointManagement: false, + canAccessFleet: false, + loading: true, + }) + ); // Release the API response await act(async () => { fleetApiMock.waitForApi(); releaseApiResponse!(); }); - expect(result.current).toEqual({ - canAccessEndpointManagement: true, - canAccessFleet: true, - loading: false, - isPlatinumPlus: true, - }); + expect(result.current).toEqual(getEndpointPrivilegesInitialStateMock()); }); it('should call Fleet permissions api to determine user privilege to fleet', async () => { @@ -113,12 +113,11 @@ describe('When using useEndpointPrivileges hook', () => { render(); await waitForNextUpdate(); await fleetApiMock.waitForApi(); - expect(result.current).toEqual({ - canAccessEndpointManagement: false, - canAccessFleet: true, // this is only true here because I did not adjust the API mock - loading: false, - isPlatinumPlus: true, - }); + expect(result.current).toEqual( + getEndpointPrivilegesInitialStateMock({ + canAccessEndpointManagement: false, + }) + ); }); it('should set privileges to false if fleet api check returns failure', async () => { @@ -130,11 +129,21 @@ describe('When using useEndpointPrivileges hook', () => { render(); await waitForNextUpdate(); await fleetApiMock.waitForApi(); - expect(result.current).toEqual({ - canAccessEndpointManagement: false, - canAccessFleet: false, - loading: false, - isPlatinumPlus: true, - }); + expect(result.current).toEqual( + getEndpointPrivilegesInitialStateMock({ + canAccessEndpointManagement: false, + canAccessFleet: false, + }) + ); }); + + it.each([['canIsolateHost'], ['canCreateArtifactsByPolicy']])( + 'should set %s to false if license is not PlatinumPlus', + async (privilege) => { + licenseServiceMock.isPlatinumPlus.mockReturnValue(false); + render(); + await waitForNextUpdate(); + expect(result.current).toEqual(expect.objectContaining({ [privilege]: false })); + } + ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts similarity index 71% rename from x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.ts rename to x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts index 315935104d107..36f02d22487fc 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.ts +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts @@ -6,9 +6,9 @@ */ import { useEffect, useMemo, useRef, useState } from 'react'; -import { useCurrentUser, useHttp } from '../../lib/kibana'; -import { appRoutesService, CheckPermissionsResponse } from '../../../../../fleet/common'; -import { useLicense } from '../../hooks/use_license'; +import { useCurrentUser, useHttp } from '../../../lib/kibana'; +import { appRoutesService, CheckPermissionsResponse } from '../../../../../../fleet/common'; +import { useLicense } from '../../../hooks/use_license'; export interface EndpointPrivileges { loading: boolean; @@ -16,6 +16,11 @@ export interface EndpointPrivileges { canAccessFleet: boolean; /** If user has permissions to access Endpoint management (includes check to ensure they also have access to fleet) */ canAccessEndpointManagement: boolean; + /** if user has permissions to create Artifacts by Policy */ + canCreateArtifactsByPolicy: boolean; + /** If user has permissions to use the Host isolation feature */ + canIsolateHost: boolean; + /** @deprecated do not use. instead, use one of the other privileges defined */ isPlatinumPlus: boolean; } @@ -29,7 +34,7 @@ export const useEndpointPrivileges = (): EndpointPrivileges => { const http = useHttp(); const user = useCurrentUser(); const isMounted = useRef(true); - const license = useLicense(); + const isPlatinumPlusLicense = useLicense().isPlatinumPlus(); const [canAccessFleet, setCanAccessFleet] = useState(false); const [fleetCheckDone, setFleetCheckDone] = useState(false); @@ -61,13 +66,19 @@ export const useEndpointPrivileges = (): EndpointPrivileges => { }, [user?.roles]); const privileges = useMemo(() => { - return { + const privilegeList: EndpointPrivileges = { loading: !fleetCheckDone || !user, canAccessFleet, canAccessEndpointManagement: canAccessFleet && isSuperUser, - isPlatinumPlus: license.isPlatinumPlus(), + canCreateArtifactsByPolicy: isPlatinumPlusLicense, + canIsolateHost: isPlatinumPlusLicense, + // FIXME: Remove usages of the property below + /** @deprecated */ + isPlatinumPlus: isPlatinumPlusLicense, }; - }, [canAccessFleet, fleetCheckDone, isSuperUser, user, license]); + + return privilegeList; + }, [canAccessFleet, fleetCheckDone, isSuperUser, user, isPlatinumPlusLicense]); // Capture if component is unmounted useEffect( diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/utils.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/utils.ts new file mode 100644 index 0000000000000..df91314479f18 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/utils.ts @@ -0,0 +1,19 @@ +/* + * 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 { EndpointPrivileges } from './use_endpoint_privileges'; + +export const getEndpointPrivilegesInitialState = (): EndpointPrivileges => { + return { + loading: true, + canAccessFleet: false, + canAccessEndpointManagement: false, + canIsolateHost: false, + canCreateArtifactsByPolicy: false, + isPlatinumPlus: false, + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx b/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx index 437d27278102b..bc0640296b33d 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx @@ -11,9 +11,10 @@ import { DeepReadonly } from 'utility-types'; import { Capabilities } from '../../../../../../../src/core/public'; import { useFetchDetectionEnginePrivileges } from '../../../detections/components/user_privileges/use_fetch_detection_engine_privileges'; import { useFetchListPrivileges } from '../../../detections/components/user_privileges/use_fetch_list_privileges'; -import { EndpointPrivileges, useEndpointPrivileges } from './use_endpoint_privileges'; +import { EndpointPrivileges, useEndpointPrivileges } from './endpoint'; import { SERVER_APP_ID } from '../../../../common/constants'; +import { getEndpointPrivilegesInitialState } from './endpoint/utils'; export interface UserPrivilegesState { listPrivileges: ReturnType; detectionEnginePrivileges: ReturnType; @@ -24,12 +25,7 @@ export interface UserPrivilegesState { export const initialUserPrivilegesState = (): UserPrivilegesState => ({ listPrivileges: { loading: false, error: undefined, result: undefined }, detectionEnginePrivileges: { loading: false, error: undefined, result: undefined }, - endpointPrivileges: { - loading: true, - canAccessEndpointManagement: false, - canAccessFleet: false, - isPlatinumPlus: false, - }, + endpointPrivileges: getEndpointPrivilegesInitialState(), kibanaSecuritySolutionsPrivileges: { crud: false, read: false }, }); diff --git a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx index 3d95dca81165e..1a8588017e4d6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx @@ -17,7 +17,7 @@ import { UserPrivilegesProvider } from '../../../common/components/user_privileg jest.mock('../../../common/lib/kibana'); jest.mock('../../containers/detection_engine/alerts/api'); -jest.mock('../../../common/components/user_privileges/use_endpoint_privileges'); +jest.mock('../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); describe('useUserInfo', () => { beforeAll(() => { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx index 40894c1d01929..1dc1423606097 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx @@ -12,6 +12,7 @@ import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock'; import { useUserPrivileges } from '../../../../common/components/user_privileges'; import { Privilege } from './types'; import { UseAlertsPrivelegesReturn, useAlertsPrivileges } from './use_alerts_privileges'; +import { getEndpointPrivilegesInitialStateMock } from '../../../../common/components/user_privileges/endpoint/mocks'; jest.mock('./api'); jest.mock('../../../../common/hooks/use_app_toasts'); @@ -86,12 +87,11 @@ const userPrivilegesInitial: ReturnType = { result: undefined, error: undefined, }, - endpointPrivileges: { + endpointPrivileges: getEndpointPrivilegesInitialStateMock({ loading: true, canAccessEndpointManagement: false, canAccessFleet: false, - isPlatinumPlus: true, - }, + }), kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx index ade83fed4fd6b..ad4ad5062c9d5 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx @@ -13,7 +13,7 @@ import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; jest.mock('./api'); jest.mock('../../../../common/hooks/use_app_toasts'); -jest.mock('../../../../common/components/user_privileges/use_endpoint_privileges'); +jest.mock('../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); describe('useSignalIndex', () => { let appToastsMock: jest.Mocked>; diff --git a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx index 084978d35d03a..3b987a7211411 100644 --- a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx @@ -11,11 +11,12 @@ import { AppContextTestRender, createAppRootMockRenderer } from '../../../common import { EndpointPrivileges, useEndpointPrivileges, -} from '../../../common/components/user_privileges/use_endpoint_privileges'; +} from '../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; import { SearchExceptions, SearchExceptionsProps } from '.'; -jest.mock('../../../common/components/user_privileges/use_endpoint_privileges'); +import { getEndpointPrivilegesInitialStateMock } from '../../../common/components/user_privileges/endpoint/mocks'; +jest.mock('../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); let onSearchMock: jest.Mock; const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock; @@ -29,13 +30,11 @@ describe('Search exceptions', () => { const loadedUserEndpointPrivilegesState = ( endpointOverrides: Partial = {} - ): EndpointPrivileges => ({ - loading: false, - canAccessFleet: true, - canAccessEndpointManagement: true, - isPlatinumPlus: false, - ...endpointOverrides, - }); + ): EndpointPrivileges => + getEndpointPrivilegesInitialStateMock({ + isPlatinumPlus: false, + ...endpointOverrides, + }); beforeEach(() => { onSearchMock = jest.fn(); diff --git a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx index 1f3eab5db2947..569916ac20315 100644 --- a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx +++ b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiButton } from '@elastic/e import { i18n } from '@kbn/i18n'; import { PolicySelectionItem, PoliciesSelector } from '../policies_selector'; import { ImmutableArray, PolicyData } from '../../../../common/endpoint/types'; -import { useEndpointPrivileges } from '../../../common/components/user_privileges/use_endpoint_privileges'; +import { useEndpointPrivileges } from '../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; export interface SearchExceptionsProps { defaultValue?: string; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx index 0729f95bb44a9..02efce1ab59e8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx @@ -14,7 +14,7 @@ import { isFailedResourceState, isLoadedResourceState } from '../../../state'; // Needed to mock the data services used by the ExceptionItem component jest.mock('../../../../common/lib/kibana'); -jest.mock('../../../../common/components/user_privileges/use_endpoint_privileges'); +jest.mock('../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); describe('When on the Event Filters List Page', () => { let render: () => ReturnType; diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx index 5113457e5bccc..625da11a3644e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx @@ -16,8 +16,7 @@ import { getHostIsolationExceptionItems } from '../service'; import { HostIsolationExceptionsList } from './host_isolation_exceptions_list'; import { useLicense } from '../../../../common/hooks/use_license'; -jest.mock('../../../../common/components/user_privileges/use_endpoint_privileges'); - +jest.mock('../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); jest.mock('../service'); jest.mock('../../../../common/hooks/use_license'); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx index ee52e1210a481..c12bec03ada04 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx @@ -10,7 +10,7 @@ import { EuiEmptyPrompt, EuiButton, EuiPageTemplate, EuiLink } from '@elastic/eu import { FormattedMessage } from '@kbn/i18n/react'; import { usePolicyDetailsNavigateCallback } from '../../policy_hooks'; import { useGetLinkTo } from './use_policy_trusted_apps_empty_hooks'; -import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/use_endpoint_privileges'; +import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; interface CommonProps { policyId: string; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.test.tsx index c1d00f7a3f99b..8e412d2020b72 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.test.tsx @@ -21,7 +21,7 @@ import { createLoadedResourceState, isLoadedResourceState } from '../../../../.. import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing'; jest.mock('../../../../trusted_apps/service'); -jest.mock('../../../../../../common/components/user_privileges/use_endpoint_privileges'); +jest.mock('../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); let mockedContext: AppContextTestRender; let waitForAction: MiddlewareActionSpyHelper['waitForAction']; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx index 43e19c00bcc8e..dbb18a1b0f2ef 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx @@ -19,13 +19,11 @@ import { createLoadedResourceState, isLoadedResourceState } from '../../../../.. import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing'; import { EndpointDocGenerator } from '../../../../../../../common/endpoint/generate_data'; import { policyListApiPathHandlers } from '../../../store/test_mock_utils'; -import { - EndpointPrivileges, - useEndpointPrivileges, -} from '../../../../../../common/components/user_privileges/use_endpoint_privileges'; +import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; +import { getEndpointPrivilegesInitialStateMock } from '../../../../../../common/components/user_privileges/endpoint/mocks'; jest.mock('../../../../trusted_apps/service'); -jest.mock('../../../../../../common/components/user_privileges/use_endpoint_privileges'); +jest.mock('../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock; let mockedContext: AppContextTestRender; @@ -37,16 +35,6 @@ let http: typeof coreStart.http; const generator = new EndpointDocGenerator(); describe('Policy trusted apps layout', () => { - const loadedUserEndpointPrivilegesState = ( - endpointOverrides: Partial = {} - ): EndpointPrivileges => ({ - loading: false, - canAccessFleet: true, - canAccessEndpointManagement: true, - isPlatinumPlus: true, - ...endpointOverrides, - }); - beforeEach(() => { mockedContext = createAppRootMockRenderer(); http = mockedContext.coreStart.http; @@ -137,7 +125,7 @@ describe('Policy trusted apps layout', () => { it('should hide assign button on empty state with unassigned policies when downgraded to a gold or below license', async () => { mockUseEndpointPrivileges.mockReturnValue( - loadedUserEndpointPrivilegesState({ + getEndpointPrivilegesInitialStateMock({ isPlatinumPlus: false, }) ); @@ -155,7 +143,7 @@ describe('Policy trusted apps layout', () => { it('should hide the `Assign trusted applications` button when there is data and the license is downgraded to gold or below', async () => { mockUseEndpointPrivileges.mockReturnValue( - loadedUserEndpointPrivilegesState({ + getEndpointPrivilegesInitialStateMock({ isPlatinumPlus: false, }) ); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx index a3f1ed215286a..49f76ad2e02c6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx @@ -30,7 +30,7 @@ import { import { usePolicyDetailsNavigateCallback, usePolicyDetailsSelector } from '../../policy_hooks'; import { PolicyTrustedAppsFlyout } from '../flyout'; import { PolicyTrustedAppsList } from '../list/policy_trusted_apps_list'; -import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/use_endpoint_privileges'; +import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; import { useAppUrl } from '../../../../../../common/lib/kibana'; import { APP_ID } from '../../../../../../../common/constants'; import { getTrustedAppsListPath } from '../../../../../common/routing'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx index a8d3cc1505463..e18d3c01791c0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx @@ -24,9 +24,10 @@ import { APP_ID } from '../../../../../../../common/constants'; import { EndpointPrivileges, useEndpointPrivileges, -} from '../../../../../../common/components/user_privileges/use_endpoint_privileges'; +} from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; +import { getEndpointPrivilegesInitialStateMock } from '../../../../../../common/components/user_privileges/endpoint/mocks'; -jest.mock('../../../../../../common/components/user_privileges/use_endpoint_privileges'); +jest.mock('../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock; describe('when rendering the PolicyTrustedAppsList', () => { @@ -43,10 +44,7 @@ describe('when rendering the PolicyTrustedAppsList', () => { const loadedUserEndpointPrivilegesState = ( endpointOverrides: Partial = {} ): EndpointPrivileges => ({ - loading: false, - canAccessFleet: true, - canAccessEndpointManagement: true, - isPlatinumPlus: true, + ...getEndpointPrivilegesInitialStateMock(), ...endpointOverrides, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx index f6afd9d502486..def0f490b7fee 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx @@ -38,7 +38,7 @@ import { ContextMenuItemNavByRouterProps } from '../../../../../components/conte import { ArtifactEntryCollapsibleCardProps } from '../../../../../components/artifact_entry_card'; import { useTestIdGenerator } from '../../../../../components/hooks/use_test_id_generator'; import { RemoveTrustedAppFromPolicyModal } from './remove_trusted_app_from_policy_modal'; -import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/use_endpoint_privileges'; +import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; const DATA_TEST_SUBJ = 'policyTrustedAppsGrid'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx index f39fd47c78771..b4366a8922927 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx @@ -52,7 +52,7 @@ jest.mock('../../../../common/hooks/use_license', () => { }; }); -jest.mock('../../../../common/components/user_privileges/use_endpoint_privileges'); +jest.mock('../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); describe('When on the Trusted Apps Page', () => { const expectedAboutInfo = diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx index cab02450f8886..ab5ae4f613e38 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx @@ -30,7 +30,7 @@ import { mockCtiLinksResponse, } from '../components/overview_cti_links/mock'; import { useCtiDashboardLinks } from '../containers/overview_cti_links'; -import { EndpointPrivileges } from '../../common/components/user_privileges/use_endpoint_privileges'; +import { EndpointPrivileges } from '../../common/components/user_privileges/endpoint/use_endpoint_privileges'; import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; import { useHostsRiskScore } from '../containers/overview_risky_host_links/use_hosts_risk_score'; From 730df8852f63bc6b652cca4f31dd56ea4e2addba Mon Sep 17 00:00:00 2001 From: "Joey F. Poon" Date: Tue, 19 Oct 2021 00:09:21 -0500 Subject: [PATCH 6/6] [Security Solution] fix endpoint list agent status logic (#115286) --- .../server/endpoint/routes/metadata/handlers.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts index 027107bcf1a59..e98cdc4f11404 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts @@ -48,6 +48,7 @@ import { } from './support/query_strategies'; import { NotFoundError } from '../../errors'; import { EndpointHostUnEnrolledError } from '../../services/metadata'; +import { getAgentStatus } from '../../../../../fleet/common/services/agent_status'; export interface MetadataRequestContext { esClient?: IScopedClusterClient; @@ -522,10 +523,11 @@ async function queryUnitedIndex( const agentPolicy = agentPoliciesMap[agent.policy_id!]; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const endpointPolicy = endpointPoliciesMap[agent.policy_id!]; + const fleetAgentStatus = getAgentStatus(agent as Agent); + return { metadata, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - host_status: fleetAgentStatusToEndpointHostStatus(agent.last_checkin_status!), + host_status: fleetAgentStatusToEndpointHostStatus(fleetAgentStatus), policy_info: { agent: { applied: {