- Recent activity
+ {RECENT_ACTIVITY_TITLE}
@@ -198,7 +232,7 @@ export const Overview: React.FC = () => {
const groupsSummary = (
<>
- Group Access
+ {GROUP_ACCESS_TITLE}
@@ -223,7 +257,7 @@ export const Overview: React.FC = () => {
<>
- Configuration
+ {CONFIGURATION_TITLE}
@@ -251,7 +285,7 @@ export const Overview: React.FC = () => {
<>
- Document-level permissions
+ {DOCUMENT_PERMISSIONS_TITLE}
@@ -261,7 +295,7 @@ export const Overview: React.FC = () => {
- Using document-level permissions
+ {DOCUMENT_PERMISSIONS_TEXT}
@@ -273,7 +307,7 @@ export const Overview: React.FC = () => {
<>
- Document-level permissions
+ {DOCUMENT_PERMISSIONS_TITLE}
@@ -284,13 +318,20 @@ export const Overview: React.FC = () => {
- Disabled for this source
+ {DOCUMENT_PERMISSIONS_DISABLED_TEXT}
-
- Learn more
- {' '}
- about permissions
+
+ {LEARN_MORE_LINK}
+
+ ),
+ }}
+ />
@@ -303,7 +344,7 @@ export const Overview: React.FC = () => {
- Status
+ {STATUS_HEADER}
@@ -313,10 +354,10 @@ export const Overview: React.FC = () => {
- Everything looks good
+ {STATUS_HEADING}
- Your endpoints are ready to accept requests.
+ {STATUS_TEXT}
@@ -327,7 +368,7 @@ export const Overview: React.FC = () => {
- Status
+ {STATUS_HEADING}
@@ -337,15 +378,21 @@ export const Overview: React.FC = () => {
- Requires additional configuration
+ {ADDITIONAL_CONFIG_HEADING}
- The{' '}
-
- External Identities API
- {' '}
- must be used to configure user access mappings. Read the guide to learn more.
+
+ {EXTERNAL_IDENTITIES_LINK}
+
+ ),
+ }}
+ />
@@ -357,13 +404,13 @@ export const Overview: React.FC = () => {
- Credentials
+ {CREDENTIALS_TITLE}
-
+
-
+
);
@@ -377,7 +424,7 @@ export const Overview: React.FC = () => {
- Documentation
+ {DOCUMENTATION_LINK_TITLE}
@@ -393,18 +440,15 @@ export const Overview: React.FC = () => {
- Document-level permissions
+ {DOCUMENT_PERMISSIONS_TITLE}
-
- Document-level permissions manage content access content on individual or group
- attributes. Allow or deny access to specific documents.
-
+ {DOC_PERMISSIONS_DESCRIPTION}
- Learn about Platinum features
+ {LEARN_CUSTOM_FEATURES_BUTTON}
@@ -449,13 +493,20 @@ export const Overview: React.FC = () => {
-
- Learn more
- {' '}
- about custom sources.
+
+ {LEARN_MORE_LINK}
+
+ ),
+ }}
+ />
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx
index 16aceacbddcd5..3f7d99629ca4a 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx
@@ -10,6 +10,8 @@ import { Location } from 'history';
import { useActions, useValues } from 'kea';
import { Redirect, useLocation } from 'react-router-dom';
+import { i18n } from '@kbn/i18n';
+
import { setErrorMessage } from '../../../../shared/flash_messages';
import { parseQueryParams } from '../../../../../applications/shared/query_params';
@@ -37,7 +39,13 @@ export const SourceAdded: React.FC = () => {
const decodedName = decodeURIComponent(name);
if (hasError) {
- const defaultError = `${decodedName} failed to connect.`;
+ const defaultError = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.sourceAdded.error',
+ {
+ defaultMessage: '{decodedName} failed to connect.',
+ values: { decodedName },
+ }
+ );
setErrorMessage(errorMessages ? errorMessages.join(' ') : defaultError);
} else {
setAddedSource(decodedName, indexPermissions, serviceType);
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx
index 728d21eb1530f..cac74d37f9f66 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx
@@ -10,6 +10,9 @@ import { useActions, useValues } from 'kea';
import { startCase } from 'lodash';
import moment from 'moment';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+
import {
EuiButton,
EuiButtonEmpty,
@@ -41,6 +44,16 @@ import { TablePaginationBar } from '../../../components/shared/table_pagination_
import { ViewContentHeader } from '../../../components/shared/view_content_header';
import { CUSTOM_SERVICE_TYPE } from '../../../constants';
+import {
+ NO_CONTENT_MESSAGE,
+ CUSTOM_DOCUMENTATION_LINK,
+ TITLE_HEADING,
+ LAST_UPDATED_HEADING,
+ GO_BUTTON,
+ RESET_BUTTON,
+ SOURCE_CONTENT_TITLE,
+ CONTENT_LOADING_TEXT,
+} from '../constants';
import { SourceLogic } from '../source_logic';
@@ -78,8 +91,11 @@ export const SourceContent: React.FC = () => {
const showPagination = totalPages > 1;
const hasItems = totalItems > 0;
const emptyMessage = contentFilterValue
- ? `No results for '${contentFilterValue}'`
- : "This source doesn't have any content yet";
+ ? i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.noContentForValue.message', {
+ defaultMessage: "No results for '{contentFilterValue}'",
+ values: { contentFilterValue },
+ })
+ : NO_CONTENT_MESSAGE;
const paginationOptions = {
totalPages,
@@ -101,10 +117,17 @@ export const SourceContent: React.FC = () => {
body={
isCustomSource ? (
- Learn more about adding content in our{' '}
-
- documentation
-
+
+ {CUSTOM_DOCUMENTATION_LINK}
+
+ ),
+ }}
+ />
) : null
}
@@ -143,9 +166,9 @@ export const SourceContent: React.FC = () => {
- Title
+ {TITLE_HEADING}
{startCase(urlField)}
- Last Updated
+ {LAST_UPDATED_HEADING}
{contentItems.map(contentItem)}
@@ -167,12 +190,12 @@ export const SourceContent: React.FC = () => {
color="primary"
onClick={() => setContentFilterValue(searchTerm)}
>
- Go
+ {GO_BUTTON}
- Reset
+ {RESET_BUTTON}
>
@@ -180,12 +203,18 @@ export const SourceContent: React.FC = () => {
return (
<>
-
+
{
{isFederatedSource && federatedSearchControls}
- {sectionLoading && }
+ {sectionLoading && }
{!sectionLoading && (hasItems ? contentTable : emptyState)}
>
);
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx
index 8e3a116e3ac33..ee877e8f61ad6 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx
@@ -18,6 +18,8 @@ import {
import { SourceIcon } from '../../../components/shared/source_icon';
+import { REMOTE_SOURCE_LABEL, CREATED_LABEL, STATUS_LABEL, READY_TEXT } from '../constants';
+
interface SourceInfoCardProps {
sourceName: string;
sourceType: string;
@@ -54,7 +56,7 @@ export const SourceInfoCard: React.FC = ({
- Remote Source
+ {REMOTE_SOURCE_LABEL}
@@ -63,7 +65,7 @@ export const SourceInfoCard: React.FC = ({
- Created:
+ {CREATED_LABEL}
{dateCreated}
@@ -71,12 +73,12 @@ export const SourceInfoCard: React.FC = ({
- Status:
+ {STATUS_LABEL}
- Ready to search
+ {READY_TEXT}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx
index 8d3219be9b02a..5f47fa2d5927b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx
@@ -10,6 +10,8 @@ import { useActions, useValues } from 'kea';
import { isEmpty } from 'lodash';
import { Link } from 'react-router-dom';
+import { FormattedMessage } from '@kbn/i18n/react';
+
import {
EuiButton,
EuiButtonEmpty,
@@ -21,6 +23,24 @@ import {
EuiFormRow,
} from '@elastic/eui';
+import {
+ CANCEL_BUTTON,
+ OK_BUTTON,
+ CONFIRM_MODAL_TITLE,
+ SAVE_CHANGES_BUTTON,
+ REMOVE_BUTTON,
+} from '../../../constants';
+import {
+ SOURCE_SETTINGS_TITLE,
+ SOURCE_SETTINGS_DESCRIPTION,
+ SOURCE_NAME_LABEL,
+ SOURCE_CONFIG_TITLE,
+ SOURCE_CONFIG_DESCRIPTION,
+ SOURCE_CONFIG_LINK,
+ SOURCE_REMOVE_TITLE,
+ SOURCE_REMOVE_DESCRIPTION,
+} from '../constants';
+
import { ContentSection } from '../../../components/shared/content_section';
import { SourceConfigFields } from '../../../components/shared/source_config_fields';
import { ViewContentHeader } from '../../../components/shared/view_content_header';
@@ -85,16 +105,22 @@ export const SourceSettings: React.FC = () => {
const confirmModal = (
- Your source documents will be deleted from Workplace Search.
- Are you sure you want to remove {name}?
+ ,
+ }}
+ />
);
@@ -102,10 +128,7 @@ export const SourceSettings: React.FC = () => {
return (
<>
-
+
{showConfig && (
-
+
{
/>
- Edit content source connector settings
+ {SOURCE_CONFIG_LINK}
)}
-
+
{
color="danger"
onClick={showConfirm}
>
- Remove
+ {REMOVE_BUTTON}
{confirmModalVisible && confirmModal}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts
new file mode 100644
index 0000000000000..48b8a06b2549c
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts
@@ -0,0 +1,313 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const SOURCES_NO_CONTENT_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.noContent.title',
+ {
+ defaultMessage: 'No content yet',
+ }
+);
+
+export const CONTENT_SUMMARY_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.contentSummary.title',
+ {
+ defaultMessage: 'Content summary',
+ }
+);
+
+export const CONTENT_TYPE_HEADER = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.contentType.header',
+ {
+ defaultMessage: 'Content type',
+ }
+);
+
+export const ITEMS_HEADER = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.items.header',
+ {
+ defaultMessage: 'Items',
+ }
+);
+
+export const EVENT_HEADER = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.event.header',
+ {
+ defaultMessage: 'Event',
+ }
+);
+
+export const STATUS_HEADER = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.status.header',
+ {
+ defaultMessage: 'Status',
+ }
+);
+
+export const TIME_HEADER = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.time.header',
+ {
+ defaultMessage: 'Time',
+ }
+);
+
+export const TOTAL_DOCUMENTS_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.totalDocuments.label',
+ {
+ defaultMessage: 'Total documents',
+ }
+);
+
+export const EMPTY_ACTIVITY_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.emptyActivity.title',
+ {
+ defaultMessage: 'There is no recent activity',
+ }
+);
+
+export const GROUP_ACCESS_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.groupAccess.title',
+ {
+ defaultMessage: 'Group access',
+ }
+);
+
+export const CONFIGURATION_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.configuration.title',
+ {
+ defaultMessage: 'Configuration',
+ }
+);
+
+export const DOCUMENT_PERMISSIONS_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.documentPermissions.title',
+ {
+ defaultMessage: 'Document-level permissions',
+ }
+);
+
+export const DOCUMENT_PERMISSIONS_TEXT = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.documentPermissions.text',
+ {
+ defaultMessage: 'Using document-level permissions',
+ }
+);
+
+export const DOCUMENT_PERMISSIONS_DISABLED_TEXT = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.documentPermissionsDisabled.text',
+ {
+ defaultMessage: 'Disabled for this sources',
+ }
+);
+
+export const LEARN_MORE_LINK = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.learnMore.link',
+ {
+ defaultMessage: 'Learn more',
+ }
+);
+
+export const STATUS_HEADING = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.status.heading',
+ {
+ defaultMessage: 'Everything looks good',
+ }
+);
+
+export const STATUS_TEXT = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.status.text',
+ {
+ defaultMessage: 'Your endpoints are ready to accept requests.',
+ }
+);
+
+export const ADDITIONAL_CONFIG_HEADING = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.additionalConfig.heading',
+ {
+ defaultMessage: 'Requires additional configuration',
+ }
+);
+
+export const EXTERNAL_IDENTITIES_LINK = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.externalIdentities.link',
+ {
+ defaultMessage: 'External Identities API',
+ }
+);
+
+export const ACCESS_TOKEN_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.accessToken.label',
+ {
+ defaultMessage: 'Access Token',
+ }
+);
+
+export const ID_LABEL = i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.id.label', {
+ defaultMessage: 'ID',
+});
+
+export const LEARN_CUSTOM_FEATURES_BUTTON = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.learnCustom.features.button',
+ {
+ defaultMessage: 'Learn about Platinum features',
+ }
+);
+
+export const DOC_PERMISSIONS_DESCRIPTION = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.docPermissions.description',
+ {
+ defaultMessage:
+ 'Document-level permissions manage content access content on individual or group attributes. Allow or deny access to specific documents.',
+ }
+);
+
+export const CUSTOM_CALLOUT_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.customCallout.title',
+ {
+ defaultMessage: 'Getting started with custom sources?',
+ }
+);
+
+export const NO_CONTENT_MESSAGE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.noContentEmpty.message',
+ {
+ defaultMessage: "This source doesn't have any content yet",
+ }
+);
+
+export const CUSTOM_DOCUMENTATION_LINK = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.customSourceDocs.link',
+ {
+ defaultMessage: 'documentation',
+ }
+);
+
+export const TITLE_HEADING = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.title.heading',
+ {
+ defaultMessage: 'Title',
+ }
+);
+
+export const LAST_UPDATED_HEADING = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.lastUpdated.heading',
+ {
+ defaultMessage: 'Last updated',
+ }
+);
+
+export const GO_BUTTON = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.go.button',
+ {
+ defaultMessage: 'Go',
+ }
+);
+
+export const RESET_BUTTON = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.contentSources.displaySettings.reset.button',
+ {
+ defaultMessage: 'Reset',
+ }
+);
+
+export const SOURCE_CONTENT_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.sourceContent.title',
+ {
+ defaultMessage: 'Source content',
+ }
+);
+
+export const CONTENT_LOADING_TEXT = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.contentLoading.text',
+ {
+ defaultMessage: 'Loading content...',
+ }
+);
+
+export const REMOTE_SOURCE_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.remoteSource.label',
+ {
+ defaultMessage: 'Remote source',
+ }
+);
+
+export const CREATED_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.created.label',
+ {
+ defaultMessage: 'Created: ',
+ }
+);
+
+export const STATUS_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.status.label',
+ {
+ defaultMessage: 'Status: ',
+ }
+);
+
+export const READY_TEXT = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.ready.text',
+ {
+ defaultMessage: 'Ready to search',
+ }
+);
+
+export const SOURCE_SETTINGS_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.settings.title',
+ {
+ defaultMessage: 'Content source name',
+ }
+);
+
+export const SOURCE_SETTINGS_DESCRIPTION = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.settings.description',
+ {
+ defaultMessage: 'Customize the name of this content source.',
+ }
+);
+
+export const SOURCE_CONFIG_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.config.title',
+ {
+ defaultMessage: 'Content source configuration',
+ }
+);
+
+export const SOURCE_CONFIG_DESCRIPTION = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.config.description',
+ {
+ defaultMessage: 'Edit content source connector settings to change.',
+ }
+);
+
+export const SOURCE_CONFIG_LINK = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.config.link',
+ {
+ defaultMessage: 'Edit content source connector settings',
+ }
+);
+
+export const SOURCE_REMOVE_TITLE = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.remove.title',
+ {
+ defaultMessage: 'Remove this source',
+ }
+);
+
+export const SOURCE_REMOVE_DESCRIPTION = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.config.description',
+ {
+ defaultMessage: 'Edit content source connector settings to change.',
+ }
+);
+
+export const SOURCE_NAME_LABEL = i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.sources.sourceName.label',
+ {
+ defaultMessage: 'Source name',
+ }
+);
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx
index 766aa511ebb2d..2402a862a62e7 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx
@@ -22,6 +22,8 @@ import {
EuiOverlayMask,
} from '@elastic/eui';
+import { CANCEL_BUTTON } from '../../../constants';
+
import { GroupsLogic } from '../groups_logic';
const ADD_GROUP_HEADER = i18n.translate(
@@ -30,12 +32,6 @@ const ADD_GROUP_HEADER = i18n.translate(
defaultMessage: 'Add a group',
}
);
-const ADD_GROUP_CANCEL = i18n.translate(
- 'xpack.enterpriseSearch.workplaceSearch.groups.addGroup.cancel.action',
- {
- defaultMessage: 'Cancel',
- }
-);
const ADD_GROUP_SUBMIT = i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.groups.addGroup.submit.action',
{
@@ -72,7 +68,7 @@ export const AddGroupModal: React.FC<{}> = () => {
- {ADD_GROUP_CANCEL}
+ {CANCEL_BUTTON}
= ({
- {CANCEL_BUTTON_TEXT}
+ {CANCEL_BUTTON}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx
index 6f55c03746aa8..a1cf7b2ca0a25 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx
@@ -22,6 +22,8 @@ import {
EuiHorizontalRule,
} from '@elastic/eui';
+import { CANCEL_BUTTON } from '../../../constants';
+
import { AppLogic } from '../../../app_logic';
import { TruncatedContent } from '../../../../shared/truncate';
import { ContentSection } from '../../../components/shared/content_section';
@@ -99,12 +101,6 @@ const REMOVE_BUTTON_TEXT = i18n.translate(
defaultMessage: 'Remove group',
}
);
-const CANCEL_REMOVE_BUTTON_TEXT = i18n.translate(
- 'xpack.enterpriseSearch.workplaceSearch.groups.overview.cancelRemoveButtonText',
- {
- defaultMessage: 'Cancel',
- }
-);
const CONFIRM_TITLE_TEXT = i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmTitleText',
{
@@ -238,7 +234,7 @@ export const GroupOverview: React.FC = () => {
onConfirm={deleteGroup}
confirmButtonText={CONFIRM_REMOVE_BUTTON_TEXT}
title={CONFIRM_TITLE_TEXT}
- cancelButtonText={CANCEL_REMOVE_BUTTON_TEXT}
+ cancelButtonText={CANCEL_BUTTON}
defaultFocusedButton="confirm"
>
{CONFIRM_REMOVE_DESCRIPTION}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx
index 6911196afa81d..a81df1aab83bd 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx
@@ -16,6 +16,7 @@ import { ContentSection } from '../../components/shared/content_section';
import { TelemetryLogic } from '../../../shared/telemetry';
import { getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url';
import { SOURCE_DETAILS_PATH, getContentSourcePath } from '../../routes';
+import { RECENT_ACTIVITY_TITLE } from '../../constants';
import { AppLogic } from '../../app_logic';
import { OverviewLogic } from './overview_logic';
@@ -38,15 +39,7 @@ export const RecentActivity: React.FC = () => {
const { activityFeed } = useValues(OverviewLogic);
return (
-
- }
- headerSpacer="m"
- >
+
{activityFeed.length > 0 ? (
<>
diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts
index 545b3b1517145..32f08e685c75d 100644
--- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts
+++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { LegacyClusterClient } from 'src/core/server';
+import { ElasticsearchClient } from 'src/core/server';
import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks';
import {
ClusterClientAdapter,
@@ -15,20 +15,21 @@ import { contextMock } from './context.mock';
import { findOptionsSchema } from '../event_log_client';
import { delay } from '../lib/delay';
import { times } from 'lodash';
+import { DeeplyMockedKeys } from '@kbn/utility-types/jest';
+import { RequestEvent } from '@elastic/elasticsearch';
-type EsClusterClient = Pick, 'callAsInternalUser' | 'asScoped'>;
type MockedLogger = ReturnType;
let logger: MockedLogger;
-let clusterClient: EsClusterClient;
+let clusterClient: DeeplyMockedKeys;
let clusterClientAdapter: IClusterClientAdapter;
beforeEach(() => {
logger = loggingSystemMock.createLogger();
- clusterClient = elasticsearchServiceMock.createLegacyClusterClient();
+ clusterClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
clusterClientAdapter = new ClusterClientAdapter({
logger,
- clusterClientPromise: Promise.resolve(clusterClient),
+ elasticsearchClientPromise: Promise.resolve(clusterClient),
context: contextMock.create(),
});
});
@@ -38,16 +39,16 @@ describe('indexDocument', () => {
clusterClientAdapter.indexDocument({ body: { message: 'foo' }, index: 'event-log' });
await retryUntil('cluster client bulk called', () => {
- return clusterClient.callAsInternalUser.mock.calls.length !== 0;
+ return clusterClient.bulk.mock.calls.length !== 0;
});
- expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('bulk', {
+ expect(clusterClient.bulk).toHaveBeenCalledWith({
body: [{ create: { _index: 'event-log' } }, { message: 'foo' }],
});
});
test('should log an error when cluster client throws an error', async () => {
- clusterClient.callAsInternalUser.mockRejectedValue(new Error('expected failure'));
+ clusterClient.bulk.mockRejectedValue(new Error('expected failure'));
clusterClientAdapter.indexDocument({ body: { message: 'foo' }, index: 'event-log' });
await retryUntil('cluster client bulk called', () => {
return logger.error.mock.calls.length !== 0;
@@ -69,7 +70,7 @@ describe('shutdown()', () => {
const resultPromise = clusterClientAdapter.shutdown();
await retryUntil('cluster client bulk called', () => {
- return clusterClient.callAsInternalUser.mock.calls.length !== 0;
+ return clusterClient.bulk.mock.calls.length !== 0;
});
const result = await resultPromise;
@@ -85,7 +86,7 @@ describe('buffering documents', () => {
}
await retryUntil('cluster client bulk called', () => {
- return clusterClient.callAsInternalUser.mock.calls.length !== 0;
+ return clusterClient.bulk.mock.calls.length !== 0;
});
const expectedBody = [];
@@ -93,7 +94,7 @@ describe('buffering documents', () => {
expectedBody.push({ create: { _index: 'event-log' } }, { message: `foo ${i}` });
}
- expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('bulk', {
+ expect(clusterClient.bulk).toHaveBeenCalledWith({
body: expectedBody,
});
});
@@ -105,7 +106,7 @@ describe('buffering documents', () => {
}
await retryUntil('cluster client bulk called', () => {
- return clusterClient.callAsInternalUser.mock.calls.length >= 2;
+ return clusterClient.bulk.mock.calls.length >= 2;
});
const expectedBody = [];
@@ -113,18 +114,18 @@ describe('buffering documents', () => {
expectedBody.push({ create: { _index: 'event-log' } }, { message: `foo ${i}` });
}
- expect(clusterClient.callAsInternalUser).toHaveBeenNthCalledWith(1, 'bulk', {
+ expect(clusterClient.bulk).toHaveBeenNthCalledWith(1, {
body: expectedBody,
});
- expect(clusterClient.callAsInternalUser).toHaveBeenNthCalledWith(2, 'bulk', {
+ expect(clusterClient.bulk).toHaveBeenNthCalledWith(2, {
body: [{ create: { _index: 'event-log' } }, { message: `foo 100` }],
});
});
test('should handle lots of docs correctly with a delay in the bulk index', async () => {
// @ts-ignore
- clusterClient.callAsInternalUser.mockImplementation = async () => await delay(100);
+ clusterClient.bulk.mockImplementation = async () => await delay(100);
const docs = times(EVENT_BUFFER_LENGTH * 10, (i) => ({
body: { message: `foo ${i}` },
@@ -137,7 +138,7 @@ describe('buffering documents', () => {
}
await retryUntil('cluster client bulk called', () => {
- return clusterClient.callAsInternalUser.mock.calls.length >= 10;
+ return clusterClient.bulk.mock.calls.length >= 10;
});
for (let i = 0; i < 10; i++) {
@@ -149,7 +150,7 @@ describe('buffering documents', () => {
);
}
- expect(clusterClient.callAsInternalUser).toHaveBeenNthCalledWith(i + 1, 'bulk', {
+ expect(clusterClient.bulk).toHaveBeenNthCalledWith(i + 1, {
body: expectedBody,
});
}
@@ -164,19 +165,19 @@ describe('doesIlmPolicyExist', () => {
test('should call cluster with proper arguments', async () => {
await clusterClientAdapter.doesIlmPolicyExist('foo');
- expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('transport.request', {
+ expect(clusterClient.transport.request).toHaveBeenCalledWith({
method: 'GET',
path: '/_ilm/policy/foo',
});
});
test('should return false when 404 error is returned by Elasticsearch', async () => {
- clusterClient.callAsInternalUser.mockRejectedValue(notFoundError);
+ clusterClient.transport.request.mockRejectedValue(notFoundError);
await expect(clusterClientAdapter.doesIlmPolicyExist('foo')).resolves.toEqual(false);
});
test('should throw error when error is not 404', async () => {
- clusterClient.callAsInternalUser.mockRejectedValue(new Error('Fail'));
+ clusterClient.transport.request.mockRejectedValue(new Error('Fail'));
await expect(
clusterClientAdapter.doesIlmPolicyExist('foo')
).rejects.toThrowErrorMatchingInlineSnapshot(`"error checking existance of ilm policy: Fail"`);
@@ -189,9 +190,9 @@ describe('doesIlmPolicyExist', () => {
describe('createIlmPolicy', () => {
test('should call cluster client with given policy', async () => {
- clusterClient.callAsInternalUser.mockResolvedValue({ success: true });
+ clusterClient.transport.request.mockResolvedValue(asApiResponse({ success: true }));
await clusterClientAdapter.createIlmPolicy('foo', { args: true });
- expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('transport.request', {
+ expect(clusterClient.transport.request).toHaveBeenCalledWith({
method: 'PUT',
path: '/_ilm/policy/foo',
body: { args: true },
@@ -199,7 +200,7 @@ describe('createIlmPolicy', () => {
});
test('should throw error when call cluster client throws', async () => {
- clusterClient.callAsInternalUser.mockRejectedValue(new Error('Fail'));
+ clusterClient.transport.request.mockRejectedValue(new Error('Fail'));
await expect(
clusterClientAdapter.createIlmPolicy('foo', { args: true })
).rejects.toThrowErrorMatchingInlineSnapshot(`"error creating ilm policy: Fail"`);
@@ -209,23 +210,23 @@ describe('createIlmPolicy', () => {
describe('doesIndexTemplateExist', () => {
test('should call cluster with proper arguments', async () => {
await clusterClientAdapter.doesIndexTemplateExist('foo');
- expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('indices.existsTemplate', {
+ expect(clusterClient.indices.existsTemplate).toHaveBeenCalledWith({
name: 'foo',
});
});
test('should return true when call cluster returns true', async () => {
- clusterClient.callAsInternalUser.mockResolvedValue(true);
+ clusterClient.indices.existsTemplate.mockResolvedValue(asApiResponse(true));
await expect(clusterClientAdapter.doesIndexTemplateExist('foo')).resolves.toEqual(true);
});
test('should return false when call cluster returns false', async () => {
- clusterClient.callAsInternalUser.mockResolvedValue(false);
+ clusterClient.indices.existsTemplate.mockResolvedValue(asApiResponse(false));
await expect(clusterClientAdapter.doesIndexTemplateExist('foo')).resolves.toEqual(false);
});
test('should throw error when call cluster throws an error', async () => {
- clusterClient.callAsInternalUser.mockRejectedValue(new Error('Fail'));
+ clusterClient.indices.existsTemplate.mockRejectedValue(new Error('Fail'));
await expect(
clusterClientAdapter.doesIndexTemplateExist('foo')
).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -237,7 +238,7 @@ describe('doesIndexTemplateExist', () => {
describe('createIndexTemplate', () => {
test('should call cluster with given template', async () => {
await clusterClientAdapter.createIndexTemplate('foo', { args: true });
- expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('indices.putTemplate', {
+ expect(clusterClient.indices.putTemplate).toHaveBeenCalledWith({
name: 'foo',
create: true,
body: { args: true },
@@ -245,16 +246,16 @@ describe('createIndexTemplate', () => {
});
test(`should throw error if index template still doesn't exist after error is thrown`, async () => {
- clusterClient.callAsInternalUser.mockRejectedValueOnce(new Error('Fail'));
- clusterClient.callAsInternalUser.mockResolvedValueOnce(false);
+ clusterClient.indices.putTemplate.mockRejectedValueOnce(new Error('Fail'));
+ clusterClient.indices.existsTemplate.mockResolvedValueOnce(asApiResponse(false));
await expect(
clusterClientAdapter.createIndexTemplate('foo', { args: true })
).rejects.toThrowErrorMatchingInlineSnapshot(`"error creating index template: Fail"`);
});
test('should not throw error if index template exists after error is thrown', async () => {
- clusterClient.callAsInternalUser.mockRejectedValueOnce(new Error('Fail'));
- clusterClient.callAsInternalUser.mockResolvedValueOnce(true);
+ clusterClient.indices.putTemplate.mockRejectedValueOnce(new Error('Fail'));
+ clusterClient.indices.existsTemplate.mockResolvedValueOnce(asApiResponse(true));
await clusterClientAdapter.createIndexTemplate('foo', { args: true });
});
});
@@ -262,23 +263,23 @@ describe('createIndexTemplate', () => {
describe('doesAliasExist', () => {
test('should call cluster with proper arguments', async () => {
await clusterClientAdapter.doesAliasExist('foo');
- expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('indices.existsAlias', {
+ expect(clusterClient.indices.existsAlias).toHaveBeenCalledWith({
name: 'foo',
});
});
test('should return true when call cluster returns true', async () => {
- clusterClient.callAsInternalUser.mockResolvedValueOnce(true);
+ clusterClient.indices.existsAlias.mockResolvedValueOnce(asApiResponse(true));
await expect(clusterClientAdapter.doesAliasExist('foo')).resolves.toEqual(true);
});
test('should return false when call cluster returns false', async () => {
- clusterClient.callAsInternalUser.mockResolvedValueOnce(false);
+ clusterClient.indices.existsAlias.mockResolvedValueOnce(asApiResponse(false));
await expect(clusterClientAdapter.doesAliasExist('foo')).resolves.toEqual(false);
});
test('should throw error when call cluster throws an error', async () => {
- clusterClient.callAsInternalUser.mockRejectedValue(new Error('Fail'));
+ clusterClient.indices.existsAlias.mockRejectedValue(new Error('Fail'));
await expect(
clusterClientAdapter.doesAliasExist('foo')
).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -290,14 +291,14 @@ describe('doesAliasExist', () => {
describe('createIndex', () => {
test('should call cluster with proper arguments', async () => {
await clusterClientAdapter.createIndex('foo');
- expect(clusterClient.callAsInternalUser).toHaveBeenCalledWith('indices.create', {
+ expect(clusterClient.indices.create).toHaveBeenCalledWith({
index: 'foo',
body: {},
});
});
test('should throw error when not getting an error of type resource_already_exists_exception', async () => {
- clusterClient.callAsInternalUser.mockRejectedValue(new Error('Fail'));
+ clusterClient.indices.create.mockRejectedValue(new Error('Fail'));
await expect(
clusterClientAdapter.createIndex('foo')
).rejects.toThrowErrorMatchingInlineSnapshot(`"error creating initial index: Fail"`);
@@ -312,7 +313,7 @@ describe('createIndex', () => {
type: 'resource_already_exists_exception',
},
};
- clusterClient.callAsInternalUser.mockRejectedValue(err);
+ clusterClient.indices.create.mockRejectedValue(err);
await clusterClientAdapter.createIndex('foo');
});
});
@@ -321,12 +322,14 @@ describe('queryEventsBySavedObject', () => {
const DEFAULT_OPTIONS = findOptionsSchema.validate({});
test('should call cluster with proper arguments with non-default namespace', async () => {
- clusterClient.callAsInternalUser.mockResolvedValue({
- hits: {
- hits: [],
- total: { value: 0 },
- },
- });
+ clusterClient.search.mockResolvedValue(
+ asApiResponse({
+ hits: {
+ hits: [],
+ total: { value: 0 },
+ },
+ })
+ );
await clusterClientAdapter.queryEventsBySavedObjects(
'index-name',
'namespace',
@@ -335,14 +338,14 @@ describe('queryEventsBySavedObject', () => {
DEFAULT_OPTIONS
);
- const [method, query] = clusterClient.callAsInternalUser.mock.calls[0];
- expect(method).toEqual('search');
+ const [query] = clusterClient.search.mock.calls[0];
expect(query).toMatchInlineSnapshot(`
Object {
"body": Object {
"from": 0,
"query": Object {
"bool": Object {
+ "filter": Array [],
"must": Array [
Object {
"nested": Object {
@@ -400,12 +403,14 @@ describe('queryEventsBySavedObject', () => {
});
test('should call cluster with proper arguments with default namespace', async () => {
- clusterClient.callAsInternalUser.mockResolvedValue({
- hits: {
- hits: [],
- total: { value: 0 },
- },
- });
+ clusterClient.search.mockResolvedValue(
+ asApiResponse({
+ hits: {
+ hits: [],
+ total: { value: 0 },
+ },
+ })
+ );
await clusterClientAdapter.queryEventsBySavedObjects(
'index-name',
undefined,
@@ -414,14 +419,14 @@ describe('queryEventsBySavedObject', () => {
DEFAULT_OPTIONS
);
- const [method, query] = clusterClient.callAsInternalUser.mock.calls[0];
- expect(method).toEqual('search');
+ const [query] = clusterClient.search.mock.calls[0];
expect(query).toMatchInlineSnapshot(`
Object {
"body": Object {
"from": 0,
"query": Object {
"bool": Object {
+ "filter": Array [],
"must": Array [
Object {
"nested": Object {
@@ -481,12 +486,14 @@ describe('queryEventsBySavedObject', () => {
});
test('should call cluster with sort', async () => {
- clusterClient.callAsInternalUser.mockResolvedValue({
- hits: {
- hits: [],
- total: { value: 0 },
- },
- });
+ clusterClient.search.mockResolvedValue(
+ asApiResponse({
+ hits: {
+ hits: [],
+ total: { value: 0 },
+ },
+ })
+ );
await clusterClientAdapter.queryEventsBySavedObjects(
'index-name',
'namespace',
@@ -495,8 +502,7 @@ describe('queryEventsBySavedObject', () => {
{ ...DEFAULT_OPTIONS, sort_field: 'event.end', sort_order: 'desc' }
);
- const [method, query] = clusterClient.callAsInternalUser.mock.calls[0];
- expect(method).toEqual('search');
+ const [query] = clusterClient.search.mock.calls[0];
expect(query).toMatchObject({
index: 'index-name',
body: {
@@ -506,12 +512,14 @@ describe('queryEventsBySavedObject', () => {
});
test('supports open ended date', async () => {
- clusterClient.callAsInternalUser.mockResolvedValue({
- hits: {
- hits: [],
- total: { value: 0 },
- },
- });
+ clusterClient.search.mockResolvedValue(
+ asApiResponse({
+ hits: {
+ hits: [],
+ total: { value: 0 },
+ },
+ })
+ );
const start = '2020-07-08T00:52:28.350Z';
@@ -523,14 +531,14 @@ describe('queryEventsBySavedObject', () => {
{ ...DEFAULT_OPTIONS, start }
);
- const [method, query] = clusterClient.callAsInternalUser.mock.calls[0];
- expect(method).toEqual('search');
+ const [query] = clusterClient.search.mock.calls[0];
expect(query).toMatchInlineSnapshot(`
Object {
"body": Object {
"from": 0,
"query": Object {
"bool": Object {
+ "filter": Array [],
"must": Array [
Object {
"nested": Object {
@@ -595,12 +603,14 @@ describe('queryEventsBySavedObject', () => {
});
test('supports optional date range', async () => {
- clusterClient.callAsInternalUser.mockResolvedValue({
- hits: {
- hits: [],
- total: { value: 0 },
- },
- });
+ clusterClient.search.mockResolvedValue(
+ asApiResponse({
+ hits: {
+ hits: [],
+ total: { value: 0 },
+ },
+ })
+ );
const start = '2020-07-08T00:52:28.350Z';
const end = '2020-07-08T00:00:00.000Z';
@@ -613,14 +623,14 @@ describe('queryEventsBySavedObject', () => {
{ ...DEFAULT_OPTIONS, start, end }
);
- const [method, query] = clusterClient.callAsInternalUser.mock.calls[0];
- expect(method).toEqual('search');
+ const [query] = clusterClient.search.mock.calls[0];
expect(query).toMatchInlineSnapshot(`
Object {
"body": Object {
"from": 0,
"query": Object {
"bool": Object {
+ "filter": Array [],
"must": Array [
Object {
"nested": Object {
@@ -697,6 +707,12 @@ type RetryableFunction = () => boolean;
const RETRY_UNTIL_DEFAULT_COUNT = 20;
const RETRY_UNTIL_DEFAULT_WAIT = 1000; // milliseconds
+function asApiResponse(body: T): RequestEvent {
+ return {
+ body,
+ } as RequestEvent;
+}
+
async function retryUntil(
label: string,
fn: RetryableFunction,
diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts
index 5d4c33f319fcc..4488dc74556ca 100644
--- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts
+++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts
@@ -5,20 +5,18 @@
*/
import { Subject } from 'rxjs';
-import { bufferTime, filter, switchMap } from 'rxjs/operators';
+import { bufferTime, filter as rxFilter, switchMap } from 'rxjs/operators';
import { reject, isUndefined } from 'lodash';
-import { Client } from 'elasticsearch';
import type { PublicMethodsOf } from '@kbn/utility-types';
-import { Logger, LegacyClusterClient } from 'src/core/server';
-import { ESSearchResponse } from '../../../../typings/elasticsearch';
+import { Logger, ElasticsearchClient } from 'src/core/server';
import { EsContext } from '.';
import { IEvent, IValidatedEvent, SAVED_OBJECT_REL_PRIMARY } from '../types';
import { FindOptionsType } from '../event_log_client';
+import { esKuery } from '../../../../../src/plugins/data/server';
export const EVENT_BUFFER_TIME = 1000; // milliseconds
export const EVENT_BUFFER_LENGTH = 100;
-export type EsClusterClient = Pick;
export type IClusterClientAdapter = PublicMethodsOf;
export interface Doc {
@@ -28,7 +26,7 @@ export interface Doc {
export interface ConstructorOpts {
logger: Logger;
- clusterClientPromise: Promise;
+ elasticsearchClientPromise: Promise;
context: EsContext;
}
@@ -41,14 +39,14 @@ export interface QueryEventsBySavedObjectResult {
export class ClusterClientAdapter {
private readonly logger: Logger;
- private readonly clusterClientPromise: Promise;
+ private readonly elasticsearchClientPromise: Promise;
private readonly docBuffer$: Subject;
private readonly context: EsContext;
private readonly docsBufferedFlushed: Promise;
constructor(opts: ConstructorOpts) {
this.logger = opts.logger;
- this.clusterClientPromise = opts.clusterClientPromise;
+ this.elasticsearchClientPromise = opts.elasticsearchClientPromise;
this.context = opts.context;
this.docBuffer$ = new Subject();
@@ -58,7 +56,7 @@ export class ClusterClientAdapter {
this.docsBufferedFlushed = this.docBuffer$
.pipe(
bufferTime(EVENT_BUFFER_TIME, null, EVENT_BUFFER_LENGTH),
- filter((docs) => docs.length > 0),
+ rxFilter((docs) => docs.length > 0),
switchMap(async (docs) => await this.indexDocuments(docs))
)
.toPromise();
@@ -97,7 +95,8 @@ export class ClusterClientAdapter {
}
try {
- await this.callEs>('bulk', { body: bulkBody });
+ const esClient = await this.elasticsearchClientPromise;
+ await esClient.bulk({ body: bulkBody });
} catch (err) {
this.logger.error(
`error writing bulk events: "${err.message}"; docs: ${JSON.stringify(bulkBody)}`
@@ -111,7 +110,8 @@ export class ClusterClientAdapter {
path: `/_ilm/policy/${policyName}`,
};
try {
- await this.callEs('transport.request', request);
+ const esClient = await this.elasticsearchClientPromise;
+ await esClient.transport.request(request);
} catch (err) {
if (err.statusCode === 404) return false;
throw new Error(`error checking existance of ilm policy: ${err.message}`);
@@ -119,14 +119,15 @@ export class ClusterClientAdapter {
return true;
}
- public async createIlmPolicy(policyName: string, policy: unknown): Promise {
+ public async createIlmPolicy(policyName: string, policy: Record): Promise {
const request = {
method: 'PUT',
path: `/_ilm/policy/${policyName}`,
body: policy,
};
try {
- await this.callEs('transport.request', request);
+ const esClient = await this.elasticsearchClientPromise;
+ await esClient.transport.request(request);
} catch (err) {
throw new Error(`error creating ilm policy: ${err.message}`);
}
@@ -135,27 +136,18 @@ export class ClusterClientAdapter {
public async doesIndexTemplateExist(name: string): Promise {
let result;
try {
- result = await this.callEs>(
- 'indices.existsTemplate',
- { name }
- );
+ const esClient = await this.elasticsearchClientPromise;
+ result = (await esClient.indices.existsTemplate({ name })).body;
} catch (err) {
throw new Error(`error checking existance of index template: ${err.message}`);
}
return result as boolean;
}
- public async createIndexTemplate(name: string, template: unknown): Promise {
- const addTemplateParams = {
- name,
- create: true,
- body: template,
- };
+ public async createIndexTemplate(name: string, template: Record): Promise {
try {
- await this.callEs>(
- 'indices.putTemplate',
- addTemplateParams
- );
+ const esClient = await this.elasticsearchClientPromise;
+ await esClient.indices.putTemplate({ name, body: template, create: true });
} catch (err) {
// The error message doesn't have a type attribute we can look to guarantee it's due
// to the template already existing (only long message) so we'll check ourselves to see
@@ -171,19 +163,21 @@ export class ClusterClientAdapter {
public async doesAliasExist(name: string): Promise {
let result;
try {
- result = await this.callEs>(
- 'indices.existsAlias',
- { name }
- );
+ const esClient = await this.elasticsearchClientPromise;
+ result = (await esClient.indices.existsAlias({ name })).body;
} catch (err) {
throw new Error(`error checking existance of initial index: ${err.message}`);
}
return result as boolean;
}
- public async createIndex(name: string, body: unknown = {}): Promise {
+ public async createIndex(
+ name: string,
+ body: string | Record = {}
+ ): Promise {
try {
- await this.callEs>('indices.create', {
+ const esClient = await this.elasticsearchClientPromise;
+ await esClient.indices.create({
index: name,
body,
});
@@ -200,7 +194,7 @@ export class ClusterClientAdapter {
type: string,
ids: string[],
// eslint-disable-next-line @typescript-eslint/naming-convention
- { page, per_page: perPage, start, end, sort_field, sort_order }: FindOptionsType
+ { page, per_page: perPage, start, end, sort_field, sort_order, filter }: FindOptionsType
): Promise {
const defaultNamespaceQuery = {
bool: {
@@ -220,12 +214,26 @@ export class ClusterClientAdapter {
};
const namespaceQuery = namespace === undefined ? defaultNamespaceQuery : namedNamespaceQuery;
+ const esClient = await this.elasticsearchClientPromise;
+ let dslFilterQuery;
+ try {
+ dslFilterQuery = filter
+ ? esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(filter))
+ : [];
+ } catch (err) {
+ this.debug(`Invalid kuery syntax for the filter (${filter}) error:`, {
+ message: err.message,
+ statusCode: err.statusCode,
+ });
+ throw err;
+ }
const body = {
size: perPage,
from: (page - 1) * perPage,
sort: { [sort_field]: { order: sort_order } },
query: {
bool: {
+ filter: dslFilterQuery,
must: reject(
[
{
@@ -283,8 +291,10 @@ export class ClusterClientAdapter {
try {
const {
- hits: { hits, total },
- }: ESSearchResponse = await this.callEs('search', {
+ body: {
+ hits: { hits, total },
+ },
+ } = await esClient.search({
index,
track_total_hits: true,
body,
@@ -293,7 +303,7 @@ export class ClusterClientAdapter {
page,
per_page: perPage,
total: total.value,
- data: hits.map((hit) => hit._source) as IValidatedEvent[],
+ data: hits.map((hit: { _source: unknown }) => hit._source) as IValidatedEvent[],
};
} catch (err) {
throw new Error(
@@ -302,24 +312,6 @@ export class ClusterClientAdapter {
}
}
- // We have a common problem typing ES-DSL Queries
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- private async callEs(operation: string, body?: any) {
- try {
- this.debug(`callEs(${operation}) calls:`, body);
- const clusterClient = await this.clusterClientPromise;
- const result = await clusterClient.callAsInternalUser(operation, body);
- this.debug(`callEs(${operation}) result:`, result);
- return result as ESQueryResult;
- } catch (err) {
- this.debug(`callEs(${operation}) error:`, {
- message: err.message,
- statusCode: err.statusCode,
- });
- throw err;
- }
- }
-
private debug(message: string, object?: unknown) {
const objectString = object == null ? '' : JSON.stringify(object);
this.logger.debug(`esContext: ${message} ${objectString}`);
diff --git a/x-pack/plugins/event_log/server/es/context.test.ts b/x-pack/plugins/event_log/server/es/context.test.ts
index 5f26399618e38..fc137b4e45b13 100644
--- a/x-pack/plugins/event_log/server/es/context.test.ts
+++ b/x-pack/plugins/event_log/server/es/context.test.ts
@@ -5,27 +5,28 @@
*/
import { createEsContext } from './context';
-import { LegacyClusterClient, Logger } from '../../../../../src/core/server';
+import { ElasticsearchClient, Logger } from '../../../../../src/core/server';
import { elasticsearchServiceMock, loggingSystemMock } from '../../../../../src/core/server/mocks';
+import { DeeplyMockedKeys } from '@kbn/utility-types/jest';
+import { RequestEvent } from '@elastic/elasticsearch';
jest.mock('../lib/../../../../package.json', () => ({ version: '1.2.3' }));
jest.mock('./init');
-type EsClusterClient = Pick, 'callAsInternalUser' | 'asScoped'>;
let logger: Logger;
-let clusterClient: EsClusterClient;
+let elasticsearchClient: DeeplyMockedKeys;
beforeEach(() => {
logger = loggingSystemMock.createLogger();
- clusterClient = elasticsearchServiceMock.createLegacyClusterClient();
+ elasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
});
describe('createEsContext', () => {
test('should return is ready state as falsy if not initialized', () => {
const context = createEsContext({
logger,
- clusterClientPromise: Promise.resolve(clusterClient),
indexNameRoot: 'test0',
kibanaVersion: '1.2.3',
+ elasticsearchClientPromise: Promise.resolve(elasticsearchClient),
});
expect(context.initialized).toBeFalsy();
@@ -37,9 +38,9 @@ describe('createEsContext', () => {
test('should return esNames', () => {
const context = createEsContext({
logger,
- clusterClientPromise: Promise.resolve(clusterClient),
indexNameRoot: 'test-index',
kibanaVersion: '1.2.3',
+ elasticsearchClientPromise: Promise.resolve(elasticsearchClient),
});
const esNames = context.esNames;
@@ -57,12 +58,12 @@ describe('createEsContext', () => {
test('should return exist false for esAdapter ilm policy, index template and alias before initialize', async () => {
const context = createEsContext({
logger,
- clusterClientPromise: Promise.resolve(clusterClient),
indexNameRoot: 'test1',
kibanaVersion: '1.2.3',
+ elasticsearchClientPromise: Promise.resolve(elasticsearchClient),
});
- clusterClient.callAsInternalUser.mockResolvedValue(false);
-
+ elasticsearchClient.indices.existsTemplate.mockResolvedValue(asApiResponse(false));
+ elasticsearchClient.indices.existsAlias.mockResolvedValue(asApiResponse(false));
const doesAliasExist = await context.esAdapter.doesAliasExist(context.esNames.alias);
expect(doesAliasExist).toBeFalsy();
@@ -75,11 +76,11 @@ describe('createEsContext', () => {
test('should return exist true for esAdapter ilm policy, index template and alias after initialize', async () => {
const context = createEsContext({
logger,
- clusterClientPromise: Promise.resolve(clusterClient),
indexNameRoot: 'test2',
kibanaVersion: '1.2.3',
+ elasticsearchClientPromise: Promise.resolve(elasticsearchClient),
});
- clusterClient.callAsInternalUser.mockResolvedValue(true);
+ elasticsearchClient.indices.existsTemplate.mockResolvedValue(asApiResponse(true));
context.initialize();
const doesIlmPolicyExist = await context.esAdapter.doesIlmPolicyExist(
@@ -100,12 +101,18 @@ describe('createEsContext', () => {
jest.requireMock('./init').initializeEs.mockResolvedValue(false);
const context = createEsContext({
logger,
- clusterClientPromise: Promise.resolve(clusterClient),
indexNameRoot: 'test2',
kibanaVersion: '1.2.3',
+ elasticsearchClientPromise: Promise.resolve(elasticsearchClient),
});
context.initialize();
const success = await context.waitTillReady();
expect(success).toBe(false);
});
});
+
+function asApiResponse(body: T): RequestEvent {
+ return {
+ body,
+ } as RequestEvent;
+}
diff --git a/x-pack/plugins/event_log/server/es/context.ts b/x-pack/plugins/event_log/server/es/context.ts
index c1777d6979c5c..26f249d3b2c06 100644
--- a/x-pack/plugins/event_log/server/es/context.ts
+++ b/x-pack/plugins/event_log/server/es/context.ts
@@ -4,15 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { Logger, LegacyClusterClient } from 'src/core/server';
+import { Logger, ElasticsearchClient } from 'src/core/server';
import { EsNames, getEsNames } from './names';
import { initializeEs } from './init';
import { ClusterClientAdapter, IClusterClientAdapter } from './cluster_client_adapter';
import { createReadySignal, ReadySignal } from '../lib/ready_signal';
-export type EsClusterClient = Pick;
-
export interface EsContext {
logger: Logger;
esNames: EsNames;
@@ -34,9 +32,9 @@ export function createEsContext(params: EsContextCtorParams): EsContext {
export interface EsContextCtorParams {
logger: Logger;
- clusterClientPromise: Promise;
indexNameRoot: string;
kibanaVersion: string;
+ elasticsearchClientPromise: Promise;
}
class EsContextImpl implements EsContext {
@@ -53,7 +51,7 @@ class EsContextImpl implements EsContext {
this.initialized = false;
this.esAdapter = new ClusterClientAdapter({
logger: params.logger,
- clusterClientPromise: params.clusterClientPromise,
+ elasticsearchClientPromise: params.elasticsearchClientPromise,
context: this,
});
}
diff --git a/x-pack/plugins/event_log/server/es/index.ts b/x-pack/plugins/event_log/server/es/index.ts
index ad1409e33589f..adc7ed011aa14 100644
--- a/x-pack/plugins/event_log/server/es/index.ts
+++ b/x-pack/plugins/event_log/server/es/index.ts
@@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { EsClusterClient, EsContext, createEsContext } from './context';
+export { EsContext, createEsContext } from './context';
diff --git a/x-pack/plugins/event_log/server/event_log_client.ts b/x-pack/plugins/event_log/server/event_log_client.ts
index 63453c6327da2..091f997fe62ea 100644
--- a/x-pack/plugins/event_log/server/event_log_client.ts
+++ b/x-pack/plugins/event_log/server/event_log_client.ts
@@ -6,14 +6,14 @@
import { Observable } from 'rxjs';
import { schema, TypeOf } from '@kbn/config-schema';
-import { LegacyClusterClient, KibanaRequest } from 'src/core/server';
+import { IClusterClient, KibanaRequest } from 'src/core/server';
import { SpacesServiceStart } from '../../spaces/server';
import { EsContext } from './es';
import { IEventLogClient } from './types';
import { QueryEventsBySavedObjectResult } from './es/cluster_client_adapter';
import { SavedObjectBulkGetterResult } from './saved_object_provider_registry';
-export type PluginClusterClient = Pick;
+export type PluginClusterClient = Pick;
export type AdminClusterClient$ = Observable;
const optionalDateFieldSchema = schema.maybe(
@@ -48,12 +48,13 @@ export const findOptionsSchema = schema.object({
sort_order: schema.oneOf([schema.literal('asc'), schema.literal('desc')], {
defaultValue: 'asc',
}),
+ filter: schema.maybe(schema.string()),
});
// page & perPage are required, other fields are optional
// using schema.maybe allows us to set undefined, but not to make the field optional
export type FindOptionsType = Pick<
TypeOf,
- 'page' | 'per_page' | 'sort_field' | 'sort_order'
+ 'page' | 'per_page' | 'sort_field' | 'sort_order' | 'filter'
> &
Partial>;
diff --git a/x-pack/plugins/event_log/server/event_log_service.ts b/x-pack/plugins/event_log/server/event_log_service.ts
index 9249288d33939..0bc675fee928d 100644
--- a/x-pack/plugins/event_log/server/event_log_service.ts
+++ b/x-pack/plugins/event_log/server/event_log_service.ts
@@ -5,14 +5,14 @@
*/
import { Observable } from 'rxjs';
-import { LegacyClusterClient } from 'src/core/server';
+import { IClusterClient } from 'src/core/server';
import { Plugin } from './plugin';
import { EsContext } from './es';
import { IEvent, IEventLogger, IEventLogService, IEventLogConfig } from './types';
import { EventLogger } from './event_logger';
import { SavedObjectProvider, SavedObjectProviderRegistry } from './saved_object_provider_registry';
-export type PluginClusterClient = Pick;
+export type PluginClusterClient = Pick;
export type AdminClusterClient$ = Observable;
type SystemLogger = Plugin['systemLogger'];
diff --git a/x-pack/plugins/event_log/server/event_log_start_service.ts b/x-pack/plugins/event_log/server/event_log_start_service.ts
index 51dd7d6e95d15..82b8f06c251a3 100644
--- a/x-pack/plugins/event_log/server/event_log_start_service.ts
+++ b/x-pack/plugins/event_log/server/event_log_start_service.ts
@@ -5,14 +5,14 @@
*/
import { Observable } from 'rxjs';
-import { LegacyClusterClient, KibanaRequest } from 'src/core/server';
+import { IClusterClient, KibanaRequest } from 'src/core/server';
import { SpacesServiceStart } from '../../spaces/server';
import { EsContext } from './es';
import { IEventLogClientService } from './types';
import { EventLogClient } from './event_log_client';
import { SavedObjectProviderRegistry } from './saved_object_provider_registry';
-export type PluginClusterClient = Pick;
+export type PluginClusterClient = Pick;
export type AdminClusterClient$ = Observable;
interface EventLogServiceCtorParams {
diff --git a/x-pack/plugins/event_log/server/plugin.ts b/x-pack/plugins/event_log/server/plugin.ts
index 3bf726de71856..e2e31864eb31f 100644
--- a/x-pack/plugins/event_log/server/plugin.ts
+++ b/x-pack/plugins/event_log/server/plugin.ts
@@ -12,7 +12,7 @@ import {
Logger,
Plugin as CorePlugin,
PluginInitializerContext,
- LegacyClusterClient,
+ IClusterClient,
SharedGlobalConfig,
IContextProvider,
} from 'src/core/server';
@@ -33,7 +33,7 @@ import { EventLogClientService } from './event_log_start_service';
import { SavedObjectProviderRegistry } from './saved_object_provider_registry';
import { findByIdsRoute } from './routes/find_by_ids';
-export type PluginClusterClient = Pick;
+export type PluginClusterClient = Pick;
const PROVIDER = 'eventLog';
@@ -77,9 +77,9 @@ export class Plugin implements CorePlugin elasticsearch.legacy.client),
+ .then(([{ elasticsearch }]) => elasticsearch.client.asInternalUser),
kibanaVersion: this.kibanaVersion,
});
diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts
index dcc686e565b8e..7b4149819dc78 100644
--- a/x-pack/plugins/fleet/server/saved_objects/index.ts
+++ b/x-pack/plugins/fleet/server/saved_objects/index.ts
@@ -6,7 +6,10 @@
import { SavedObjectsServiceSetup, SavedObjectsType } from 'kibana/server';
import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server';
-import { migratePackagePolicyToV7110 } from '../../../security_solution/common';
+import {
+ migratePackagePolicyToV7110,
+ migratePackagePolicyToV7120,
+} from '../../../security_solution/common';
import {
OUTPUT_SAVED_OBJECT_TYPE,
AGENT_POLICY_SAVED_OBJECT_TYPE,
@@ -273,6 +276,7 @@ const getSavedObjectTypes = (
migrations: {
'7.10.0': migratePackagePolicyToV7100,
'7.11.0': migratePackagePolicyToV7110,
+ '7.12.0': migratePackagePolicyToV7120,
},
},
[PACKAGES_SAVED_OBJECT_TYPE]: {
diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts
index fafd0c2772842..7e2c634b2f1cf 100644
--- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts
@@ -17,7 +17,7 @@ import {
PolicyData,
SafeEndpointEvent,
} from './types';
-import { factory as policyFactory } from './models/policy_config';
+import { policyFactory } from './models/policy_config';
import {
ancestryArray,
entityIDSafeVersion,
diff --git a/x-pack/plugins/security_solution/common/endpoint/index_data.ts b/x-pack/plugins/security_solution/common/endpoint/index_data.ts
index b3259b19cf2c0..b9d7f6dfe7a5a 100644
--- a/x-pack/plugins/security_solution/common/endpoint/index_data.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/index_data.ts
@@ -30,7 +30,7 @@ import {
PostAgentAcksResponse,
PostAgentAcksRequest,
} from '../../../fleet/common';
-import { factory as policyConfigFactory } from './models/policy_config';
+import { policyFactory as policyConfigFactory } from './models/policy_config';
import { HostMetadata } from './types';
import { KbnClientWithApiKeySupport } from '../../scripts/endpoint/kbn_client_with_api_key_support';
diff --git a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts
index 14941b019421b..614aac6ea8041 100644
--- a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts
@@ -7,9 +7,9 @@
import { PolicyConfig, ProtectionModes } from '../types';
/**
- * Return a new default `PolicyConfig`.
+ * Return a new default `PolicyConfig` for platinum and above licenses
*/
-export const factory = (): PolicyConfig => {
+export const policyFactory = (): PolicyConfig => {
return {
windows: {
events: {
@@ -24,11 +24,18 @@ export const factory = (): PolicyConfig => {
malware: {
mode: ProtectionModes.prevent,
},
+ ransomware: {
+ mode: ProtectionModes.prevent,
+ },
popup: {
malware: {
message: '',
enabled: true,
},
+ ransomware: {
+ message: '',
+ enabled: true,
+ },
},
logging: {
file: 'info',
@@ -46,11 +53,18 @@ export const factory = (): PolicyConfig => {
malware: {
mode: ProtectionModes.prevent,
},
+ ransomware: {
+ mode: ProtectionModes.prevent,
+ },
popup: {
malware: {
message: '',
enabled: true,
},
+ ransomware: {
+ message: '',
+ enabled: true,
+ },
},
logging: {
file: 'info',
@@ -69,6 +83,51 @@ export const factory = (): PolicyConfig => {
};
};
+/**
+ * Strips paid features from an existing or new `PolicyConfig` for gold and below license
+ */
+export const policyFactoryWithoutPaidFeatures = (
+ policy: PolicyConfig = policyFactory()
+): PolicyConfig => {
+ return {
+ ...policy,
+ windows: {
+ ...policy.windows,
+ ransomware: {
+ mode: ProtectionModes.off,
+ },
+ popup: {
+ ...policy.windows.popup,
+ malware: {
+ message: '',
+ enabled: true,
+ },
+ ransomware: {
+ message: '',
+ enabled: false,
+ },
+ },
+ },
+ mac: {
+ ...policy.mac,
+ ransomware: {
+ mode: ProtectionModes.off,
+ },
+ popup: {
+ ...policy.mac.popup,
+ malware: {
+ message: '',
+ enabled: true,
+ },
+ ransomware: {
+ message: '',
+ enabled: false,
+ },
+ },
+ },
+ };
+};
+
/**
* Reflects what string the Endpoint will use when message field is default/empty
*/
diff --git a/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.test.ts b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11_0.test.ts
similarity index 98%
rename from x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.test.ts
rename to x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11_0.test.ts
index 1b70a13935b7d..932afc6af3f16 100644
--- a/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.test.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11_0.test.ts
@@ -6,7 +6,7 @@
import { SavedObjectMigrationContext, SavedObjectUnsanitizedDoc } from 'kibana/server';
import { PackagePolicy } from '../../../../../fleet/common';
-import { migratePackagePolicyToV7110 } from './to_v7_11.0';
+import { migratePackagePolicyToV7110 } from './to_v7_11_0';
describe('7.11.0 Endpoint Package Policy migration', () => {
const migration = migratePackagePolicyToV7110;
diff --git a/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.ts b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11_0.ts
similarity index 100%
rename from x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11.0.ts
rename to x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_11_0.ts
diff --git a/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_12_0.test.ts b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_12_0.test.ts
new file mode 100644
index 0000000000000..2666b477921fc
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_12_0.test.ts
@@ -0,0 +1,199 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SavedObjectMigrationContext, SavedObjectUnsanitizedDoc } from 'kibana/server';
+import { PackagePolicy } from '../../../../../fleet/common';
+import { PolicyData, ProtectionModes } from '../../types';
+import { migratePackagePolicyToV7120 } from './to_v7_12_0';
+
+describe('7.12.0 Endpoint Package Policy migration', () => {
+ const migration = migratePackagePolicyToV7120;
+ it('adds ransomware option and notification customization', () => {
+ const doc: SavedObjectUnsanitizedDoc = {
+ id: 'mock-saved-object-id',
+ attributes: {
+ name: 'Some Policy Name',
+ package: {
+ name: 'endpoint',
+ title: '',
+ version: '',
+ },
+ id: 'endpoint',
+ policy_id: '',
+ enabled: true,
+ namespace: '',
+ output_id: '',
+ revision: 0,
+ updated_at: '',
+ updated_by: '',
+ created_at: '',
+ created_by: '',
+ inputs: [
+ {
+ type: 'endpoint',
+ enabled: true,
+ streams: [],
+ config: {
+ policy: {
+ value: {
+ windows: {
+ // @ts-expect-error
+ popup: {
+ malware: {
+ message: '',
+ enabled: false,
+ },
+ },
+ },
+ mac: {
+ // @ts-expect-error
+ popup: {
+ malware: {
+ message: '',
+ enabled: false,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ ],
+ },
+ type: ' nested',
+ };
+
+ expect(
+ migration(doc, {} as SavedObjectMigrationContext) as SavedObjectUnsanitizedDoc
+ ).toEqual({
+ attributes: {
+ name: 'Some Policy Name',
+ package: {
+ name: 'endpoint',
+ title: '',
+ version: '',
+ },
+ id: 'endpoint',
+ policy_id: '',
+ enabled: true,
+ namespace: '',
+ output_id: '',
+ revision: 0,
+ updated_at: '',
+ updated_by: '',
+ created_at: '',
+ created_by: '',
+ inputs: [
+ {
+ type: 'endpoint',
+ enabled: true,
+ streams: [],
+ config: {
+ policy: {
+ value: {
+ windows: {
+ ransomware: ProtectionModes.off,
+ popup: {
+ malware: {
+ message: '',
+ enabled: false,
+ },
+ ransomware: {
+ message: '',
+ enabled: false,
+ },
+ },
+ },
+ mac: {
+ ransomware: ProtectionModes.off,
+ popup: {
+ malware: {
+ message: '',
+ enabled: false,
+ },
+ ransomware: {
+ message: '',
+ enabled: false,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ ],
+ },
+ type: ' nested',
+ id: 'mock-saved-object-id',
+ });
+ });
+
+ it('does not modify non-endpoint package policies', () => {
+ const doc: SavedObjectUnsanitizedDoc = {
+ id: 'mock-saved-object-id',
+ attributes: {
+ name: 'Some Policy Name',
+ package: {
+ name: 'notEndpoint',
+ title: '',
+ version: '',
+ },
+ id: 'notEndpoint',
+ policy_id: '',
+ enabled: true,
+ namespace: '',
+ output_id: '',
+ revision: 0,
+ updated_at: '',
+ updated_by: '',
+ created_at: '',
+ created_by: '',
+ inputs: [
+ {
+ type: 'notEndpoint',
+ enabled: true,
+ streams: [],
+ config: {},
+ },
+ ],
+ },
+ type: ' nested',
+ };
+
+ expect(
+ migration(doc, {} as SavedObjectMigrationContext) as SavedObjectUnsanitizedDoc
+ ).toEqual({
+ attributes: {
+ name: 'Some Policy Name',
+ package: {
+ name: 'notEndpoint',
+ title: '',
+ version: '',
+ },
+ id: 'notEndpoint',
+ policy_id: '',
+ enabled: true,
+ namespace: '',
+ output_id: '',
+ revision: 0,
+ updated_at: '',
+ updated_by: '',
+ created_at: '',
+ created_by: '',
+ inputs: [
+ {
+ type: 'notEndpoint',
+ enabled: true,
+ streams: [],
+ config: {},
+ },
+ ],
+ },
+ type: ' nested',
+ id: 'mock-saved-object-id',
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_12_0.ts b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_12_0.ts
new file mode 100644
index 0000000000000..6004ef533d5ad
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/endpoint/policy/migrations/to_v7_12_0.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SavedObjectMigrationFn, SavedObjectUnsanitizedDoc } from 'kibana/server';
+import { cloneDeep } from 'lodash';
+import { PackagePolicy } from '../../../../../fleet/common';
+import { ProtectionModes } from '../../types';
+
+export const migratePackagePolicyToV7120: SavedObjectMigrationFn = (
+ packagePolicyDoc
+) => {
+ const updatedPackagePolicyDoc: SavedObjectUnsanitizedDoc = cloneDeep(
+ packagePolicyDoc
+ );
+ if (packagePolicyDoc.attributes.package?.name === 'endpoint') {
+ const input = updatedPackagePolicyDoc.attributes.inputs[0];
+ const ransomware = {
+ message: '',
+ enabled: false,
+ };
+ if (input && input.config) {
+ const policy = input.config.policy.value;
+
+ policy.windows.ransomware = ProtectionModes.off;
+ policy.mac.ransomware = ProtectionModes.off;
+ policy.windows.popup.ransomware = ransomware;
+ policy.mac.popup.ransomware = ransomware;
+ }
+ }
+
+ return updatedPackagePolicyDoc;
+};
diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts
index fab5bd9daae00..f72373a6544a0 100644
--- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts
@@ -816,7 +816,8 @@ export interface PolicyConfig {
registry: boolean;
security: boolean;
};
- malware: MalwareFields;
+ malware: ProtectionFields;
+ ransomware: ProtectionFields;
logging: {
file: string;
};
@@ -825,6 +826,10 @@ export interface PolicyConfig {
message: string;
enabled: boolean;
};
+ ransomware: {
+ message: string;
+ enabled: boolean;
+ };
};
antivirus_registration: {
enabled: boolean;
@@ -837,12 +842,17 @@ export interface PolicyConfig {
process: boolean;
network: boolean;
};
- malware: MalwareFields;
+ malware: ProtectionFields;
+ ransomware: ProtectionFields;
popup: {
malware: {
message: string;
enabled: boolean;
};
+ ransomware: {
+ message: string;
+ enabled: boolean;
+ };
};
logging: {
file: string;
@@ -870,20 +880,20 @@ export interface UIPolicyConfig {
*/
windows: Pick<
PolicyConfig['windows'],
- 'events' | 'malware' | 'popup' | 'antivirus_registration' | 'advanced'
+ 'events' | 'malware' | 'ransomware' | 'popup' | 'antivirus_registration' | 'advanced'
>;
/**
* Mac-specific policy configuration that is supported via the UI
*/
- mac: Pick;
+ mac: Pick;
/**
* Linux-specific policy configuration that is supported via the UI
*/
linux: Pick;
}
-/** Policy: Malware protection fields */
-export interface MalwareFields {
+/** Policy: Protection fields */
+export interface ProtectionFields {
mode: ProtectionModes;
}
diff --git a/x-pack/plugins/security_solution/common/license/policy_config.test.ts b/x-pack/plugins/security_solution/common/license/policy_config.test.ts
index 6923bf00055f6..ef10bba7428ee 100644
--- a/x-pack/plugins/security_solution/common/license/policy_config.test.ts
+++ b/x-pack/plugins/security_solution/common/license/policy_config.test.ts
@@ -8,8 +8,13 @@ import {
isEndpointPolicyValidForLicense,
unsetPolicyFeaturesAboveLicenseLevel,
} from './policy_config';
-import { DefaultMalwareMessage, factory } from '../endpoint/models/policy_config';
+import {
+ DefaultMalwareMessage,
+ policyFactory,
+ policyFactoryWithoutPaidFeatures,
+} from '../endpoint/models/policy_config';
import { licenseMock } from '../../../licensing/common/licensing.mock';
+import { ProtectionModes } from '../endpoint/types';
describe('policy_config and licenses', () => {
const Platinum = licenseMock.createLicense({ license: { type: 'platinum', mode: 'platinum' } });
@@ -18,13 +23,13 @@ describe('policy_config and licenses', () => {
describe('isEndpointPolicyValidForLicense', () => {
it('allows malware notification to be disabled with a Platinum license', () => {
- const policy = factory();
+ const policy = policyFactory();
policy.windows.popup.malware.enabled = false; // make policy change
const valid = isEndpointPolicyValidForLicense(policy, Platinum);
expect(valid).toBeTruthy();
});
it('blocks windows malware notification changes below Platinum licenses', () => {
- const policy = factory();
+ const policy = policyFactory();
policy.windows.popup.malware.enabled = false; // make policy change
let valid = isEndpointPolicyValidForLicense(policy, Gold);
expect(valid).toBeFalsy();
@@ -34,7 +39,7 @@ describe('policy_config and licenses', () => {
});
it('blocks mac malware notification changes below Platinum licenses', () => {
- const policy = factory();
+ const policy = policyFactory();
policy.mac.popup.malware.enabled = false; // make policy change
let valid = isEndpointPolicyValidForLicense(policy, Gold);
expect(valid).toBeFalsy();
@@ -44,13 +49,13 @@ describe('policy_config and licenses', () => {
});
it('allows malware notification message changes with a Platinum license', () => {
- const policy = factory();
+ const policy = policyFactory();
policy.windows.popup.malware.message = 'BOOM'; // make policy change
const valid = isEndpointPolicyValidForLicense(policy, Platinum);
expect(valid).toBeTruthy();
});
it('blocks windows malware notification message changes below Platinum licenses', () => {
- const policy = factory();
+ const policy = policyFactory();
policy.windows.popup.malware.message = 'BOOM'; // make policy change
let valid = isEndpointPolicyValidForLicense(policy, Gold);
expect(valid).toBeFalsy();
@@ -59,7 +64,7 @@ describe('policy_config and licenses', () => {
expect(valid).toBeFalsy();
});
it('blocks mac malware notification message changes below Platinum licenses', () => {
- const policy = factory();
+ const policy = policyFactory();
policy.mac.popup.malware.message = 'BOOM'; // make policy change
let valid = isEndpointPolicyValidForLicense(policy, Gold);
expect(valid).toBeFalsy();
@@ -68,16 +73,71 @@ describe('policy_config and licenses', () => {
expect(valid).toBeFalsy();
});
+ it('allows ransomware to be turned on for Platinum licenses', () => {
+ const policy = policyFactoryWithoutPaidFeatures();
+ policy.windows.ransomware.mode = ProtectionModes.prevent;
+ policy.mac.ransomware.mode = ProtectionModes.prevent;
+
+ const valid = isEndpointPolicyValidForLicense(policy, Platinum);
+ expect(valid).toBeTruthy();
+ });
+ it('blocks ransomware to be turned on for Gold and below licenses', () => {
+ const policy = policyFactoryWithoutPaidFeatures();
+ policy.windows.ransomware.mode = ProtectionModes.prevent;
+ policy.mac.ransomware.mode = ProtectionModes.prevent;
+
+ let valid = isEndpointPolicyValidForLicense(policy, Gold);
+ expect(valid).toBeFalsy();
+ valid = isEndpointPolicyValidForLicense(policy, Basic);
+ expect(valid).toBeFalsy();
+ });
+
+ it('allows ransomware notification to be turned on with a Platinum license', () => {
+ const policy = policyFactoryWithoutPaidFeatures();
+ policy.windows.popup.ransomware.enabled = true;
+ policy.mac.popup.ransomware.enabled = true;
+ const valid = isEndpointPolicyValidForLicense(policy, Platinum);
+ expect(valid).toBeTruthy();
+ });
+ it('blocks ransomware notification to be turned on for Gold and below licenses', () => {
+ const policy = policyFactoryWithoutPaidFeatures();
+ policy.windows.popup.ransomware.enabled = true;
+ policy.mac.popup.ransomware.enabled = true;
+ let valid = isEndpointPolicyValidForLicense(policy, Gold);
+ expect(valid).toBeFalsy();
+
+ valid = isEndpointPolicyValidForLicense(policy, Basic);
+ expect(valid).toBeFalsy();
+ });
+
+ it('allows ransomware notification message changes with a Platinum license', () => {
+ const policy = policyFactory();
+ policy.windows.popup.ransomware.message = 'BOOM';
+ policy.mac.popup.ransomware.message = 'BOOM';
+ const valid = isEndpointPolicyValidForLicense(policy, Platinum);
+ expect(valid).toBeTruthy();
+ });
+ it('blocks ransomware notification message changes for Gold and below licenses', () => {
+ const policy = policyFactory();
+ policy.windows.popup.ransomware.message = 'BOOM';
+ policy.mac.popup.ransomware.message = 'BOOM';
+ let valid = isEndpointPolicyValidForLicense(policy, Gold);
+ expect(valid).toBeFalsy();
+
+ valid = isEndpointPolicyValidForLicense(policy, Basic);
+ expect(valid).toBeFalsy();
+ });
+
it('allows default policyConfig with Basic', () => {
- const policy = factory();
+ const policy = policyFactoryWithoutPaidFeatures();
const valid = isEndpointPolicyValidForLicense(policy, Basic);
expect(valid).toBeTruthy();
});
});
describe('unsetPolicyFeaturesAboveLicenseLevel', () => {
- it('does not change any fields with a Platinum license', () => {
- const policy = factory();
+ it('does not change any malware fields with a Platinum license', () => {
+ const policy = policyFactory();
const popupMessage = 'WOOP WOOP';
policy.windows.popup.malware.message = popupMessage;
policy.mac.popup.malware.message = popupMessage;
@@ -88,14 +148,37 @@ describe('policy_config and licenses', () => {
expect(retPolicy.windows.popup.malware.message).toEqual(popupMessage);
expect(retPolicy.mac.popup.malware.message).toEqual(popupMessage);
});
- it('resets Platinum-paid fields for lower license tiers', () => {
- const defaults = factory(); // reference
- const policy = factory(); // what we will modify, and should be reset
+
+ it('does not change any ransomware fields with a Platinum license', () => {
+ const policy = policyFactory();
+ const popupMessage = 'WOOP WOOP';
+ policy.windows.ransomware.mode = ProtectionModes.detect;
+ policy.mac.ransomware.mode = ProtectionModes.detect;
+ policy.windows.popup.ransomware.enabled = false;
+ policy.mac.popup.ransomware.enabled = false;
+ policy.windows.popup.ransomware.message = popupMessage;
+ policy.mac.popup.ransomware.message = popupMessage;
+
+ const retPolicy = unsetPolicyFeaturesAboveLicenseLevel(policy, Platinum);
+ expect(retPolicy.windows.ransomware.mode).toEqual(ProtectionModes.detect);
+ expect(retPolicy.mac.ransomware.mode).toEqual(ProtectionModes.detect);
+ expect(retPolicy.windows.popup.ransomware.enabled).toBeFalsy();
+ expect(retPolicy.mac.popup.ransomware.enabled).toBeFalsy();
+ expect(retPolicy.windows.popup.ransomware.message).toEqual(popupMessage);
+ expect(retPolicy.mac.popup.ransomware.message).toEqual(popupMessage);
+ });
+
+ it('resets Platinum-paid malware fields for lower license tiers', () => {
+ const defaults = policyFactory(); // reference
+ const policy = policyFactory(); // what we will modify, and should be reset
const popupMessage = 'WOOP WOOP';
policy.windows.popup.malware.message = popupMessage;
policy.mac.popup.malware.message = popupMessage;
policy.windows.popup.malware.enabled = false;
+ policy.windows.popup.ransomware.message = popupMessage;
+ policy.mac.popup.ransomware.message = popupMessage;
+ policy.windows.popup.ransomware.enabled = false;
const retPolicy = unsetPolicyFeaturesAboveLicenseLevel(policy, Gold);
expect(retPolicy.windows.popup.malware.enabled).toEqual(
defaults.windows.popup.malware.enabled
@@ -106,5 +189,37 @@ describe('policy_config and licenses', () => {
// need to invert the test, since it could be either value
expect(['', DefaultMalwareMessage]).toContain(retPolicy.windows.popup.malware.message);
});
+
+ it('resets Platinum-paid ransomware fields for lower license tiers', () => {
+ const defaults = policyFactoryWithoutPaidFeatures(); // reference
+ const policy = policyFactory(); // what we will modify, and should be reset
+ const popupMessage = 'WOOP WOOP';
+ policy.windows.popup.ransomware.message = popupMessage;
+ policy.mac.popup.ransomware.message = popupMessage;
+
+ const retPolicy = unsetPolicyFeaturesAboveLicenseLevel(policy, Gold);
+
+ expect(retPolicy.windows.ransomware.mode).toEqual(defaults.windows.ransomware.mode);
+ expect(retPolicy.mac.ransomware.mode).toEqual(defaults.mac.ransomware.mode);
+ expect(retPolicy.windows.popup.ransomware.enabled).toEqual(
+ defaults.windows.popup.ransomware.enabled
+ );
+ expect(retPolicy.mac.popup.ransomware.enabled).toEqual(defaults.mac.popup.ransomware.enabled);
+ expect(retPolicy.windows.popup.ransomware.message).not.toEqual(popupMessage);
+ expect(retPolicy.mac.popup.ransomware.message).not.toEqual(popupMessage);
+
+ // need to invert the test, since it could be either value
+ expect(['', DefaultMalwareMessage]).toContain(retPolicy.windows.popup.ransomware.message);
+ expect(['', DefaultMalwareMessage]).toContain(retPolicy.mac.popup.ransomware.message);
+ });
+ });
+
+ describe('policyFactoryWithoutPaidFeatures for gold and below license', () => {
+ it('preserves non license-gated features', () => {
+ const policy = policyFactory(); // what we will modify, and should be reset
+ policy.windows.events.file = false;
+ const retPolicy = policyFactoryWithoutPaidFeatures(policy);
+ expect(retPolicy.windows.events.file).toBeFalsy();
+ });
});
});
diff --git a/x-pack/plugins/security_solution/common/license/policy_config.ts b/x-pack/plugins/security_solution/common/license/policy_config.ts
index da2260ad55e8b..e791b68f12f40 100644
--- a/x-pack/plugins/security_solution/common/license/policy_config.ts
+++ b/x-pack/plugins/security_solution/common/license/policy_config.ts
@@ -7,7 +7,10 @@
import { ILicense } from '../../../licensing/common/types';
import { isAtLeast } from './license';
import { PolicyConfig } from '../endpoint/types';
-import { DefaultMalwareMessage, factory } from '../endpoint/models/policy_config';
+import {
+ DefaultMalwareMessage,
+ policyFactoryWithoutPaidFeatures,
+} from '../endpoint/models/policy_config';
/**
* Given an endpoint package policy, verifies that all enabled features that
@@ -21,7 +24,7 @@ export const isEndpointPolicyValidForLicense = (
return true; // currently, platinum allows all features
}
- const defaults = factory();
+ const defaults = policyFactoryWithoutPaidFeatures();
// only platinum or higher may disable malware notification
if (
@@ -40,6 +43,32 @@ export const isEndpointPolicyValidForLicense = (
return false;
}
+ // only platinum or higher may enable ransomware
+ if (
+ policy.windows.ransomware.mode !== defaults.windows.ransomware.mode ||
+ policy.mac.ransomware.mode !== defaults.mac.ransomware.mode
+ ) {
+ return false;
+ }
+
+ // only platinum or higher may enable ransomware notification
+ if (
+ policy.windows.popup.ransomware.enabled !== defaults.windows.popup.ransomware.enabled ||
+ policy.mac.popup.ransomware.enabled !== defaults.mac.popup.ransomware.enabled
+ ) {
+ return false;
+ }
+
+ // Only Platinum or higher may change the ransomware message (which can be blank or what Endpoint defaults)
+ if (
+ [policy.windows, policy.mac].some(
+ (p) =>
+ p.popup.ransomware.message !== '' && p.popup.ransomware.message !== DefaultMalwareMessage
+ )
+ ) {
+ return false;
+ }
+
return true;
};
@@ -55,12 +84,6 @@ export const unsetPolicyFeaturesAboveLicenseLevel = (
return policy;
}
- const defaults = factory();
// set any license-gated features back to the defaults
- policy.windows.popup.malware.enabled = defaults.windows.popup.malware.enabled;
- policy.mac.popup.malware.enabled = defaults.mac.popup.malware.enabled;
- policy.windows.popup.malware.message = defaults.windows.popup.malware.message;
- policy.mac.popup.malware.message = defaults.mac.popup.malware.message;
-
- return policy;
+ return policyFactoryWithoutPaidFeatures(policy);
};
diff --git a/x-pack/plugins/security_solution/common/shared_exports.ts b/x-pack/plugins/security_solution/common/shared_exports.ts
index fb457933f4b54..92a736ae601df 100644
--- a/x-pack/plugins/security_solution/common/shared_exports.ts
+++ b/x-pack/plugins/security_solution/common/shared_exports.ts
@@ -16,4 +16,5 @@ export { exactCheck } from './exact_check';
export { getPaths, foldLeftRight, removeExternalLinkText } from './test_utils';
export { validate, validateEither } from './validate';
export { formatErrors } from './format_errors';
-export { migratePackagePolicyToV7110 } from './endpoint/policy/migrations/to_v7_11.0';
+export { migratePackagePolicyToV7110 } from './endpoint/policy/migrations/to_v7_11_0';
+export { migratePackagePolicyToV7120 } from './endpoint/policy/migrations/to_v7_12_0';
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts
index 70ffc1f8a9fc4..bda268c1fad00 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts
@@ -8,7 +8,7 @@ import { PolicyDetailsState } from '../../types';
import { applyMiddleware, createStore, Dispatch, Store } from 'redux';
import { policyDetailsReducer, PolicyDetailsAction, policyDetailsMiddlewareFactory } from './index';
import { policyConfig } from './selectors';
-import { factory as policyConfigFactory } from '../../../../../../common/endpoint/models/policy_config';
+import { policyFactory } from '../../../../../../common/endpoint/models/policy_config';
import { PolicyData } from '../../../../../../common/endpoint/types';
import {
createSpyMiddleware,
@@ -54,7 +54,7 @@ describe('policy details: ', () => {
},
},
policy: {
- value: policyConfigFactory(),
+ value: policyFactory(),
},
},
},
@@ -254,6 +254,7 @@ describe('policy details: ', () => {
http.put.mock.calls.length - 1
] as unknown) as [string, HttpFetchOptions])[1];
+ // license is below platinum in this test, paid features are off
expect(JSON.parse(lastPutCallPayload.body as string)).toEqual({
name: '',
description: '',
@@ -282,11 +283,16 @@ describe('policy details: ', () => {
security: true,
},
malware: { mode: 'prevent' },
+ ransomware: { mode: 'off' },
popup: {
malware: {
enabled: true,
message: '',
},
+ ransomware: {
+ enabled: false,
+ message: '',
+ },
},
logging: { file: 'info' },
antivirus_registration: {
@@ -296,11 +302,16 @@ describe('policy details: ', () => {
mac: {
events: { process: true, file: true, network: true },
malware: { mode: 'prevent' },
+ ransomware: { mode: 'off' },
popup: {
malware: {
enabled: true,
message: '',
},
+ ransomware: {
+ enabled: false,
+ message: '',
+ },
},
logging: { file: 'info' },
},
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts
index cc286b4c478d3..4d54acb5eae13 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts
@@ -41,6 +41,10 @@ export const policyDetailsMiddlewareFactory: ImmutableMiddlewareFactory {
if (policyData) {
- unsetPolicyFeaturesAboveLicenseLevel(
- policyData?.inputs[0]?.config.policy.value,
+ const policyValue = unsetPolicyFeaturesAboveLicenseLevel(
+ policyData.inputs[0].config.policy.value,
license as ILicense
);
+ const newPolicyData: Immutable = {
+ ...policyData,
+ inputs: [
+ {
+ ...policyData.inputs[0],
+ config: {
+ ...policyData.inputs[0].config,
+ policy: {
+ ...policyData.inputs[0].config.policy,
+ value: policyValue,
+ },
+ },
+ },
+ ],
+ };
+ return newPolicyData;
}
return policyData;
}
@@ -167,6 +183,7 @@ export const policyConfig: (s: PolicyDetailsState) => UIPolicyConfig = createSel
advanced: windows.advanced,
events: windows.events,
malware: windows.malware,
+ ransomware: windows.ransomware,
popup: windows.popup,
antivirus_registration: windows.antivirus_registration,
},
@@ -174,6 +191,7 @@ export const policyConfig: (s: PolicyDetailsState) => UIPolicyConfig = createSel
advanced: mac.advanced,
events: mac.events,
malware: mac.malware,
+ ransomware: mac.ransomware,
popup: mac.popup,
},
linux: {
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts
index 228e8cc1c4385..a37e404cdc522 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts
@@ -8,7 +8,7 @@ import { ILicense } from '../../../../../licensing/common/types';
import {
AppLocation,
Immutable,
- MalwareFields,
+ ProtectionFields,
PolicyData,
UIPolicyConfig,
} from '../../../../common/endpoint/types';
@@ -108,7 +108,16 @@ export type KeysByValueCriteria = {
}[keyof O];
/** Returns an array of the policy OSes that have a malware protection field */
-export type MalwareProtectionOSes = KeysByValueCriteria;
+export type MalwareProtectionOSes = KeysByValueCriteria<
+ UIPolicyConfig,
+ { malware: ProtectionFields }
+>;
+
+/** Returns an array of the policy OSes that have a ransomware protection field */
+export type RansomwareProtectionOSes = KeysByValueCriteria<
+ UIPolicyConfig,
+ { ransomware: ProtectionFields }
+>;
export interface GetPolicyListResponse extends GetPackagePoliciesResponse {
items: PolicyData[];
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx
index ce5eb03d60cd0..aea4df5b2a6fd 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx
@@ -58,7 +58,7 @@ export const ConfigForm: FC = memo(
{TITLES.type}
{type}
-
+
{TITLES.os}
{supportedOss.map((os) => OS_TITLES[os]).join(', ')}
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx
index 1280f1c351c2b..55fc7703de44b 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx
@@ -301,11 +301,16 @@ describe('Policy Details', () => {
const userNotificationCustomMessageTextArea = policyView.find(
'EuiTextArea[data-test-subj="malwareUserNotificationCustomMessage"]'
);
- const tooltip = policyView.find('EuiIconTip');
+ const tooltip = policyView.find('EuiIconTip[data-test-subj="malwareTooltip"]');
expect(userNotificationCheckbox).toHaveLength(1);
expect(userNotificationCustomMessageTextArea).toHaveLength(1);
expect(tooltip).toHaveLength(1);
});
+
+ it('ransomware card is shown', () => {
+ const ransomware = policyView.find('EuiPanel[data-test-subj="ransomwareProtectionsForm"]');
+ expect(ransomware).toHaveLength(1);
+ });
});
describe('when the subscription tier is gold or lower', () => {
beforeEach(() => {
@@ -325,6 +330,11 @@ describe('Policy Details', () => {
expect(userNotificationCustomMessageTextArea).toHaveLength(0);
expect(tooltip).toHaveLength(0);
});
+
+ it('ransomware card is hidden', () => {
+ const ransomware = policyView.find('EuiPanel[data-test-subj="ransomwareProtectionsForm"]');
+ expect(ransomware).toHaveLength(0);
+ });
});
});
});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx
index a0bf2b37e8a12..8710f696fad41 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx
@@ -11,12 +11,15 @@ import { MalwareProtections } from './policy_forms/protections/malware';
import { LinuxEvents, MacEvents, WindowsEvents } from './policy_forms/events';
import { AdvancedPolicyForms } from './policy_advanced';
import { AntivirusRegistrationForm } from './components/antivirus_registration_form';
+import { Ransomware } from './policy_forms/protections/ransomware';
+import { useLicense } from '../../../../common/hooks/use_license';
export const PolicyDetailsForm = memo(() => {
const [showAdvancedPolicy, setShowAdvancedPolicy] = useState(false);
const handleAdvancedPolicyClick = useCallback(() => {
setShowAdvancedPolicy(!showAdvancedPolicy);
}, [showAdvancedPolicy]);
+ const isPlatinumPlus = useLicense().isPlatinumPlus();
return (
<>
@@ -31,6 +34,8 @@ export const PolicyDetailsForm = memo(() => {
+
+ {isPlatinumPlus && }
@@ -44,14 +49,14 @@ export const PolicyDetailsForm = memo(() => {
-
+
-
+
-
+
-
+
props.theme.eui.euiSizeXXL};
+export const RadioFlexGroup = styled(EuiFlexGroup)`
+ .no-right-margin-radio {
+ margin-right: 0;
+ }
+ .no-horizontal-margin-radio {
+ margin: ${(props) => props.theme.eui.ruleMargins.marginSmall} 0;
}
`;
const OSes: Immutable = [OS.windows, OS.mac];
const protection = 'malware';
-const ProtectionRadio = React.memo(({ id, label }: { id: ProtectionModes; label: string }) => {
- const policyDetailsConfig = usePolicyDetailsSelector(policyConfig);
- const dispatch = useDispatch();
- const radioButtonId = useMemo(() => htmlIdGenerator()(), []);
- // currently just taking windows.malware, but both windows.malware and mac.malware should be the same value
- const selected = policyDetailsConfig && policyDetailsConfig.windows.malware.mode;
- const isPlatinumPlus = useLicense().isPlatinumPlus();
+const ProtectionRadio = React.memo(
+ ({ protectionMode, label }: { protectionMode: ProtectionModes; label: string }) => {
+ const policyDetailsConfig = usePolicyDetailsSelector(policyConfig);
+ const dispatch = useDispatch<(action: AppAction) => void>();
+ const radioButtonId = useMemo(() => htmlIdGenerator()(), []);
+ // currently just taking windows.malware, but both windows.malware and mac.malware should be the same value
+ const selected = policyDetailsConfig && policyDetailsConfig.windows.malware.mode;
+ const isPlatinumPlus = useLicense().isPlatinumPlus();
- const handleRadioChange = useCallback(() => {
- if (policyDetailsConfig) {
- const newPayload = cloneDeep(policyDetailsConfig);
- for (const os of OSes) {
- newPayload[os][protection].mode = id;
- if (isPlatinumPlus) {
- if (id === ProtectionModes.prevent) {
- newPayload[os].popup[protection].enabled = true;
- } else {
- newPayload[os].popup[protection].enabled = false;
+ const handleRadioChange = useCallback(() => {
+ if (policyDetailsConfig) {
+ const newPayload = cloneDeep(policyDetailsConfig);
+ for (const os of OSes) {
+ newPayload[os][protection].mode = protectionMode;
+ if (isPlatinumPlus) {
+ if (protectionMode === ProtectionModes.prevent) {
+ newPayload[os].popup[protection].enabled = true;
+ } else {
+ newPayload[os].popup[protection].enabled = false;
+ }
}
}
+ dispatch({
+ type: 'userChangedPolicyConfig',
+ payload: { policyConfig: newPayload },
+ });
}
- dispatch({
- type: 'userChangedPolicyConfig',
- payload: { policyConfig: newPayload },
- });
- }
- }, [dispatch, id, policyDetailsConfig, isPlatinumPlus]);
+ }, [dispatch, protectionMode, policyDetailsConfig, isPlatinumPlus]);
- /**
- * Passing an arbitrary id because EuiRadio
- * requires an id if label is passed
- */
+ /**
+ * Passing an arbitrary id because EuiRadio
+ * requires an id if label is passed
+ */
- return (
-
- );
-});
-
-ProtectionRadio.displayName = 'ProtectionRadio';
-
-const SupportedVersionNotice = ({ optionName }: { optionName: string }) => {
- const version = popupVersionsMap.get(optionName);
- if (!version) {
- return null;
+ return (
+
+ );
}
+);
- return (
-
-
-
-
-
- );
-};
+ProtectionRadio.displayName = 'ProtectionRadio';
/** The Malware Protections form for policy details
* which will configure for all relevant OSes.
*/
export const MalwareProtections = React.memo(() => {
const policyDetailsConfig = usePolicyDetailsSelector(policyConfig);
- const dispatch = useDispatch();
+ const dispatch = useDispatch<(action: AppAction) => void>();
// currently just taking windows.malware, but both windows.malware and mac.malware should be the same value
const selected = policyDetailsConfig && policyDetailsConfig.windows.malware.mode;
const userNotificationSelected =
@@ -224,19 +209,25 @@ export const MalwareProtections = React.memo(() => {
/>
-
- {radios.map((radio) => {
- return (
-
- );
- })}
-
+
+
+
+
+
+
+
+
{isPlatinumPlus && (
<>
+
{
@@ -327,7 +319,7 @@ export const MalwareProtections = React.memo(() => {
label={i18n.translate(
'xpack.securitySolution.endpoint.policy.details.malwareProtectionsEnabled',
{
- defaultMessage: 'Malware Protections {mode, select, true {Enabled} false {Disabled}}',
+ defaultMessage: 'Malware protections {mode, select, true {enabled} false {disabled}}',
values: {
mode: selected !== ProtectionModes.off,
},
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/popup_options_to_versions.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/popup_options_to_versions.ts
index d4c7d0102ebd4..795f7dda52499 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/popup_options_to_versions.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/popup_options_to_versions.ts
@@ -4,6 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-const popupVersions: Array<[string, string]> = [['malware', '7.11+']];
+const popupVersions: Array<[string, string]> = [
+ ['malware', '7.11+'],
+ ['ransomware', '7.12+'],
+];
export const popupVersionsMap: ReadonlyMap = new Map(popupVersions);
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/ransomware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/ransomware.tsx
new file mode 100644
index 0000000000000..eb2dd4b2fe8d8
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/ransomware.tsx
@@ -0,0 +1,346 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useCallback, useMemo } from 'react';
+import { useDispatch } from 'react-redux';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiCallOut,
+ EuiCheckbox,
+ EuiRadio,
+ EuiSpacer,
+ EuiSwitch,
+ EuiText,
+ EuiTextArea,
+ htmlIdGenerator,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIconTip,
+ EuiCheckboxProps,
+ EuiRadioProps,
+ EuiSwitchProps,
+} from '@elastic/eui';
+import { cloneDeep } from 'lodash';
+import { APP_ID } from '../../../../../../../common/constants';
+import { SecurityPageName } from '../../../../../../app/types';
+import {
+ Immutable,
+ OperatingSystem,
+ ProtectionModes,
+} from '../../../../../../../common/endpoint/types';
+import { RansomwareProtectionOSes, OS } from '../../../types';
+import { ConfigForm, ConfigFormHeading } from '../../components/config_form';
+import { policyConfig } from '../../../store/policy_details/selectors';
+import { usePolicyDetailsSelector } from '../../policy_hooks';
+import { LinkToApp } from '../../../../../../common/components/endpoint/link_to_app';
+import { AppAction } from '../../../../../../common/store/actions';
+import { SupportedVersionNotice } from './supported_version';
+import { RadioFlexGroup } from './malware';
+
+const OSes: Immutable = [OS.windows, OS.mac];
+const protection = 'ransomware';
+
+const ProtectionRadio = React.memo(
+ ({ protectionMode, label }: { protectionMode: ProtectionModes; label: string }) => {
+ const policyDetailsConfig = usePolicyDetailsSelector(policyConfig);
+ const dispatch = useDispatch<(action: AppAction) => void>();
+ const radioButtonId = useMemo(() => htmlIdGenerator()(), []);
+ // currently just taking windows.ransomware, but both windows.ransomware and mac.ransomware should be the same value
+ const selected = policyDetailsConfig && policyDetailsConfig.windows.ransomware.mode;
+
+ const handleRadioChange: EuiRadioProps['onChange'] = useCallback(() => {
+ if (policyDetailsConfig) {
+ const newPayload = cloneDeep(policyDetailsConfig);
+ for (const os of OSes) {
+ newPayload[os][protection].mode = protectionMode;
+ if (protectionMode === ProtectionModes.prevent) {
+ newPayload[os].popup[protection].enabled = true;
+ } else {
+ newPayload[os].popup[protection].enabled = false;
+ }
+ }
+ dispatch({
+ type: 'userChangedPolicyConfig',
+ payload: { policyConfig: newPayload },
+ });
+ }
+ }, [dispatch, protectionMode, policyDetailsConfig]);
+
+ /**
+ * Passing an arbitrary id because EuiRadio
+ * requires an id if label is passed
+ */
+
+ return (
+
+ );
+ }
+);
+
+ProtectionRadio.displayName = 'ProtectionRadio';
+
+/** The Ransomware Protections form for policy details
+ * which will configure for all relevant OSes.
+ */
+export const Ransomware = React.memo(() => {
+ const policyDetailsConfig = usePolicyDetailsSelector(policyConfig);
+ const dispatch = useDispatch<(action: AppAction) => void>();
+ // currently just taking windows.ransomware, but both windows.ransomware and mac.ransomware should be the same value
+ const selected = policyDetailsConfig && policyDetailsConfig.windows.ransomware.mode;
+ const userNotificationSelected =
+ policyDetailsConfig && policyDetailsConfig.windows.popup.ransomware.enabled;
+ const userNotificationMessage =
+ policyDetailsConfig && policyDetailsConfig.windows.popup.ransomware.message;
+
+ const radios: Immutable<
+ Array<{
+ id: ProtectionModes;
+ label: string;
+ protection: 'ransomware';
+ }>
+ > = useMemo(() => {
+ return [
+ {
+ id: ProtectionModes.detect,
+ label: i18n.translate('xpack.securitySolution.endpoint.policy.details.detect', {
+ defaultMessage: 'Detect',
+ }),
+ protection: 'ransomware',
+ },
+ {
+ id: ProtectionModes.prevent,
+ label: i18n.translate('xpack.securitySolution.endpoint.policy.details.prevent', {
+ defaultMessage: 'Prevent',
+ }),
+ protection: 'ransomware',
+ },
+ ];
+ }, []);
+
+ const handleSwitchChange: EuiSwitchProps['onChange'] = useCallback(
+ (event) => {
+ if (policyDetailsConfig) {
+ const newPayload = cloneDeep(policyDetailsConfig);
+ if (event.target.checked === false) {
+ for (const os of OSes) {
+ newPayload[os][protection].mode = ProtectionModes.off;
+ newPayload[os].popup[protection].enabled = event.target.checked;
+ }
+ } else {
+ for (const os of OSes) {
+ newPayload[os][protection].mode = ProtectionModes.prevent;
+ newPayload[os].popup[protection].enabled = event.target.checked;
+ }
+ }
+ dispatch({
+ type: 'userChangedPolicyConfig',
+ payload: { policyConfig: newPayload },
+ });
+ }
+ },
+ [dispatch, policyDetailsConfig]
+ );
+
+ const handleUserNotificationCheckbox: EuiCheckboxProps['onChange'] = useCallback(
+ (event) => {
+ if (policyDetailsConfig) {
+ const newPayload = cloneDeep(policyDetailsConfig);
+ for (const os of OSes) {
+ newPayload[os].popup[protection].enabled = event.target.checked;
+ }
+ dispatch({
+ type: 'userChangedPolicyConfig',
+ payload: { policyConfig: newPayload },
+ });
+ }
+ },
+ [policyDetailsConfig, dispatch]
+ );
+
+ const handleCustomUserNotification = useCallback(
+ (event) => {
+ if (policyDetailsConfig) {
+ const newPayload = cloneDeep(policyDetailsConfig);
+ for (const os of OSes) {
+ newPayload[os].popup[protection].message = event.target.value;
+ }
+ dispatch({
+ type: 'userChangedPolicyConfig',
+ payload: { policyConfig: newPayload },
+ });
+ }
+ },
+ [policyDetailsConfig, dispatch]
+ );
+
+ const radioButtons = useMemo(() => {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {userNotificationSelected && (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ }
+ />
+
+
+
+
+ >
+ )}
+ >
+ );
+ }, [
+ radios,
+ selected,
+ handleUserNotificationCheckbox,
+ userNotificationSelected,
+ userNotificationMessage,
+ handleCustomUserNotification,
+ ]);
+
+ const protectionSwitch = useMemo(() => {
+ return (
+
+ );
+ }, [handleSwitchChange, selected]);
+
+ return (
+
+ {radioButtons}
+
+
+
+
+
+ ),
+ }}
+ />
+
+
+ );
+});
+
+Ransomware.displayName = 'RansomwareProtections';
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/supported_version.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/supported_version.tsx
new file mode 100644
index 0000000000000..dee6418b4f3ff
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/supported_version.tsx
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiText } from '@elastic/eui';
+import { popupVersionsMap } from './popup_options_to_versions';
+
+export const SupportedVersionNotice = ({ optionName }: { optionName: string }) => {
+ const version = popupVersionsMap.get(optionName);
+ if (!version) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts
index ec3d35cbb6585..7f9e8b42490fa 100644
--- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts
@@ -124,6 +124,7 @@ export class EndpointAppContextService {
dependencies.config.maxTimelineImportExportSize,
dependencies.security,
dependencies.alerts,
+ dependencies.licenseService,
dependencies.exceptionListsClient
)
);
diff --git a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts
index d287ada74eebc..2710e4afb5968 100644
--- a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts
@@ -6,7 +6,10 @@
import { httpServerMock, loggingSystemMock } from 'src/core/server/mocks';
import { createNewPackagePolicyMock } from '../../../fleet/common/mocks';
-import { factory as policyConfigFactory } from '../../common/endpoint/models/policy_config';
+import {
+ policyFactory,
+ policyFactoryWithoutPaidFeatures,
+} from '../../common/endpoint/models/policy_config';
import {
getManifestManagerMock,
ManifestManagerMockType,
@@ -55,6 +58,9 @@ describe('ingest_integration tests ', () => {
});
describe('ingest_integration sanity checks', () => {
+ beforeEach(() => {
+ licenseEmitter.next(Platinum); // set license level to platinum
+ });
test('policy is updated with initial manifest', async () => {
const logger = loggingSystemMock.create().get('ingest_integration.test');
const manifestManager = getManifestManagerMock({
@@ -68,13 +74,14 @@ describe('ingest_integration tests ', () => {
maxTimelineImportExportSize,
endpointAppContextMock.security,
endpointAppContextMock.alerts,
+ licenseService,
exceptionListClient
);
const policyConfig = createNewPackagePolicyMock(); // policy config without manifest
const newPolicyConfig = await callback(policyConfig, ctx, req); // policy config WITH manifest
expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint');
- expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory());
+ expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyFactory());
expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual({
artifacts: {
'endpoint-exceptionlist-macos-v1': {
@@ -146,13 +153,14 @@ describe('ingest_integration tests ', () => {
maxTimelineImportExportSize,
endpointAppContextMock.security,
endpointAppContextMock.alerts,
+ licenseService,
exceptionListClient
);
const policyConfig = createNewPackagePolicyMock();
const newPolicyConfig = await callback(policyConfig, ctx, req);
expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint');
- expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory());
+ expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyFactory());
expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual(
lastComputed!.toEndpointFormat()
);
@@ -174,13 +182,14 @@ describe('ingest_integration tests ', () => {
maxTimelineImportExportSize,
endpointAppContextMock.security,
endpointAppContextMock.alerts,
+ licenseService,
exceptionListClient
);
const policyConfig = createNewPackagePolicyMock();
const newPolicyConfig = await callback(policyConfig, ctx, req);
expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint');
- expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory());
+ expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyFactory());
});
test('subsequent policy creations succeed', async () => {
@@ -196,13 +205,14 @@ describe('ingest_integration tests ', () => {
maxTimelineImportExportSize,
endpointAppContextMock.security,
endpointAppContextMock.alerts,
+ licenseService,
exceptionListClient
);
const policyConfig = createNewPackagePolicyMock();
const newPolicyConfig = await callback(policyConfig, ctx, req);
expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint');
- expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory());
+ expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyFactory());
expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual(
lastComputed!.toEndpointFormat()
);
@@ -221,6 +231,7 @@ describe('ingest_integration tests ', () => {
maxTimelineImportExportSize,
endpointAppContextMock.security,
endpointAppContextMock.alerts,
+ licenseService,
exceptionListClient
);
const policyConfig = createNewPackagePolicyMock();
@@ -228,7 +239,7 @@ describe('ingest_integration tests ', () => {
expect(exceptionListClient.createEndpointList).toHaveBeenCalled();
expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint');
- expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyConfigFactory());
+ expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyFactory());
expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual(
lastComputed!.toEndpointFormat()
);
@@ -239,8 +250,7 @@ describe('ingest_integration tests ', () => {
licenseEmitter.next(Gold); // set license level to gold
});
it('returns an error if paid features are turned on in the policy', async () => {
- const mockPolicy = policyConfigFactory();
- mockPolicy.windows.popup.malware.message = 'paid feature';
+ const mockPolicy = policyFactory(); // defaults with paid features on
const logger = loggingSystemMock.create().get('ingest_integration.test');
const callback = getPackagePolicyUpdateCallback(logger, licenseService);
const policyConfig = generator.generatePolicyPackagePolicy();
@@ -250,7 +260,7 @@ describe('ingest_integration tests ', () => {
);
});
it('updates successfully if no paid features are turned on in the policy', async () => {
- const mockPolicy = policyConfigFactory();
+ const mockPolicy = policyFactoryWithoutPaidFeatures();
mockPolicy.windows.malware.mode = ProtectionModes.detect;
const logger = loggingSystemMock.create().get('ingest_integration.test');
const callback = getPackagePolicyUpdateCallback(logger, licenseService);
@@ -265,7 +275,7 @@ describe('ingest_integration tests ', () => {
licenseEmitter.next(Platinum); // set license level to platinum
});
it('updates successfully when paid features are turned on', async () => {
- const mockPolicy = policyConfigFactory();
+ const mockPolicy = policyFactory();
mockPolicy.windows.popup.malware.message = 'paid feature';
const logger = loggingSystemMock.create().get('ingest_integration.test');
const callback = getPackagePolicyUpdateCallback(logger, licenseService);
diff --git a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts
index 1e7f440ed6788..114c6ba969227 100644
--- a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts
@@ -10,7 +10,10 @@ import { SecurityPluginSetup } from '../../../security/server';
import { ExternalCallback } from '../../../fleet/server';
import { KibanaRequest, Logger, RequestHandlerContext } from '../../../../../src/core/server';
import { NewPackagePolicy, UpdatePackagePolicy } from '../../../fleet/common/types/models';
-import { factory as policyConfigFactory } from '../../common/endpoint/models/policy_config';
+import {
+ policyFactory as policyConfigFactory,
+ policyFactoryWithoutPaidFeatures as policyConfigFactoryWithoutPaidFeatures,
+} from '../../common/endpoint/models/policy_config';
import { NewPolicyData } from '../../common/endpoint/types';
import { ManifestManager } from './services/artifacts';
import { Manifest } from './lib/artifacts';
@@ -22,7 +25,7 @@ import { createDetectionIndex } from '../lib/detection_engine/routes/index/creat
import { createPrepackagedRules } from '../lib/detection_engine/routes/rules/add_prepackaged_rules_route';
import { buildFrameworkRequest } from '../lib/timeline/routes/utils/common';
import { isEndpointPolicyValidForLicense } from '../../common/license/policy_config';
-import { LicenseService } from '../../common/license/license';
+import { isAtLeast, LicenseService } from '../../common/license/license';
const getManifest = async (logger: Logger, manifestManager: ManifestManager): Promise => {
let manifest: Manifest | null = null;
@@ -86,6 +89,7 @@ export const getPackagePolicyCreateCallback = (
maxTimelineImportExportSize: number,
securitySetup: SecurityPluginSetup,
alerts: AlertsStartContract,
+ licenseService: LicenseService,
exceptionsClient: ExceptionListClient | undefined
): ExternalCallback[1] => {
const handlePackagePolicyCreate = async (
@@ -151,6 +155,12 @@ export const getPackagePolicyCreateCallback = (
// Until we get the Default Policy Configuration in the Endpoint package,
// we will add it here manually at creation time.
+
+ // generate the correct default policy depending on the license
+ const defaultPolicy = isAtLeast(licenseService.getLicenseInformation(), 'platinum')
+ ? policyConfigFactory()
+ : policyConfigFactoryWithoutPaidFeatures();
+
updatedPackagePolicy = {
...newPackagePolicy,
inputs: [
@@ -163,7 +173,7 @@ export const getPackagePolicyCreateCallback = (
value: serializedManifest,
},
policy: {
- value: policyConfigFactory(),
+ value: defaultPolicy,
},
},
},
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts
index 225592fa8e686..8e7c4d2d4daf5 100644
--- a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts
@@ -18,7 +18,7 @@ import { licenseMock } from '../../../../../licensing/common/licensing.mock';
import { PackagePolicyServiceInterface } from '../../../../../fleet/server';
import { PackagePolicy } from '../../../../../fleet/common';
import { createPackagePolicyMock } from '../../../../../fleet/common/mocks';
-import { factory } from '../../../../common/endpoint/models/policy_config';
+import { policyFactory } from '../../../../common/endpoint/models/policy_config';
import { PolicyConfig } from '../../../../common/endpoint/types';
const MockPPWithEndpointPolicy = (cb?: (p: PolicyConfig) => PolicyConfig): PackagePolicy => {
@@ -27,7 +27,7 @@ const MockPPWithEndpointPolicy = (cb?: (p: PolicyConfig) => PolicyConfig): Packa
// eslint-disable-next-line no-param-reassign
cb = (p) => p;
}
- const policyConfig = cb(factory());
+ const policyConfig = cb(policyFactory());
packagePolicy.inputs[0].config = { policy: { value: policyConfig } };
return packagePolicy;
};
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 4a2d3b20b339a..833b57868f386 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -7369,7 +7369,6 @@
"xpack.enterpriseSearch.troubleshooting.standardAuth.title": "標準認証の{productName}はサポートされていません",
"xpack.enterpriseSearch.workplaceSearch.activityFeedEmptyDefault.title": "組織には最近のアクティビティがありません",
"xpack.enterpriseSearch.workplaceSearch.activityFeedNamedDefault.title": "{name}には最近のアクティビティがありません",
- "xpack.enterpriseSearch.workplaceSearch.groups.addGroup.cancel.action": "キャンセル",
"xpack.enterpriseSearch.workplaceSearch.groups.addGroup.heading": "グループを追加",
"xpack.enterpriseSearch.workplaceSearch.groups.addGroup.submit.action": "グループを追加",
"xpack.enterpriseSearch.workplaceSearch.groups.addGroupForm.action": "グループを作成",
@@ -7381,7 +7380,6 @@
"xpack.enterpriseSearch.workplaceSearch.groups.filterUsers.buttonText": "ユーザー",
"xpack.enterpriseSearch.workplaceSearch.groups.filterUsers.placeholder": "ユーザーをフィルター...",
"xpack.enterpriseSearch.workplaceSearch.groups.groupDeleted": "グループ「{groupName}」が正常に削除されました。",
- "xpack.enterpriseSearch.workplaceSearch.groups.groupManagerCancel": "キャンセル",
"xpack.enterpriseSearch.workplaceSearch.groups.groupManagerHeaderTitle": "{label}を管理",
"xpack.enterpriseSearch.workplaceSearch.groups.groupManagerSelectAllToggle": "{action}すべて",
"xpack.enterpriseSearch.workplaceSearch.groups.groupManagerSourceEmpty.body": "まだ共有コンテンツソースが追加されていない可能性があります。",
@@ -7406,7 +7404,6 @@
"xpack.enterpriseSearch.workplaceSearch.groups.noSourcesMessage": "共有コンテンツソースがありません",
"xpack.enterpriseSearch.workplaceSearch.groups.noUsersFound": "ユーザーが見つかりません",
"xpack.enterpriseSearch.workplaceSearch.groups.noUsersMessage": "ユーザーがありません",
- "xpack.enterpriseSearch.workplaceSearch.groups.overview.cancelRemoveButtonText": "キャンセル",
"xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveButtonText": "{name}を削除",
"xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveDescription": "グループはWorkplace Searchから削除されます。{name}を削除してよろしいですか?",
"xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmTitleText": "確認",
@@ -18729,8 +18726,6 @@
"xpack.securitySolution.endpoint.policyDetails.malware.userNotification.placeholder": "カスタム通知メッセージを入力",
"xpack.securitySolution.endpoint.policyDetails.supportedVersion": "エージェントバージョン {version}",
"xpack.securitySolution.endpoint.policyDetailsConfig.customizeUserNotification": "通知メッセージをカスタマイズ",
- "xpack.securitySolution.endpoint.policyDetailsConfig.customizeUserNotification.tooltip.a": "ユーザー通知オプションを選択すると、マルウェアが防御または検出されたときに、ホストユーザーに通知を表示します。",
- "xpack.securitySolution.endpoint.policyDetailsConfig.customizeUserNotification.tooltip.b": " ユーザー通知は、以下のテキストボックスでカスタマイズできます。括弧内のタグを使用すると、該当するアクション(防御または検出など)とファイル名を動的に入力できます。",
"xpack.securitySolution.endpoint.policyDetailsConfig.eventingEvents": "イベント",
"xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.file": "ファイル",
"xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.network": "ネットワーク",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index d5ada7f7c7c8f..cacb122d3c61b 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -7388,7 +7388,6 @@
"xpack.enterpriseSearch.troubleshooting.standardAuth.title": "不支持使用标准身份验证的 {productName}",
"xpack.enterpriseSearch.workplaceSearch.activityFeedEmptyDefault.title": "您的组织最近无活动",
"xpack.enterpriseSearch.workplaceSearch.activityFeedNamedDefault.title": "{name} 最近无活动",
- "xpack.enterpriseSearch.workplaceSearch.groups.addGroup.cancel.action": "取消",
"xpack.enterpriseSearch.workplaceSearch.groups.addGroup.heading": "添加组",
"xpack.enterpriseSearch.workplaceSearch.groups.addGroup.submit.action": "添加组",
"xpack.enterpriseSearch.workplaceSearch.groups.addGroupForm.action": "创建组",
@@ -7400,7 +7399,6 @@
"xpack.enterpriseSearch.workplaceSearch.groups.filterUsers.buttonText": "用户",
"xpack.enterpriseSearch.workplaceSearch.groups.filterUsers.placeholder": "筛选用户......",
"xpack.enterpriseSearch.workplaceSearch.groups.groupDeleted": "组“{groupName}”已成功删除。",
- "xpack.enterpriseSearch.workplaceSearch.groups.groupManagerCancel": "取消",
"xpack.enterpriseSearch.workplaceSearch.groups.groupManagerHeaderTitle": "管理 {label}",
"xpack.enterpriseSearch.workplaceSearch.groups.groupManagerSelectAllToggle": "全部{action}",
"xpack.enterpriseSearch.workplaceSearch.groups.groupManagerSourceEmpty.body": "可能您尚未添加任何共享内容源。",
@@ -7425,7 +7423,6 @@
"xpack.enterpriseSearch.workplaceSearch.groups.noSourcesMessage": "无共享内容源",
"xpack.enterpriseSearch.workplaceSearch.groups.noUsersFound": "找不到用户",
"xpack.enterpriseSearch.workplaceSearch.groups.noUsersMessage": "无用户",
- "xpack.enterpriseSearch.workplaceSearch.groups.overview.cancelRemoveButtonText": "取消",
"xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveButtonText": "删除 {name}",
"xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveDescription": "您的组将从 Workplace Search 中删除。确定要移除 {name}?",
"xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmTitleText": "确认",
@@ -18776,8 +18773,6 @@
"xpack.securitySolution.endpoint.policyDetails.malware.userNotification.placeholder": "输入您的定制通知消息",
"xpack.securitySolution.endpoint.policyDetails.supportedVersion": "代理版本 {version}",
"xpack.securitySolution.endpoint.policyDetailsConfig.customizeUserNotification": "定制通知消息",
- "xpack.securitySolution.endpoint.policyDetailsConfig.customizeUserNotification.tooltip.a": "选择用户通知选项后,在阻止或检测到恶意软件时将向主机用户显示通知。",
- "xpack.securitySolution.endpoint.policyDetailsConfig.customizeUserNotification.tooltip.b": " 可在下方文本框中定制用户通知。括号中的标签可用于动态填充适用操作(如已阻止或已检测)和文件名。",
"xpack.securitySolution.endpoint.policyDetailsConfig.eventingEvents": "事件",
"xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.file": "文件",
"xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.network": "网络",
diff --git a/x-pack/plugins/upgrade_assistant/public/application/app.tsx b/x-pack/plugins/upgrade_assistant/public/application/app.tsx
index 17eff71f1039b..2b245bceceb6c 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/app.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/app.tsx
@@ -16,10 +16,10 @@ export interface AppDependencies extends ContextValue {
i18n: I18nStart;
}
-export const RootComponent = ({ i18n, ...contexValue }: AppDependencies) => {
+export const RootComponent = ({ i18n, ...contextValue }: AppDependencies) => {
return (
-
+
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/latest_minor_banner.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/latest_minor_banner.tsx
index 43d0364425cbb..d9ec183231739 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/latest_minor_banner.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/latest_minor_banner.tsx
@@ -10,40 +10,49 @@ import { EuiCallOut, EuiLink } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { CURRENT_MAJOR_VERSION, NEXT_MAJOR_VERSION } from '../../../common/version';
+import { useAppContext } from '../app_context';
-export const LatestMinorBanner: React.FunctionComponent = () => (
-
- }
- color="warning"
- iconType="help"
- >
-
-
+ }
+ color="warning"
+ iconType="help"
+ >
+
+
-
-
- ),
- nextEsVersion: `${NEXT_MAJOR_VERSION}.x`,
- currentEsVersion: `${CURRENT_MAJOR_VERSION}.x`,
- }}
- />
-
-
-);
+ values={{
+ breakingChangesDocButton: (
+
+
+
+ ),
+ nextEsVersion: `${NEXT_MAJOR_VERSION}.x`,
+ currentEsVersion: `${CURRENT_MAJOR_VERSION}.x`,
+ }}
+ />
+
+
+ );
+};
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.test.tsx
index 463c0c9d016b3..6a99bd24ef26b 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.test.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.test.tsx
@@ -17,6 +17,19 @@ const promisesToResolve = () => new Promise((resolve) => setTimeout(resolve, 0))
const mockHttp = httpServiceMock.createSetupContract();
+jest.mock('../app_context', () => {
+ return {
+ useAppContext: () => {
+ return {
+ docLinks: {
+ DOC_LINK_VERSION: 'current',
+ ELASTIC_WEBSITE_URL: 'https://www.elastic.co/',
+ },
+ };
+ },
+ };
+});
+
describe('UpgradeAssistantTabs', () => {
test('renders loading state', async () => {
mockHttp.get.mockReturnValue(
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap
index dda051e715234..5aa4a469e4f02 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap
@@ -40,7 +40,8 @@ exports[`CheckupTab render with deprecations 1`] = `
values={
Object {
"snapshotRestoreDocsButton":
{
+ return {
+ useAppContext: () => {
+ return {
+ docLinks: {
+ DOC_LINK_VERSION: 'current',
+ ELASTIC_WEBSITE_URL: 'https://www.elastic.co/',
+ },
+ };
+ },
+ };
+});
+
/**
* Mostly a dumb container with copy, test the three main states.
*/
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.tsx
index 5688903b8f7cd..02cbc87483e55 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.tsx
@@ -5,7 +5,7 @@
*/
import { find } from 'lodash';
-import React, { Fragment } from 'react';
+import React, { FunctionComponent, useState } from 'react';
import {
EuiCallOut,
@@ -20,211 +20,65 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { NEXT_MAJOR_VERSION } from '../../../../../common/version';
import { LoadingErrorBanner } from '../../error_banner';
+import { useAppContext } from '../../../app_context';
import {
GroupByOption,
LevelFilterOption,
LoadingState,
- UpgradeAssistantTabComponent,
UpgradeAssistantTabProps,
} from '../../types';
import { CheckupControls } from './controls';
import { GroupedDeprecations } from './deprecations/grouped';
-interface CheckupTabProps extends UpgradeAssistantTabProps {
+export interface CheckupTabProps extends UpgradeAssistantTabProps {
checkupLabel: string;
showBackupWarning?: boolean;
}
-interface CheckupTabState {
- currentFilter: LevelFilterOption;
- search: string;
- currentGroupBy: GroupByOption;
-}
-
/**
* Displays a list of deprecations that filterable and groupable. Can be used for cluster,
* nodes, or indices checkups.
*/
-export class CheckupTab extends UpgradeAssistantTabComponent {
- constructor(props: CheckupTabProps) {
- super(props);
-
- this.state = {
- // initialize to all filters
- currentFilter: LevelFilterOption.all,
- search: '',
- currentGroupBy: GroupByOption.message,
- };
- }
-
- public render() {
- const {
- alertBanner,
- checkupLabel,
- deprecations,
- loadingError,
- loadingState,
- refreshCheckupData,
- setSelectedTabIndex,
- showBackupWarning = false,
- } = this.props;
- const { currentFilter, currentGroupBy } = this.state;
-
- return (
-
-
-
-
- {checkupLabel},
- nextEsVersion: `${NEXT_MAJOR_VERSION}.x`,
- }}
- />
-
-
-
-
-
- {alertBanner && (
-
- {alertBanner}
-
-
- )}
-
- {showBackupWarning && (
-
-
- }
- color="warning"
- iconType="help"
- >
-
-
-
-
- ),
- }}
- />
-
-
-
-
- )}
-
-
-
- {loadingState === LoadingState.Error ? (
-
- ) : deprecations && deprecations.length > 0 ? (
-
-
-
- {this.renderCheckupData()}
-
- ) : (
-
-
-
- }
- body={
-
-
- {checkupLabel},
- }}
- />
-
-
- setSelectedTabIndex(0)}>
-
-
- ),
- }}
- />
-
-
- }
- />
- )}
-
-
-
- );
- }
-
- private changeFilter = (filter: LevelFilterOption) => {
- this.setState({ currentFilter: filter });
+export const CheckupTab: FunctionComponent = ({
+ alertBanner,
+ checkupLabel,
+ deprecations,
+ loadingError,
+ loadingState,
+ refreshCheckupData,
+ setSelectedTabIndex,
+ showBackupWarning = false,
+}) => {
+ const [currentFilter, setCurrentFilter] = useState(LevelFilterOption.all);
+ const [search, setSearch] = useState('');
+ const [currentGroupBy, setCurrentGroupBy] = useState(GroupByOption.message);
+
+ const { docLinks } = useAppContext();
+
+ const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } = docLinks;
+ const esDocBasePath = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}`;
+
+ const changeFilter = (filter: LevelFilterOption) => {
+ setCurrentFilter(filter);
};
- private changeSearch = (search: string) => {
- this.setState({ search });
+ const changeSearch = (newSearch: string) => {
+ setSearch(newSearch);
};
- private changeGroupBy = (groupBy: GroupByOption) => {
- this.setState({ currentGroupBy: groupBy });
+ const changeGroupBy = (groupBy: GroupByOption) => {
+ setCurrentGroupBy(groupBy);
};
- private availableGroupByOptions() {
- const { deprecations } = this.props;
-
+ const availableGroupByOptions = () => {
if (!deprecations) {
return [];
}
return Object.keys(GroupByOption).filter((opt) => find(deprecations, opt)) as GroupByOption[];
- }
-
- private renderCheckupData() {
- const { deprecations } = this.props;
- const { currentFilter, currentGroupBy, search } = this.state;
+ };
+ const renderCheckupData = () => {
return (
);
- }
-}
+ };
+
+ return (
+ <>
+
+
+
+ {checkupLabel},
+ nextEsVersion: `${NEXT_MAJOR_VERSION}.x`,
+ }}
+ />
+
+
+
+
+
+ {alertBanner && (
+ <>
+ {alertBanner}
+
+ >
+ )}
+
+ {showBackupWarning && (
+ <>
+
+ }
+ color="warning"
+ iconType="help"
+ >
+
+
+
+
+ ),
+ }}
+ />
+
+
+
+ >
+ )}
+
+
+
+ {loadingState === LoadingState.Error ? (
+
+ ) : deprecations && deprecations.length > 0 ? (
+ <>
+
+
+ {renderCheckupData()}
+ >
+ ) : (
+
+
+
+ }
+ body={
+ <>
+
+ {checkupLabel},
+ }}
+ />
+
+
+ setSelectedTabIndex(0)}>
+
+
+ ),
+ }}
+ />
+
+ >
+ }
+ />
+ )}
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warning_step.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warning_step.test.tsx
index 318d2bc7baffe..6428edfbe904d 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warning_step.test.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warning_step.test.tsx
@@ -11,6 +11,19 @@ import React from 'react';
import { ReindexWarning } from '../../../../../../../../common/types';
import { idForWarning, WarningsFlyoutStep } from './warnings_step';
+jest.mock('../../../../../../app_context', () => {
+ return {
+ useAppContext: () => {
+ return {
+ docLinks: {
+ DOC_LINK_VERSION: 'current',
+ ELASTIC_WEBSITE_URL: 'https://www.elastic.co/',
+ },
+ };
+ },
+ };
+});
+
describe('WarningsFlyoutStep', () => {
const defaultProps = {
advanceNextStep: jest.fn(),
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx
index c3ef0fde6e749..9f48c77ec38e1 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { Fragment } from 'react';
+import React, { useState } from 'react';
import {
EuiButton,
@@ -21,6 +21,7 @@ import {
EuiText,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
+import { useAppContext } from '../../../../../../app_context';
import { ReindexWarning } from '../../../../../../../../common/types';
interface CheckedIds {
@@ -37,7 +38,7 @@ const WarningCheckbox: React.FunctionComponent<{
documentationUrl: string;
onChange: (event: React.ChangeEvent) => void;
}> = ({ checkedIds, warning, label, onChange, description, documentationUrl }) => (
-
+ <>
{description}
-
+
-
+ >
);
interface WarningsConfirmationFlyoutProps {
@@ -68,175 +69,169 @@ interface WarningsConfirmationFlyoutProps {
advanceNextStep: () => void;
}
-interface WarningsConfirmationFlyoutState {
- checkedIds: CheckedIds;
-}
-
/**
* Displays warning text about destructive changes required to reindex this index. The user
* must acknowledge each change before being allowed to proceed.
*/
-export class WarningsFlyoutStep extends React.Component<
- WarningsConfirmationFlyoutProps,
- WarningsConfirmationFlyoutState
-> {
- constructor(props: WarningsConfirmationFlyoutProps) {
- super(props);
-
- this.state = {
- checkedIds: props.warnings.reduce((checkedIds, warning) => {
- checkedIds[idForWarning(warning)] = false;
- return checkedIds;
- }, {} as { [id: string]: boolean }),
- };
- }
-
- public render() {
- const { warnings, closeFlyout, advanceNextStep, renderGlobalCallouts } = this.props;
- const { checkedIds } = this.state;
-
- // Do not allow to proceed until all checkboxes are checked.
- const blockAdvance = Object.values(checkedIds).filter((v) => v).length < warnings.length;
-
- return (
-
-
- {renderGlobalCallouts()}
- = ({
+ warnings,
+ renderGlobalCallouts,
+ closeFlyout,
+ advanceNextStep,
+}) => {
+ const [checkedIds, setCheckedIds] = useState(
+ warnings.reduce((initialCheckedIds, warning) => {
+ initialCheckedIds[idForWarning(warning)] = false;
+ return initialCheckedIds;
+ }, {} as { [id: string]: boolean })
+ );
+
+ // Do not allow to proceed until all checkboxes are checked.
+ const blockAdvance = Object.values(checkedIds).filter((v) => v).length < warnings.length;
+
+ const onChange = (e: React.ChangeEvent) => {
+ const optionId = e.target.id;
+
+ setCheckedIds((prev) => ({
+ ...prev,
+ ...{
+ [optionId]: !checkedIds[optionId],
+ },
+ }));
+ };
+
+ const { docLinks } = useAppContext();
+ const { ELASTIC_WEBSITE_URL } = docLinks;
+ const esDocBasePath = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference`;
+ const observabilityDocBasePath = `${ELASTIC_WEBSITE_URL}guide/en/observability`;
+
+ // TODO: Revisit warnings returned for 8.0 upgrade; many of these are likely obselete now
+ return (
+ <>
+
+ {renderGlobalCallouts()}
+
+ }
+ color="danger"
+ iconType="alert"
+ >
+
+
+
+
+
+
+
+ {warnings.includes(ReindexWarning.allField) && (
+ _all,
+ }}
/>
}
- color="danger"
- iconType="alert"
- >
-
+ description={
-
-
-
-
-
- {warnings.includes(ReindexWarning.allField) && (
- _all,
- }}
- />
- }
- description={
- _all,
- }}
- />
- }
- documentationUrl="https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_the_literal__all_literal_meta_field_is_now_disabled_by_default"
- />
- )}
-
- {warnings.includes(ReindexWarning.apmReindex) && (
-
- }
- description={
-
+ }
+ description={
+
- }
- documentationUrl="https://www.elastic.co/guide/en/apm/get-started/master/apm-release-notes.html"
- />
- )}
-
- {warnings.includes(ReindexWarning.booleanFields) && (
- _source }}
- />
- }
- description={
- _source }}
+ />
+ }
+ description={
+ true,
- false: false ,
- yes: "yes" ,
- on: "on" ,
- one: 1 ,
- }}
- />
- }
- documentationUrl="https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_field"
- />
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- }
-
- private onChange = (e: React.ChangeEvent) => {
- const optionId = e.target.id;
- const nextCheckedIds = {
- ...this.state.checkedIds,
- ...{
- [optionId]: !this.state.checkedIds[optionId],
- },
- };
-
- this.setState({ checkedIds: nextCheckedIds });
- };
-}
+ values={{
+ true: true ,
+ false: false ,
+ yes: "yes" ,
+ on: "on" ,
+ one: 1 ,
+ }}
+ />
+ }
+ documentationUrl={`${esDocBasePath}/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_field`}
+ />
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/steps.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/steps.tsx
index 85d275b080e13..1a1ea48a350c8 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/steps.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/overview/steps.tsx
@@ -54,7 +54,7 @@ const WAIT_FOR_RELEASE_STEP = {
// Swap in this step for the one above it on the last minor release.
// @ts-ignore
-const START_UPGRADE_STEP = (isCloudEnabled: boolean) => ({
+const START_UPGRADE_STEP = (isCloudEnabled: boolean, esDocBasePath: string) => ({
title: i18n.translate('xpack.upgradeAssistant.overviewTab.steps.startUpgradeStep.stepTitle', {
defaultMessage: 'Start your upgrade',
}),
@@ -73,10 +73,7 @@ const START_UPGRADE_STEP = (isCloudEnabled: boolean) => ({
defaultMessage="Follow {instructionButton} to start your upgrade."
values={{
instructionButton: (
-
+
);
diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts
index cf944008c08d6..4193843c63bce 100644
--- a/x-pack/test/alerting_api_integration/common/config.ts
+++ b/x-pack/test/alerting_api_integration/common/config.ts
@@ -17,6 +17,7 @@ interface CreateTestConfigOptions {
disabledPlugins?: string[];
ssl?: boolean;
enableActionsProxy: boolean;
+ rejectUnauthorized?: boolean;
}
// test.not-enabled is specifically not enabled
@@ -39,7 +40,12 @@ const enabledActionTypes = [
];
export function createTestConfig(name: string, options: CreateTestConfigOptions) {
- const { license = 'trial', disabledPlugins = [], ssl = false } = options;
+ const {
+ license = 'trial',
+ disabledPlugins = [],
+ ssl = false,
+ rejectUnauthorized = true,
+ } = options;
return async ({ readConfigFile }: FtrConfigProviderContext) => {
const xPackApiIntegrationTestsConfig = await readConfigFile(
@@ -95,6 +101,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
'--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"',
'--xpack.alerts.invalidateApiKeysTask.interval="15s"',
`--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`,
+ `--xpack.actions.rejectUnauthorized=${rejectUnauthorized}`,
...actionsProxyUrl,
'--xpack.eventLog.logEntries=true',
diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts
index e7ce0638c6319..18f3c83b00141 100644
--- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts
+++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts
@@ -5,6 +5,7 @@
*/
import http from 'http';
+import https from 'https';
import { Plugin, CoreSetup, IRouter } from 'kibana/server';
import { EncryptedSavedObjectsPluginStart } from '../../../../../../../plugins/encrypted_saved_objects/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../../plugins/features/server';
@@ -47,7 +48,13 @@ export function getAllExternalServiceSimulatorPaths(): string[] {
}
export async function getWebhookServer(): Promise {
- return await initWebhook();
+ const { httpServer } = await initWebhook();
+ return httpServer;
+}
+
+export async function getHttpsWebhookServer(): Promise {
+ const { httpsServer } = await initWebhook();
+ return httpsServer;
}
export async function getSlackServer(): Promise {
diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/webhook_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/webhook_simulation.ts
index a34293090d7af..116f0604a37c9 100644
--- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/webhook_simulation.ts
+++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/webhook_simulation.ts
@@ -3,16 +3,35 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
+import fs from 'fs';
import expect from '@kbn/expect';
import http from 'http';
+import https from 'https';
+import { promisify } from 'util';
import { fromNullable, map, filter, getOrElse } from 'fp-ts/lib/Option';
import { pipe } from 'fp-ts/lib/pipeable';
import { constant } from 'fp-ts/lib/function';
+import { KBN_KEY_PATH, KBN_CERT_PATH } from '@kbn/dev-utils';
export async function initPlugin() {
- const payloads: string[] = [];
+ const httpsServerKey = await promisify(fs.readFile)(KBN_KEY_PATH, 'utf8');
+ const httpsServerCert = await promisify(fs.readFile)(KBN_CERT_PATH, 'utf8');
+
+ return {
+ httpServer: http.createServer(createServerCallback()),
+ httpsServer: https.createServer(
+ {
+ key: httpsServerKey,
+ cert: httpsServerCert,
+ },
+ createServerCallback()
+ ),
+ };
+}
- return http.createServer((request, response) => {
+function createServerCallback() {
+ const payloads: string[] = [];
+ return (request: http.IncomingMessage, response: http.ServerResponse) => {
const credentials = pipe(
fromNullable(request.headers.authorization),
map((authorization) => authorization.split(/\s+/)),
@@ -77,7 +96,7 @@ export async function initPlugin() {
return;
});
}
- });
+ };
}
function validateAuthentication(credentials: any, res: any) {
diff --git a/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts b/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts
index 6336d834c3943..5b093dfb28eab 100644
--- a/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts
+++ b/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts
@@ -28,6 +28,7 @@ interface GetEventLogParams {
id: string;
provider: string;
actions: Map;
+ filter?: string;
}
// Return event log entries given the specified parameters; for the `actions`
@@ -37,7 +38,9 @@ export async function getEventLog(params: GetEventLogParams): Promise {
- let webhookSimulatorURL: string = '';
- let webhookServer: http.Server;
- before(async () => {
- webhookServer = await getWebhookServer();
- const availablePort = await getPort({ port: 9000 });
- webhookServer.listen(availablePort);
- webhookSimulatorURL = `http://localhost:${availablePort}`;
- });
+ describe('with http endpoint', () => {
+ let webhookSimulatorURL: string = '';
+ let webhookServer: http.Server;
+ before(async () => {
+ webhookServer = await getWebhookServer();
+ const availablePort = await getPort({ port: 9000 });
+ webhookServer.listen(availablePort);
+ webhookSimulatorURL = `http://localhost:${availablePort}`;
+ });
+
+ it('webhook can be executed without username and password', async () => {
+ const webhookActionId = await createWebhookAction(webhookSimulatorURL);
+ const { body: result } = await supertest
+ .post(`/api/actions/action/${webhookActionId}/_execute`)
+ .set('kbn-xsrf', 'test')
+ .send({
+ params: {
+ body: 'success',
+ },
+ })
+ .expect(200);
- it('webhook can be executed without username and password', async () => {
- const webhookActionId = await createWebhookAction(webhookSimulatorURL);
- const { body: result } = await supertest
- .post(`/api/actions/action/${webhookActionId}/_execute`)
- .set('kbn-xsrf', 'test')
- .send({
- params: {
- body: 'success',
- },
- })
- .expect(200);
+ expect(result.status).to.eql('ok');
+ });
- expect(result.status).to.eql('ok');
+ after(() => {
+ webhookServer.close();
+ });
});
- after(() => {
- webhookServer.close();
+ describe('with https endpoint and rejectUnauthorized=false', () => {
+ let webhookSimulatorURL: string = '';
+ let webhookServer: https.Server;
+
+ before(async () => {
+ webhookServer = await getHttpsWebhookServer();
+ const availablePort = await getPort({ port: getPort.makeRange(9000, 9100) });
+ webhookServer.listen(availablePort);
+ webhookSimulatorURL = `https://localhost:${availablePort}`;
+ });
+
+ it('should support the POST method against webhook target', async () => {
+ const webhookActionId = await createWebhookAction(webhookSimulatorURL, { method: 'post' });
+ const { body: result } = await supertest
+ .post(`/api/actions/action/${webhookActionId}/_execute`)
+ .set('kbn-xsrf', 'test')
+ .send({
+ params: {
+ body: 'success_post_method',
+ },
+ })
+ .expect(200);
+
+ expect(result.status).to.eql('ok');
+ });
+
+ after(() => {
+ webhookServer.close();
+ });
});
});
}
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts
index d3e1370bef285..5ff7b0d45a019 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts
@@ -84,6 +84,23 @@ export default function eventLogTests({ getService }: FtrProviderContext) {
});
});
+ // get the filtered events only with action 'new-instance'
+ const filteredEvents = await retry.try(async () => {
+ return await getEventLog({
+ getService,
+ spaceId: Spaces.space1.id,
+ type: 'alert',
+ id: alertId,
+ provider: 'alerting',
+ actions: new Map([['new-instance', { equal: 1 }]]),
+ filter: 'event.action:(new-instance)',
+ });
+ });
+
+ expect(getEventsByAction(filteredEvents, 'execute').length).equal(0);
+ expect(getEventsByAction(filteredEvents, 'execute-action').length).equal(0);
+ expect(getEventsByAction(events, 'new-instance').length).equal(1);
+
const executeEvents = getEventsByAction(events, 'execute');
const executeActionEvents = getEventsByAction(events, 'execute-action');
const newInstanceEvents = getEventsByAction(events, 'new-instance');
diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts
index f53c1c589daab..ce1b58433362b 100644
--- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts
+++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts
@@ -255,11 +255,16 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
events: { file: false, network: true, process: true },
logging: { file: 'info' },
malware: { mode: 'prevent' },
+ ransomware: { mode: 'prevent' },
popup: {
malware: {
enabled: true,
message: 'Elastic Security {action} {filename}',
},
+ ransomware: {
+ enabled: true,
+ message: 'Elastic Security {action} {filename}',
+ },
},
},
windows: {
@@ -274,11 +279,16 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
},
logging: { file: 'info' },
malware: { mode: 'prevent' },
+ ransomware: { mode: 'prevent' },
popup: {
malware: {
enabled: true,
message: 'Elastic Security {action} {filename}',
},
+ ransomware: {
+ enabled: true,
+ message: 'Elastic Security {action} {filename}',
+ },
},
antivirus_registration: {
enabled: false,
@@ -399,11 +409,16 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
events: { file: true, network: true, process: true },
logging: { file: 'info' },
malware: { mode: 'prevent' },
+ ransomware: { mode: 'prevent' },
popup: {
malware: {
enabled: true,
message: 'Elastic Security {action} {filename}',
},
+ ransomware: {
+ enabled: true,
+ message: 'Elastic Security {action} {filename}',
+ },
},
},
windows: {
@@ -418,11 +433,16 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
},
logging: { file: 'info' },
malware: { mode: 'prevent' },
+ ransomware: { mode: 'prevent' },
popup: {
malware: {
enabled: true,
message: 'Elastic Security {action} {filename}',
},
+ ransomware: {
+ enabled: true,
+ message: 'Elastic Security {action} {filename}',
+ },
},
antivirus_registration: {
enabled: false,
@@ -536,11 +556,16 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
events: { file: true, network: true, process: true },
logging: { file: 'info' },
malware: { mode: 'prevent' },
+ ransomware: { mode: 'prevent' },
popup: {
malware: {
enabled: true,
message: 'Elastic Security {action} {filename}',
},
+ ransomware: {
+ enabled: true,
+ message: 'Elastic Security {action} {filename}',
+ },
},
},
windows: {
@@ -555,11 +580,16 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
},
logging: { file: 'info' },
malware: { mode: 'prevent' },
+ ransomware: { mode: 'prevent' },
popup: {
malware: {
enabled: true,
message: 'Elastic Security {action} {filename}',
},
+ ransomware: {
+ enabled: true,
+ message: 'Elastic Security {action} {filename}',
+ },
},
antivirus_registration: {
enabled: false,
diff --git a/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts b/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts
index 5f54ab2539c5d..82e47896ce411 100644
--- a/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts
+++ b/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts
@@ -16,7 +16,7 @@ import {
GetFullAgentPolicyResponse,
GetPackagesResponse,
} from '../../../plugins/fleet/common';
-import { factory as policyConfigFactory } from '../../../plugins/security_solution/common/endpoint/models/policy_config';
+import { policyFactory } from '../../../plugins/security_solution/common/endpoint/models/policy_config';
import { Immutable } from '../../../plugins/security_solution/common/endpoint/types';
// NOTE: import path below should be the deep path to the actual module - else we get CI errors
@@ -178,7 +178,7 @@ export function EndpointPolicyTestResourcesProvider({ getService }: FtrProviderC
streams: [],
config: {
policy: {
- value: policyConfigFactory(),
+ value: policyFactory(),
},
},
},
diff --git a/yarn.lock b/yarn.lock
index fcafc23e42d73..befb729569945 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3517,6 +3517,10 @@
version "0.0.0"
uid ""
+"@kbn/tinymath@link:packages/kbn-tinymath":
+ version "0.0.0"
+ uid ""
+
"@kbn/ui-framework@link:packages/kbn-ui-framework":
version "0.0.0"
uid ""
@@ -28056,11 +28060,6 @@ tinygradient@0.4.3:
"@types/tinycolor2" "^1.4.0"
tinycolor2 "^1.0.0"
-tinymath@1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/tinymath/-/tinymath-1.2.1.tgz#f97ed66c588cdbf3c19dfba2ae266ee323db7e47"
- integrity sha512-8CYutfuHR3ywAJus/3JUhaJogZap1mrUQGzNxdBiQDhP3H0uFdQenvaXvqI8lMehX4RsanRZzxVfjMBREFdQaA==
-
tinyqueue@^1.1.0:
version "1.2.3"
resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-1.2.3.tgz#b6a61de23060584da29f82362e45df1ec7353f3d"