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: {