diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_footer.test.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_footer.test.tsx
new file mode 100644
index 0000000000000..a4bfe84b68126
--- /dev/null
+++ b/src/plugins/discover/public/components/discover_grid/discover_grid_footer.test.tsx
@@ -0,0 +1,138 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+import { mountWithIntl } from '@kbn/test-jest-helpers';
+import { findTestSubject } from '@elastic/eui/lib/test';
+import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
+import { DiscoverGridFooter } from './discover_grid_footer';
+import { discoverServiceMock } from '../../__mocks__/services';
+
+describe('DiscoverGridFooter', function () {
+ it('should not render anything when not on the last page', async () => {
+ const component = mountWithIntl(
+
+
+
+ );
+ expect(component.isEmptyRender()).toBe(true);
+ });
+
+ it('should not render anything yet when all rows shown', async () => {
+ const component = mountWithIntl(
+
+
+
+ );
+ expect(component.isEmptyRender()).toBe(true);
+ });
+
+ it('should render a message for the last page', async () => {
+ const component = mountWithIntl(
+
+
+
+ );
+ expect(findTestSubject(component, 'discoverTableFooter').text()).toBe(
+ 'Search results are limited to 500 documents. Add more search terms to narrow your search.'
+ );
+ expect(findTestSubject(component, 'dscGridSampleSizeFetchMoreLink').exists()).toBe(false);
+ });
+
+ it('should render a message and the button for the last page', async () => {
+ const mockLoadMore = jest.fn();
+
+ const component = mountWithIntl(
+
+
+
+ );
+ expect(findTestSubject(component, 'discoverTableFooter').text()).toBe(
+ 'Search results are limited to 500 documents.Load more'
+ );
+
+ const button = findTestSubject(component, 'dscGridSampleSizeFetchMoreLink');
+ expect(button.exists()).toBe(true);
+
+ button.simulate('click');
+
+ expect(mockLoadMore).toHaveBeenCalledTimes(1);
+ });
+
+ it('should render a disabled button when loading more', async () => {
+ const mockLoadMore = jest.fn();
+
+ const component = mountWithIntl(
+
+
+
+ );
+ expect(findTestSubject(component, 'discoverTableFooter').text()).toBe(
+ 'Search results are limited to 500 documents.Load more'
+ );
+
+ const button = findTestSubject(component, 'dscGridSampleSizeFetchMoreLink');
+ expect(button.exists()).toBe(true);
+ expect(button.prop('disabled')).toBe(true);
+
+ button.simulate('click');
+
+ expect(mockLoadMore).not.toHaveBeenCalled();
+ });
+
+ it('should render a message when max total limit is reached', async () => {
+ const component = mountWithIntl(
+
+
+
+ );
+ expect(findTestSubject(component, 'discoverTableFooter').text()).toBe(
+ 'Search results are limited to 10000 documents. Add more search terms to narrow your search.'
+ );
+ });
+});
diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_footer.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_footer.tsx
new file mode 100644
index 0000000000000..540c7102bd424
--- /dev/null
+++ b/src/plugins/discover/public/components/discover_grid/discover_grid_footer.tsx
@@ -0,0 +1,161 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React, { useEffect, useState } from 'react';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { EuiButtonEmpty, EuiToolTip, useEuiTheme } from '@elastic/eui';
+import { css } from '@emotion/react';
+import { i18n } from '@kbn/i18n';
+import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types';
+import { MAX_LOADED_GRID_ROWS } from '../../../common/constants';
+import { useDiscoverServices } from '../../hooks/use_discover_services';
+
+export interface DiscoverGridFooterProps {
+ isLoadingMore?: boolean;
+ rowCount: number;
+ sampleSize: number;
+ pageIndex?: number; // starts from 0
+ pageCount: number;
+ totalHits?: number;
+ onFetchMoreRecords?: () => void;
+}
+
+export const DiscoverGridFooter: React.FC = (props) => {
+ const {
+ isLoadingMore,
+ rowCount,
+ sampleSize,
+ pageIndex,
+ pageCount,
+ totalHits = 0,
+ onFetchMoreRecords,
+ } = props;
+ const { data } = useDiscoverServices();
+ const timefilter = data.query.timefilter.timefilter;
+ const [refreshInterval, setRefreshInterval] = useState(timefilter.getRefreshInterval());
+
+ useEffect(() => {
+ const subscriber = timefilter.getRefreshIntervalUpdate$().subscribe(() => {
+ setRefreshInterval(timefilter.getRefreshInterval());
+ });
+
+ return () => subscriber.unsubscribe();
+ }, [timefilter, setRefreshInterval]);
+
+ const isRefreshIntervalOn = Boolean(
+ refreshInterval && refreshInterval.pause === false && refreshInterval.value > 0
+ );
+
+ const { euiTheme } = useEuiTheme();
+ const isOnLastPage = pageIndex === pageCount - 1 && rowCount < totalHits;
+
+ if (!isOnLastPage) {
+ return null;
+ }
+
+ // allow to fetch more records on Discover page
+ if (onFetchMoreRecords && typeof isLoadingMore === 'boolean') {
+ if (rowCount <= MAX_LOADED_GRID_ROWS - sampleSize) {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ return ;
+ }
+
+ if (rowCount < totalHits) {
+ // show only a message for embeddable
+ return ;
+ }
+
+ return null;
+};
+
+interface DiscoverGridFooterContainerProps extends DiscoverGridFooterProps {
+ hasButton: boolean;
+}
+
+const DiscoverGridFooterContainer: React.FC = ({
+ hasButton,
+ rowCount,
+ children,
+}) => {
+ const { euiTheme } = useEuiTheme();
+ const { fieldFormats } = useDiscoverServices();
+
+ const formattedRowCount = fieldFormats
+ .getDefaultInstance(KBN_FIELD_TYPES.NUMBER, [ES_FIELD_TYPES.INTEGER])
+ .convert(rowCount);
+
+ return (
+
+
+ {hasButton ? (
+
+ ) : (
+
+ )}
+
+ {children}
+
+ );
+};
diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx
index 23e22de975ec3..cfdba5d56b3eb 100644
--- a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx
+++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx
@@ -56,7 +56,6 @@ import {
SAMPLE_SIZE_SETTING,
SEARCH_FIELDS_FROM_SOURCE,
SHOW_FIELD_STATISTICS,
- SORT_DEFAULT_ORDER_SETTING,
buildDataTableRecord,
} from '@kbn/discover-utils';
import { VIEW_MODE, DISABLE_SHARD_FAILURE_WARNING } from '../../common/constants';
@@ -276,8 +275,8 @@ export class SavedSearchEmbeddable
useNewFieldsApi,
{
sampleSize: this.services.uiSettings.get(SAMPLE_SIZE_SETTING),
- defaultSort: this.services.uiSettings.get(SORT_DEFAULT_ORDER_SETTING),
- }
+ },
+ this.services.uiSettings
);
// Log request to inspector
diff --git a/src/plugins/discover/public/embeddable/saved_search_grid.tsx b/src/plugins/discover/public/embeddable/saved_search_grid.tsx
index 87258347b474e..142bb1cff8118 100644
--- a/src/plugins/discover/public/embeddable/saved_search_grid.tsx
+++ b/src/plugins/discover/public/embeddable/saved_search_grid.tsx
@@ -33,6 +33,7 @@ export function DiscoverGridEmbeddable(props: DiscoverGridEmbeddableProps) {
>
(key === SORT_DEFAULT_ORDER_SETTING ? 'asc' : null));
describe('updateSearchSource', () => {
const defaults = {
sampleSize: 50,
- defaultSort: 'asc',
};
it('updates a given search source', async () => {
const searchSource = createSearchSourceMock({});
- updateSearchSource(searchSource, dataViewMock, [] as SortOrder[], false, defaults);
+ updateSearchSource(
+ searchSource,
+ dataViewMock,
+ [] as SortOrder[],
+ false,
+ defaults,
+ uiSettingWithAscSorting
+ );
expect(searchSource.getField('fields')).toBe(undefined);
// does not explicitly request fieldsFromSource when not using fields API
expect(searchSource.getField('fieldsFromSource')).toBe(undefined);
@@ -26,7 +39,14 @@ describe('updateSearchSource', () => {
it('updates a given search source with the usage of the new fields api', async () => {
const searchSource = createSearchSourceMock({});
- updateSearchSource(searchSource, dataViewMock, [] as SortOrder[], true, defaults);
+ updateSearchSource(
+ searchSource,
+ dataViewMock,
+ [] as SortOrder[],
+ true,
+ defaults,
+ uiSettingWithAscSorting
+ );
expect(searchSource.getField('fields')).toEqual([{ field: '*', include_unmapped: 'true' }]);
expect(searchSource.getField('fieldsFromSource')).toBe(undefined);
});
diff --git a/src/plugins/discover/public/embeddable/utils/update_search_source.ts b/src/plugins/discover/public/embeddable/utils/update_search_source.ts
index 87ee137aed796..d3885ee210f9c 100644
--- a/src/plugins/discover/public/embeddable/utils/update_search_source.ts
+++ b/src/plugins/discover/public/embeddable/utils/update_search_source.ts
@@ -8,6 +8,7 @@
import type { DataView } from '@kbn/data-views-plugin/public';
import type { ISearchSource } from '@kbn/data-plugin/public';
import type { SortOrder } from '@kbn/saved-search-plugin/public';
+import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
import { getSortForSearchSource } from '../../utils/sorting';
export const updateSearchSource = (
@@ -17,12 +18,15 @@ export const updateSearchSource = (
useNewFieldsApi: boolean,
defaults: {
sampleSize: number;
- defaultSort: string;
- }
+ },
+ uiSettings: IUiSettingsClient
) => {
- const { sampleSize, defaultSort } = defaults;
+ const { sampleSize } = defaults;
searchSource.setField('size', sampleSize);
- searchSource.setField('sort', getSortForSearchSource(sort, dataView, defaultSort));
+ searchSource.setField(
+ 'sort',
+ getSortForSearchSource({ sort, dataView, uiSettings, includeTieBreaker: true })
+ );
if (useNewFieldsApi) {
searchSource.removeField('fieldsFromSource');
const fields: Record = { field: '*', include_unmapped: 'true' };
diff --git a/src/plugins/discover/public/utils/get_sharing_data.ts b/src/plugins/discover/public/utils/get_sharing_data.ts
index 3334ecd7220c4..89a161416189d 100644
--- a/src/plugins/discover/public/utils/get_sharing_data.ts
+++ b/src/plugins/discover/public/utils/get_sharing_data.ts
@@ -15,11 +15,7 @@ import type {
} from '@kbn/data-plugin/public';
import type { Filter } from '@kbn/es-query';
import type { SavedSearch, SortOrder } from '@kbn/saved-search-plugin/public';
-import {
- DOC_HIDE_TIME_COLUMN_SETTING,
- SEARCH_FIELDS_FROM_SOURCE,
- SORT_DEFAULT_ORDER_SETTING,
-} from '@kbn/discover-utils';
+import { DOC_HIDE_TIME_COLUMN_SETTING, SEARCH_FIELDS_FROM_SOURCE } from '@kbn/discover-utils';
import {
DiscoverAppState,
isEqualFilters,
@@ -35,14 +31,18 @@ export async function getSharingData(
services: { uiSettings: IUiSettingsClient; data: DataPublicPluginStart },
isPlainRecord?: boolean
) {
- const { uiSettings: config, data } = services;
+ const { uiSettings, data } = services;
const searchSource = currentSearchSource.createCopy();
const index = searchSource.getField('index')!;
let existingFilter = searchSource.getField('filter') as Filter[] | Filter | undefined;
searchSource.setField(
'sort',
- getSortForSearchSource(state.sort as SortOrder[], index, config.get(SORT_DEFAULT_ORDER_SETTING))
+ getSortForSearchSource({
+ sort: state.sort as SortOrder[],
+ dataView: index,
+ uiSettings,
+ })
);
searchSource.removeField('filter');
@@ -57,7 +57,7 @@ export async function getSharingData(
if (columns && columns.length > 0) {
// conditionally add the time field column:
let timeFieldName: string | undefined;
- const hideTimeColumn = config.get(DOC_HIDE_TIME_COLUMN_SETTING);
+ const hideTimeColumn = uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING);
if (!hideTimeColumn && index && index.timeFieldName && !isPlainRecord) {
timeFieldName = index.timeFieldName;
}
@@ -98,7 +98,7 @@ export async function getSharingData(
* Otherwise, the requests will ask for all fields, even if only a few are really needed.
* Discover does not set fields, since having all fields is needed for the UI.
*/
- const useFieldsApi = !config.get(SEARCH_FIELDS_FROM_SOURCE);
+ const useFieldsApi = !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE);
if (useFieldsApi) {
searchSource.removeField('fieldsFromSource');
const fields = columns.length
diff --git a/src/plugins/discover/server/locator/searchsource_from_locator.ts b/src/plugins/discover/server/locator/searchsource_from_locator.ts
index 455efc968b534..9d9f1e924fe01 100644
--- a/src/plugins/discover/server/locator/searchsource_from_locator.ts
+++ b/src/plugins/discover/server/locator/searchsource_from_locator.ts
@@ -11,6 +11,7 @@ import { DataView } from '@kbn/data-views-plugin/common';
import { AggregateQuery, Filter, Query } from '@kbn/es-query';
import { SavedSearch } from '@kbn/saved-search-plugin/common';
import { getSavedSearch } from '@kbn/saved-search-plugin/server';
+import { SORT_DEFAULT_ORDER_SETTING } from '@kbn/discover-utils';
import { LocatorServicesDeps } from '.';
import { DiscoverAppLocatorParams } from '../../common';
import { getSortForSearchSource } from '../../common/utils/sorting';
@@ -147,7 +148,17 @@ export function searchSourceFromLocatorFactory(services: LocatorServicesDeps) {
// Inject sort
if (savedSearch.sort) {
- const sort = getSortForSearchSource(savedSearch.sort as Array<[string, string]>, index);
+ const defaultSortingSetting = await services.uiSettings.get(SORT_DEFAULT_ORDER_SETTING);
+ const uiSettingsSyncReplacement = {
+ get: (key: string) =>
+ key === SORT_DEFAULT_ORDER_SETTING ? defaultSortingSetting : undefined,
+ };
+
+ const sort = getSortForSearchSource({
+ sort: savedSearch.sort as Array<[string, string]>,
+ dataView: index,
+ uiSettings: uiSettingsSyncReplacement,
+ });
searchSource.setField('sort', sort);
}
diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json
index 6e3ccd93599d0..7f1fc8d6dfabb 100644
--- a/src/plugins/discover/tsconfig.json
+++ b/src/plugins/discover/tsconfig.json
@@ -65,6 +65,7 @@
"@kbn/core-application-browser",
"@kbn/core-saved-objects-server",
"@kbn/discover-utils",
+ "@kbn/field-types",
"@kbn/search-response-warnings",
"@kbn/content-management-plugin",
"@kbn/react-kibana-mount",
diff --git a/src/plugins/files/server/usage/integration_tests/usage.test.ts b/src/plugins/files/server/usage/integration_tests/usage.test.ts
index 02cc7dfee55fa..9455453023732 100644
--- a/src/plugins/files/server/usage/integration_tests/usage.test.ts
+++ b/src/plugins/files/server/usage/integration_tests/usage.test.ts
@@ -6,6 +6,10 @@
* Side Public License, v 1.
*/
+import {
+ ELASTIC_HTTP_VERSION_HEADER,
+ X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
+} from '@kbn/core-http-common';
import { setupIntegrationEnvironment, TestEnvironmentUtils } from '../../test_utils';
describe('Files usage telemetry', () => {
@@ -45,7 +49,9 @@ describe('Files usage telemetry', () => {
]);
const { body } = await request
- .post(root, '/api/telemetry/v2/clusters/_stats')
+ .post(root, '/internal/telemetry/clusters/_stats')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: true });
expect(body[0].stats.stack_stats.kibana.plugins.files).toMatchInlineSnapshot(`
diff --git a/src/plugins/files/tsconfig.json b/src/plugins/files/tsconfig.json
index 08d910f23c5e9..a45132f21d592 100644
--- a/src/plugins/files/tsconfig.json
+++ b/src/plugins/files/tsconfig.json
@@ -32,6 +32,7 @@
"@kbn/core-elasticsearch-server-mocks",
"@kbn/core-saved-objects-server-mocks",
"@kbn/logging",
+ "@kbn/core-http-common",
],
"exclude": [
"target/**/*",
diff --git a/src/plugins/presentation_util/public/__stories__/fixtures/flights.ts b/src/plugins/presentation_util/public/__stories__/fixtures/flights.ts
index 17cedff90ad23..7b6d6efc880c8 100644
--- a/src/plugins/presentation_util/public/__stories__/fixtures/flights.ts
+++ b/src/plugins/presentation_util/public/__stories__/fixtures/flights.ts
@@ -61,14 +61,19 @@ const numberFields = [
const getConfig = (() => {}) as FieldFormatsGetConfigFn;
export const flightFieldByName: { [key: string]: DataViewField } = {};
-flightFieldNames.forEach(
- (flightFieldName) =>
- (flightFieldByName[flightFieldName] = {
- name: flightFieldName,
- type: numberFields.includes(flightFieldName) ? 'number' : 'string',
- aggregatable: true,
- } as unknown as DataViewField)
-);
+flightFieldNames.forEach((flightFieldName) => {
+ const fieldBase = {
+ name: flightFieldName,
+ type: numberFields.includes(flightFieldName) ? 'number' : 'string',
+ aggregatable: true,
+ };
+ flightFieldByName[flightFieldName] = {
+ ...fieldBase,
+ toSpec: () => {
+ return fieldBase;
+ },
+ } as unknown as DataViewField;
+});
flightFieldByName.Cancelled = { name: 'Cancelled', type: 'boolean' } as DataViewField;
flightFieldByName.timestamp = { name: 'timestamp', type: 'date' } as DataViewField;
diff --git a/src/plugins/telemetry/common/routes.ts b/src/plugins/telemetry/common/routes.ts
index 06d6f746bf2c1..ce4db9c87b5ea 100644
--- a/src/plugins/telemetry/common/routes.ts
+++ b/src/plugins/telemetry/common/routes.ts
@@ -6,7 +6,49 @@
* Side Public License, v 1.
*/
+const BASE_INTERNAL_PATH = '/internal/telemetry';
+
+export const INTERNAL_VERSION = { version: '2' };
+
+/**
+ * Fetch Telemetry Config
+ * @public Kept public and path-based because we know other Elastic products fetch the opt-in status via this endpoint.
+ */
+export const FetchTelemetryConfigRoutePathBasedV2 = '/api/telemetry/v2/config';
+
/**
* Fetch Telemetry Config
+ * @internal
+ */
+export const FetchTelemetryConfigRoute = `${BASE_INTERNAL_PATH}/config`;
+
+/**
+ * GET/PUT Last reported date for Snapshot telemetry
+ * @internal
+ */
+export const LastReportedRoute = `${BASE_INTERNAL_PATH}/last_reported`;
+
+/**
+ * Set user has seen notice
+ * @internal
+ */
+export const UserHasSeenNoticeRoute = `${BASE_INTERNAL_PATH}/userHasSeenNotice`;
+
+/**
+ * Set opt-in/out status
+ * @internal
+ */
+export const OptInRoute = `${BASE_INTERNAL_PATH}/optIn`;
+
+/**
+ * Fetch the Snapshot telemetry report
+ * @internal
+ */
+export const FetchSnapshotTelemetry = `${BASE_INTERNAL_PATH}/clusters/_stats`;
+
+/**
+ * Get Opt-in stats
+ * @internal
+ * @deprecated
*/
-export const FetchTelemetryConfigRoute = '/api/telemetry/v2/config';
+export const GetOptInStatsRoutePathBasedV2 = '/api/telemetry/v2/clusters/_opt_in_stats';
diff --git a/src/plugins/telemetry/common/types/index.ts b/src/plugins/telemetry/common/types/index.ts
index 14b2d3cbefcf4..f03d8f821b1eb 100644
--- a/src/plugins/telemetry/common/types/index.ts
+++ b/src/plugins/telemetry/common/types/index.ts
@@ -8,4 +8,5 @@
export * from './latest';
+export * as v1 from './v2'; // Just so v1 can also be used (but for some reason telemetry endpoints have always been v2 :shrug:)
export * as v2 from './v2';
diff --git a/src/plugins/telemetry/public/plugin.ts b/src/plugins/telemetry/public/plugin.ts
index cf879472a2bb0..c0d0faf0819b0 100644
--- a/src/plugins/telemetry/public/plugin.ts
+++ b/src/plugins/telemetry/public/plugin.ts
@@ -24,7 +24,7 @@ import type { HomePublicPluginSetup } from '@kbn/home-plugin/public';
import { ElasticV3BrowserShipper } from '@kbn/analytics-shippers-elastic-v3-browser';
import { of } from 'rxjs';
-import { FetchTelemetryConfigRoute } from '../common/routes';
+import { FetchTelemetryConfigRoute, INTERNAL_VERSION } from '../common/routes';
import type { v2 } from '../common/types';
import { TelemetrySender, TelemetryService, TelemetryNotifications } from './services';
import { renderWelcomeTelemetryNotice } from './render_welcome_telemetry_notice';
@@ -329,7 +329,7 @@ export class TelemetryPlugin
*/
private async fetchUpdatedConfig(http: HttpStart | HttpSetup): Promise {
const { allowChangingOptInStatus, optIn, sendUsageFrom, telemetryNotifyUserAboutOptInDefault } =
- await http.get(FetchTelemetryConfigRoute);
+ await http.get(FetchTelemetryConfigRoute, INTERNAL_VERSION);
return {
...this.config,
diff --git a/src/plugins/telemetry/public/services/telemetry_service.test.ts b/src/plugins/telemetry/public/services/telemetry_service.test.ts
index 4934495d57d8b..d072d654cceaa 100644
--- a/src/plugins/telemetry/public/services/telemetry_service.test.ts
+++ b/src/plugins/telemetry/public/services/telemetry_service.test.ts
@@ -10,6 +10,12 @@
/* eslint-disable dot-notation */
import { mockTelemetryService } from '../mocks';
+import {
+ FetchSnapshotTelemetry,
+ INTERNAL_VERSION,
+ OptInRoute,
+ UserHasSeenNoticeRoute,
+} from '../../common/routes';
describe('TelemetryService', () => {
describe('fetchTelemetry', () => {
@@ -17,7 +23,8 @@ describe('TelemetryService', () => {
const telemetryService = mockTelemetryService();
await telemetryService.fetchTelemetry();
- expect(telemetryService['http'].post).toBeCalledWith('/api/telemetry/v2/clusters/_stats', {
+ expect(telemetryService['http'].post).toBeCalledWith(FetchSnapshotTelemetry, {
+ ...INTERNAL_VERSION,
body: JSON.stringify({ unencrypted: false, refreshCache: false }),
});
});
@@ -64,7 +71,8 @@ describe('TelemetryService', () => {
const optedIn = true;
await telemetryService.setOptIn(optedIn);
- expect(telemetryService['http'].post).toBeCalledWith('/api/telemetry/v2/optIn', {
+ expect(telemetryService['http'].post).toBeCalledWith(OptInRoute, {
+ ...INTERNAL_VERSION,
body: JSON.stringify({ enabled: optedIn }),
});
});
@@ -77,7 +85,8 @@ describe('TelemetryService', () => {
const optedIn = false;
await telemetryService.setOptIn(optedIn);
- expect(telemetryService['http'].post).toBeCalledWith('/api/telemetry/v2/optIn', {
+ expect(telemetryService['http'].post).toBeCalledWith(OptInRoute, {
+ ...INTERNAL_VERSION,
body: JSON.stringify({ enabled: optedIn }),
});
});
@@ -110,7 +119,7 @@ describe('TelemetryService', () => {
config: { allowChangingOptInStatus: true },
});
telemetryService['http'].post = jest.fn().mockImplementation((url: string) => {
- if (url === '/api/telemetry/v2/optIn') {
+ if (url === OptInRoute) {
throw Error('failed to update opt in.');
}
});
@@ -203,7 +212,7 @@ describe('TelemetryService', () => {
});
telemetryService['http'].put = jest.fn().mockImplementation((url: string) => {
- if (url === '/api/telemetry/v2/userHasSeenNotice') {
+ if (url === UserHasSeenNoticeRoute) {
throw Error('failed to update opt in.');
}
});
diff --git a/src/plugins/telemetry/public/services/telemetry_service.ts b/src/plugins/telemetry/public/services/telemetry_service.ts
index 0629630fea48a..ec67a4e675e26 100644
--- a/src/plugins/telemetry/public/services/telemetry_service.ts
+++ b/src/plugins/telemetry/public/services/telemetry_service.ts
@@ -8,11 +8,19 @@
import { i18n } from '@kbn/i18n';
import type { CoreSetup, CoreStart } from '@kbn/core/public';
+import {
+ LastReportedRoute,
+ INTERNAL_VERSION,
+ OptInRoute,
+ FetchSnapshotTelemetry,
+ UserHasSeenNoticeRoute,
+} from '../../common/routes';
import type { TelemetryPluginConfig } from '../plugin';
-import { getTelemetryChannelEndpoint } from '../../common/telemetry_config/get_telemetry_channel_endpoint';
+import { getTelemetryChannelEndpoint } from '../../common/telemetry_config';
import type {
UnencryptedTelemetryPayload,
EncryptedTelemetryPayload,
+ FetchLastReportedResponse,
} from '../../common/types/latest';
import { PAYLOAD_CONTENT_ENCODING } from '../../common/constants';
@@ -93,8 +101,7 @@ export class TelemetryService {
/** Is the cluster allowed to change the opt-in/out status **/
public getCanChangeOptInStatus = () => {
- const allowChangingOptInStatus = this.config.allowChangingOptInStatus;
- return allowChangingOptInStatus;
+ return this.config.allowChangingOptInStatus;
};
/** Retrieve the opt-in/out notification URL **/
@@ -156,17 +163,18 @@ export class TelemetryService {
};
public fetchLastReported = async (): Promise => {
- const response = await this.http.get<{ lastReported?: number }>(
- '/api/telemetry/v2/last_reported'
+ const response = await this.http.get(
+ LastReportedRoute,
+ INTERNAL_VERSION
);
return response?.lastReported;
};
public updateLastReported = async (): Promise => {
- return this.http.put('/api/telemetry/v2/last_reported');
+ return this.http.put(LastReportedRoute);
};
- /** Fetches an unencrypted telemetry payload so we can show it to the user **/
+ /** Fetches an unencrypted telemetry payload, so we can show it to the user **/
public fetchExample = async (): Promise => {
return await this.fetchTelemetry({ unencrypted: true, refreshCache: true });
};
@@ -174,12 +182,14 @@ export class TelemetryService {
/**
* Fetches telemetry payload
* @param unencrypted Default `false`. Whether the returned payload should be encrypted or not.
+ * @param refreshCache Default `false`. Set to `true` to force the regeneration of the telemetry report.
*/
public fetchTelemetry = async ({
unencrypted = false,
refreshCache = false,
} = {}): Promise => {
- return this.http.post('/api/telemetry/v2/clusters/_stats', {
+ return this.http.post(FetchSnapshotTelemetry, {
+ ...INTERNAL_VERSION,
body: JSON.stringify({ unencrypted, refreshCache }),
});
};
@@ -198,12 +208,10 @@ export class TelemetryService {
try {
// Report the option to the Kibana server to store the settings.
// It returns the encrypted update to send to the telemetry cluster [{cluster_uuid, opt_in_status}]
- const optInStatusPayload = await this.http.post(
- '/api/telemetry/v2/optIn',
- {
- body: JSON.stringify({ enabled: optedIn }),
- }
- );
+ const optInStatusPayload = await this.http.post(OptInRoute, {
+ ...INTERNAL_VERSION,
+ body: JSON.stringify({ enabled: optedIn }),
+ });
if (this.reportOptInStatusChange) {
// Use the response to report about the change to the remote telemetry cluster.
// If it's opt-out, this will be the last communication to the remote service.
@@ -231,7 +239,7 @@ export class TelemetryService {
*/
public setUserHasSeenNotice = async (): Promise => {
try {
- await this.http.put('/api/telemetry/v2/userHasSeenNotice');
+ await this.http.put(UserHasSeenNoticeRoute, INTERNAL_VERSION);
this.userHasSeenOptedInNotice = true;
} catch (error) {
this.notifications.toasts.addError(error, {
@@ -248,7 +256,7 @@ export class TelemetryService {
/**
* Pushes the encrypted payload [{cluster_uuid, opt_in_status}] to the remote telemetry service
- * @param optInPayload [{cluster_uuid, opt_in_status}] encrypted by the server into an array of strings
+ * @param optInStatusPayload [{cluster_uuid, opt_in_status}] encrypted by the server into an array of strings
*/
private reportOptInStatus = async (
optInStatusPayload: EncryptedTelemetryPayload
diff --git a/src/plugins/telemetry/server/routes/telemetry_config.ts b/src/plugins/telemetry/server/routes/telemetry_config.ts
index 60a34d80aad2e..37daef537b568 100644
--- a/src/plugins/telemetry/server/routes/telemetry_config.ts
+++ b/src/plugins/telemetry/server/routes/telemetry_config.ts
@@ -8,9 +8,14 @@
import { type Observable, firstValueFrom } from 'rxjs';
import type { IRouter, SavedObjectsClient } from '@kbn/core/server';
+import { schema } from '@kbn/config-schema';
+import { RequestHandler } from '@kbn/core-http-server';
import type { TelemetryConfigType } from '../config';
import { v2 } from '../../common/types';
-import { FetchTelemetryConfigRoute } from '../../common/routes';
+import {
+ FetchTelemetryConfigRoutePathBasedV2,
+ FetchTelemetryConfigRoute,
+} from '../../common/routes';
import { getTelemetrySavedObject } from '../saved_objects';
import {
getNotifyUserAboutOptInDefault,
@@ -25,54 +30,74 @@ interface RegisterTelemetryConfigRouteOptions {
currentKibanaVersion: string;
savedObjectsInternalClient$: Observable;
}
+
export function registerTelemetryConfigRoutes({
router,
config$,
currentKibanaVersion,
savedObjectsInternalClient$,
}: RegisterTelemetryConfigRouteOptions) {
- // GET to retrieve
- router.get(
- {
- path: FetchTelemetryConfigRoute,
- validate: false,
- },
- async (context, req, res) => {
- const config = await firstValueFrom(config$);
- const savedObjectsInternalClient = await firstValueFrom(savedObjectsInternalClient$);
- const telemetrySavedObject = await getTelemetrySavedObject(savedObjectsInternalClient);
- const allowChangingOptInStatus = getTelemetryAllowChangingOptInStatus({
- configTelemetryAllowChangingOptInStatus: config.allowChangingOptInStatus,
- telemetrySavedObject,
- });
+ const v2Handler: RequestHandler = async (context, req, res) => {
+ const config = await firstValueFrom(config$);
+ const savedObjectsInternalClient = await firstValueFrom(savedObjectsInternalClient$);
+ const telemetrySavedObject = await getTelemetrySavedObject(savedObjectsInternalClient);
+ const allowChangingOptInStatus = getTelemetryAllowChangingOptInStatus({
+ configTelemetryAllowChangingOptInStatus: config.allowChangingOptInStatus,
+ telemetrySavedObject,
+ });
+
+ const optIn = getTelemetryOptIn({
+ configTelemetryOptIn: config.optIn,
+ allowChangingOptInStatus,
+ telemetrySavedObject,
+ currentKibanaVersion,
+ });
- const optIn = getTelemetryOptIn({
- configTelemetryOptIn: config.optIn,
- allowChangingOptInStatus,
- telemetrySavedObject,
- currentKibanaVersion,
- });
+ const sendUsageFrom = getTelemetrySendUsageFrom({
+ configTelemetrySendUsageFrom: config.sendUsageFrom,
+ telemetrySavedObject,
+ });
- const sendUsageFrom = getTelemetrySendUsageFrom({
- configTelemetrySendUsageFrom: config.sendUsageFrom,
- telemetrySavedObject,
- });
+ const telemetryNotifyUserAboutOptInDefault = getNotifyUserAboutOptInDefault({
+ telemetrySavedObject,
+ allowChangingOptInStatus,
+ configTelemetryOptIn: config.optIn,
+ telemetryOptedIn: optIn,
+ });
- const telemetryNotifyUserAboutOptInDefault = getNotifyUserAboutOptInDefault({
- telemetrySavedObject,
- allowChangingOptInStatus,
- configTelemetryOptIn: config.optIn,
- telemetryOptedIn: optIn,
- });
+ const body: v2.FetchTelemetryConfigResponse = {
+ allowChangingOptInStatus,
+ optIn,
+ sendUsageFrom,
+ telemetryNotifyUserAboutOptInDefault,
+ };
+
+ return res.ok({ body });
+ };
+
+ const v2Validations = {
+ response: {
+ 200: {
+ body: schema.object({
+ allowChangingOptInStatus: schema.boolean(),
+ optIn: schema.oneOf([schema.boolean(), schema.literal(null)]),
+ sendUsageFrom: schema.oneOf([schema.literal('server'), schema.literal('browser')]),
+ telemetryNotifyUserAboutOptInDefault: schema.boolean(),
+ }),
+ },
+ },
+ };
- const body: v2.FetchTelemetryConfigResponse = {
- allowChangingOptInStatus,
- optIn,
- sendUsageFrom,
- telemetryNotifyUserAboutOptInDefault,
- };
+ // Register the internal versioned API
+ router.versioned
+ .get({ access: 'internal', path: FetchTelemetryConfigRoute })
+ // Just because it used to be /v2/, we are creating identical v1 and v2.
+ .addVersion({ version: '1', validate: v2Validations }, v2Handler)
+ .addVersion({ version: '2', validate: v2Validations }, v2Handler);
- return res.ok({ body });
- }
- );
+ // Register the deprecated public and path-based for BWC
+ // as we know this one is used by other Elastic products to fetch the opt-in status.
+ router.versioned
+ .get({ access: 'public', path: FetchTelemetryConfigRoutePathBasedV2 })
+ .addVersion({ version: '2023-10-31', validate: v2Validations }, v2Handler);
}
diff --git a/src/plugins/telemetry/server/routes/telemetry_last_reported.ts b/src/plugins/telemetry/server/routes/telemetry_last_reported.ts
index 2e21785b9296d..23c15521d870d 100644
--- a/src/plugins/telemetry/server/routes/telemetry_last_reported.ts
+++ b/src/plugins/telemetry/server/routes/telemetry_last_reported.ts
@@ -6,9 +6,12 @@
* Side Public License, v 1.
*/
+import { schema } from '@kbn/config-schema';
import type { IRouter, SavedObjectsClient } from '@kbn/core/server';
import type { Observable } from 'rxjs';
import { firstValueFrom } from 'rxjs';
+import { RequestHandler } from '@kbn/core-http-server';
+import { LastReportedRoute } from '../../common/routes';
import { v2 } from '../../common/types';
import { getTelemetrySavedObject, updateTelemetrySavedObject } from '../saved_objects';
@@ -17,38 +20,38 @@ export function registerTelemetryLastReported(
savedObjectsInternalClient$: Observable
) {
// GET to retrieve
- router.get(
- {
- path: '/api/telemetry/v2/last_reported',
- validate: false,
- },
- async (context, req, res) => {
- const savedObjectsInternalClient = await firstValueFrom(savedObjectsInternalClient$);
- const telemetrySavedObject = await getTelemetrySavedObject(savedObjectsInternalClient);
+ const v2GetValidations = {
+ response: { 200: { body: schema.object({ lastReported: schema.maybe(schema.number()) }) } },
+ };
- const body: v2.FetchLastReportedResponse = {
- lastReported: telemetrySavedObject && telemetrySavedObject?.lastReported,
- };
+ const v2GetHandler: RequestHandler = async (context, req, res) => {
+ const savedObjectsInternalClient = await firstValueFrom(savedObjectsInternalClient$);
+ const telemetrySavedObject = await getTelemetrySavedObject(savedObjectsInternalClient);
- return res.ok({
- body,
- });
- }
- );
+ const body: v2.FetchLastReportedResponse = {
+ lastReported: telemetrySavedObject && telemetrySavedObject?.lastReported,
+ };
+ return res.ok({ body });
+ };
+
+ router.versioned
+ .get({ access: 'internal', path: LastReportedRoute })
+ // Just because it used to be /v2/, we are creating identical v1 and v2.
+ .addVersion({ version: '1', validate: v2GetValidations }, v2GetHandler)
+ .addVersion({ version: '2', validate: v2GetValidations }, v2GetHandler);
// PUT to update
- router.put(
- {
- path: '/api/telemetry/v2/last_reported',
- validate: false,
- },
- async (context, req, res) => {
- const savedObjectsInternalClient = await firstValueFrom(savedObjectsInternalClient$);
- await updateTelemetrySavedObject(savedObjectsInternalClient, {
- lastReported: Date.now(),
- });
+ const v2PutHandler: RequestHandler = async (context, req, res) => {
+ const savedObjectsInternalClient = await firstValueFrom(savedObjectsInternalClient$);
+ await updateTelemetrySavedObject(savedObjectsInternalClient, {
+ lastReported: Date.now(),
+ });
+ return res.ok();
+ };
- return res.ok();
- }
- );
+ router.versioned
+ .put({ access: 'internal', path: LastReportedRoute })
+ // Just because it used to be /v2/, we are creating identical v1 and v2.
+ .addVersion({ version: '1', validate: false }, v2PutHandler)
+ .addVersion({ version: '2', validate: false }, v2PutHandler);
}
diff --git a/src/plugins/telemetry/server/routes/telemetry_opt_in.ts b/src/plugins/telemetry/server/routes/telemetry_opt_in.ts
index f523031e181ed..689e0cd06fad8 100644
--- a/src/plugins/telemetry/server/routes/telemetry_opt_in.ts
+++ b/src/plugins/telemetry/server/routes/telemetry_opt_in.ts
@@ -9,12 +9,14 @@
import { firstValueFrom, type Observable } from 'rxjs';
import { schema } from '@kbn/config-schema';
import type { IRouter, Logger } from '@kbn/core/server';
-import { SavedObjectsErrorHelpers } from '@kbn/core/server';
+import { RequestHandlerContext, SavedObjectsErrorHelpers } from '@kbn/core/server';
import type {
StatsGetterConfig,
TelemetryCollectionManagerPluginSetup,
} from '@kbn/telemetry-collection-manager-plugin/server';
-import { v2 } from '../../common/types';
+import { RequestHandler } from '@kbn/core-http-server';
+import { OptInRoute } from '../../common/routes';
+import { OptInBody, v2 } from '../../common/types';
import { sendTelemetryOptInStatus } from './telemetry_opt_in_stats';
import {
getTelemetrySavedObject,
@@ -41,78 +43,91 @@ export function registerTelemetryOptInRoutes({
currentKibanaVersion,
telemetryCollectionManager,
}: RegisterOptInRoutesParams) {
- router.post(
- {
- path: '/api/telemetry/v2/optIn',
- validate: {
- body: schema.object({ enabled: schema.boolean() }),
- },
- },
- async (context, req, res) => {
- const newOptInStatus = req.body.enabled;
- const soClient = (await context.core).savedObjects.getClient({
- includedHiddenTypes: [TELEMETRY_SAVED_OBJECT_TYPE],
- });
- const attributes: TelemetrySavedObject = {
- enabled: newOptInStatus,
- lastVersionChecked: currentKibanaVersion,
- };
- const config = await firstValueFrom(config$);
+ const v2Handler: RequestHandler = async (
+ context,
+ req,
+ res
+ ) => {
+ const newOptInStatus = req.body.enabled;
+ const soClient = (await context.core).savedObjects.getClient({
+ includedHiddenTypes: [TELEMETRY_SAVED_OBJECT_TYPE],
+ });
+ const attributes: TelemetrySavedObject = {
+ enabled: newOptInStatus,
+ lastVersionChecked: currentKibanaVersion,
+ };
+ const config = await firstValueFrom(config$);
- let telemetrySavedObject: TelemetrySavedObject | undefined;
- try {
- telemetrySavedObject = await getTelemetrySavedObject(soClient);
- } catch (err) {
- if (SavedObjectsErrorHelpers.isForbiddenError(err)) {
- // If we couldn't get the saved object due to lack of permissions,
- // we can assume the user won't be able to update it either
- return res.forbidden();
- }
+ let telemetrySavedObject: TelemetrySavedObject | undefined;
+ try {
+ telemetrySavedObject = await getTelemetrySavedObject(soClient);
+ } catch (err) {
+ if (SavedObjectsErrorHelpers.isForbiddenError(err)) {
+ // If we couldn't get the saved object due to lack of permissions,
+ // we can assume the user won't be able to update it either
+ return res.forbidden();
}
+ }
- const allowChangingOptInStatus = getTelemetryAllowChangingOptInStatus({
- configTelemetryAllowChangingOptInStatus: config.allowChangingOptInStatus,
- telemetrySavedObject,
+ const allowChangingOptInStatus = getTelemetryAllowChangingOptInStatus({
+ configTelemetryAllowChangingOptInStatus: config.allowChangingOptInStatus,
+ telemetrySavedObject,
+ });
+ if (!allowChangingOptInStatus) {
+ return res.badRequest({
+ body: JSON.stringify({ error: 'Not allowed to change Opt-in Status.' }),
});
- if (!allowChangingOptInStatus) {
- return res.badRequest({
- body: JSON.stringify({ error: 'Not allowed to change Opt-in Status.' }),
- });
- }
+ }
- const statsGetterConfig: StatsGetterConfig = {
- unencrypted: false,
- };
+ const statsGetterConfig: StatsGetterConfig = {
+ unencrypted: false,
+ };
- const optInStatus = await telemetryCollectionManager.getOptInStats(
- newOptInStatus,
+ const optInStatus = await telemetryCollectionManager.getOptInStats(
+ newOptInStatus,
+ statsGetterConfig
+ );
+
+ if (config.sendUsageFrom === 'server') {
+ const { appendServerlessChannelsSuffix, sendUsageTo } = config;
+ sendTelemetryOptInStatus(
+ telemetryCollectionManager,
+ { appendServerlessChannelsSuffix, sendUsageTo, newOptInStatus, currentKibanaVersion },
statsGetterConfig
- );
+ ).catch((err) => {
+ // The server is likely behind a firewall and can't reach the remote service
+ logger.warn(
+ `Failed to notify the telemetry endpoint about the opt-in selection. Possibly blocked by a firewall? - Error: ${err.message}`
+ );
+ });
+ }
- if (config.sendUsageFrom === 'server') {
- const { appendServerlessChannelsSuffix, sendUsageTo } = config;
- sendTelemetryOptInStatus(
- telemetryCollectionManager,
- { appendServerlessChannelsSuffix, sendUsageTo, newOptInStatus, currentKibanaVersion },
- statsGetterConfig
- ).catch((err) => {
- // The server is likely behind a firewall and can't reach the remote service
- logger.warn(
- `Failed to notify the telemetry endpoint about the opt-in selection. Possibly blocked by a firewall? - Error: ${err.message}`
- );
- });
+ try {
+ await updateTelemetrySavedObject(soClient, attributes);
+ } catch (e) {
+ if (SavedObjectsErrorHelpers.isForbiddenError(e)) {
+ return res.forbidden();
}
+ }
- try {
- await updateTelemetrySavedObject(soClient, attributes);
- } catch (e) {
- if (SavedObjectsErrorHelpers.isForbiddenError(e)) {
- return res.forbidden();
- }
- }
+ const body: v2.OptInResponse = optInStatus;
+ return res.ok({ body });
+ };
- const body: v2.OptInResponse = optInStatus;
- return res.ok({ body });
- }
- );
+ const v2Validations = {
+ request: { body: schema.object({ enabled: schema.boolean() }) },
+ response: {
+ 200: {
+ body: schema.arrayOf(
+ schema.object({ clusterUuid: schema.string(), stats: schema.string() })
+ ),
+ },
+ },
+ };
+
+ router.versioned
+ .post({ access: 'internal', path: OptInRoute })
+ // Just because it used to be /v2/, we are creating identical v1 and v2.
+ .addVersion({ version: '1', validate: v2Validations }, v2Handler)
+ .addVersion({ version: '2', validate: v2Validations }, v2Handler);
}
diff --git a/src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts b/src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts
index f715c84fc9341..378dbdcc9e495 100644
--- a/src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts
+++ b/src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts
@@ -14,6 +14,7 @@ import type {
TelemetryCollectionManagerPluginSetup,
StatsGetterConfig,
} from '@kbn/telemetry-collection-manager-plugin/server';
+import { GetOptInStatsRoutePathBasedV2 } from '../../common/routes';
import type { v2 } from '../../common/types';
import { EncryptedTelemetryPayload, UnencryptedTelemetryPayload } from '../../common/types';
import { getTelemetryChannelEndpoint } from '../../common/telemetry_config';
@@ -62,43 +63,64 @@ export function registerTelemetryOptInStatsRoutes(
router: IRouter,
telemetryCollectionManager: TelemetryCollectionManagerPluginSetup
) {
- router.post(
- {
- path: '/api/telemetry/v2/clusters/_opt_in_stats',
- validate: {
- body: schema.object({
- enabled: schema.boolean(),
- unencrypted: schema.boolean({ defaultValue: true }),
- }),
+ router.versioned
+ .post({
+ access: 'public', // It's not used across Kibana, and I didn't want to remove it in this PR just in case.
+ path: GetOptInStatsRoutePathBasedV2,
+ })
+ .addVersion(
+ {
+ version: '2023-10-31',
+ validate: {
+ request: {
+ body: schema.object({
+ enabled: schema.boolean(),
+ unencrypted: schema.boolean({ defaultValue: true }),
+ }),
+ },
+ response: {
+ 200: {
+ body: schema.arrayOf(
+ schema.object({
+ clusterUuid: schema.string(),
+ stats: schema.object({
+ cluster_uuid: schema.string(),
+ opt_in_status: schema.boolean(),
+ }),
+ })
+ ),
+ },
+ 503: { body: schema.string() },
+ },
+ },
},
- },
- async (context, req, res) => {
- try {
- const newOptInStatus = req.body.enabled;
- const unencrypted = req.body.unencrypted;
+ async (context, req, res) => {
+ try {
+ const newOptInStatus = req.body.enabled;
+ const unencrypted = req.body.unencrypted;
- if (!(await telemetryCollectionManager.shouldGetTelemetry())) {
- // We probably won't reach here because there is a license check in the auth phase of the HTTP requests.
- // But let's keep it here should that changes at any point.
- return res.customError({
- statusCode: 503,
- body: `Can't fetch telemetry at the moment because some services are down. Check the /status page for more details.`,
- });
- }
+ if (!(await telemetryCollectionManager.shouldGetTelemetry())) {
+ // We probably won't reach here because there is a license check in the auth phase of the HTTP requests.
+ // But let's keep it here should that changes at any point.
+ return res.customError({
+ statusCode: 503,
+ body: `Can't fetch telemetry at the moment because some services are down. Check the /status page for more details.`,
+ });
+ }
- const statsGetterConfig: StatsGetterConfig = {
- unencrypted,
- };
+ const statsGetterConfig: StatsGetterConfig = {
+ unencrypted,
+ };
- const optInStatus = await telemetryCollectionManager.getOptInStats(
- newOptInStatus,
- statsGetterConfig
- );
- const body: v2.OptInStatsResponse = optInStatus;
- return res.ok({ body });
- } catch (err) {
- return res.ok({ body: [] });
+ const optInStatus = await telemetryCollectionManager.getOptInStats(
+ newOptInStatus,
+ statsGetterConfig
+ );
+ const body: v2.OptInStatsResponse = optInStatus;
+ return res.ok({ body });
+ } catch (err) {
+ return res.ok({ body: [] });
+ }
}
- }
- );
+ );
}
diff --git a/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts b/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts
index 152aaa4c9eed9..4cbb1381c0566 100644
--- a/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts
+++ b/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts
@@ -16,8 +16,9 @@ async function runRequest(
mockRouter: IRouter,
body?: { unencrypted?: boolean; refreshCache?: boolean }
) {
- expect(mockRouter.post).toBeCalled();
- const [, handler] = (mockRouter.post as jest.Mock).mock.calls[0];
+ expect(mockRouter.versioned.post).toBeCalled();
+ const [, handler] = (mockRouter.versioned.post as jest.Mock).mock.results[0].value.addVersion.mock
+ .calls[0];
const mockResponse = httpServerMock.createResponseFactory();
const mockRequest = httpServerMock.createKibanaRequest({ body });
await handler(null, mockRequest, mockResponse);
@@ -49,10 +50,10 @@ describe('registerTelemetryUsageStatsRoutes', () => {
describe('clusters/_stats POST route', () => {
it('registers _stats POST route and accepts body configs', () => {
registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true, getSecurity);
- expect(mockRouter.post).toBeCalledTimes(1);
- const [routeConfig, handler] = (mockRouter.post as jest.Mock).mock.calls[0];
- expect(routeConfig.path).toMatchInlineSnapshot(`"/api/telemetry/v2/clusters/_stats"`);
- expect(Object.keys(routeConfig.validate.body.props)).toEqual(['unencrypted', 'refreshCache']);
+ expect(mockRouter.versioned.post).toBeCalledTimes(1);
+ const [routeConfig, handler] = (mockRouter.versioned.post as jest.Mock).mock.results[0].value
+ .addVersion.mock.calls[0];
+ expect(routeConfig.version).toMatchInlineSnapshot(`"1"`);
expect(handler).toBeInstanceOf(Function);
});
diff --git a/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts b/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts
index 600c6d3ef2c70..a828de911c29a 100644
--- a/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts
+++ b/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts
@@ -13,7 +13,9 @@ import type {
StatsGetterConfig,
} from '@kbn/telemetry-collection-manager-plugin/server';
import type { SecurityPluginStart } from '@kbn/security-plugin/server';
-import { v2 } from '../../common/types';
+import { RequestHandler } from '@kbn/core-http-server';
+import { FetchSnapshotTelemetry } from '../../common/routes';
+import { UsageStatsBody, v2 } from '../../common/types';
export type SecurityGetter = () => SecurityPluginStart | undefined;
@@ -23,64 +25,75 @@ export function registerTelemetryUsageStatsRoutes(
isDev: boolean,
getSecurity: SecurityGetter
) {
- router.post(
- {
- path: '/api/telemetry/v2/clusters/_stats',
- validate: {
- body: schema.object({
- unencrypted: schema.boolean({ defaultValue: false }),
- refreshCache: schema.boolean({ defaultValue: false }),
- }),
- },
- },
- async (context, req, res) => {
- const { unencrypted, refreshCache } = req.body;
+ const v2Handler: RequestHandler = async (
+ context,
+ req,
+ res
+ ) => {
+ const { unencrypted, refreshCache } = req.body;
- if (!(await telemetryCollectionManager.shouldGetTelemetry())) {
- // We probably won't reach here because there is a license check in the auth phase of the HTTP requests.
- // But let's keep it here should that changes at any point.
- return res.customError({
- statusCode: 503,
- body: `Can't fetch telemetry at the moment because some services are down. Check the /status page for more details.`,
- });
- }
+ if (!(await telemetryCollectionManager.shouldGetTelemetry())) {
+ // We probably won't reach here because there is a license check in the auth phase of the HTTP requests.
+ // But let's keep it here should that changes at any point.
+ return res.customError({
+ statusCode: 503,
+ body: `Can't fetch telemetry at the moment because some services are down. Check the /status page for more details.`,
+ });
+ }
- const security = getSecurity();
- // We need to check useRbacForRequest to figure out if ES has security enabled before making the privileges check
- if (security && unencrypted && security.authz.mode.useRbacForRequest(req)) {
- // Normally we would use `options: { tags: ['access:decryptedTelemetry'] }` in the route definition to check authorization for an
- // API action, however, we want to check this conditionally based on the `unencrypted` parameter. In this case we need to use the
- // security API directly to check privileges for this action. Note that the 'decryptedTelemetry' API privilege string is only
- // granted to users that have "Global All" or "Global Read" privileges in Kibana.
- const { checkPrivilegesWithRequest, actions } = security.authz;
- const privileges = { kibana: actions.api.get('decryptedTelemetry') };
- const { hasAllRequested } = await checkPrivilegesWithRequest(req).globally(privileges);
- if (!hasAllRequested) {
- return res.forbidden();
- }
+ const security = getSecurity();
+ // We need to check useRbacForRequest to figure out if ES has security enabled before making the privileges check
+ if (security && unencrypted && security.authz.mode.useRbacForRequest(req)) {
+ // Normally we would use `options: { tags: ['access:decryptedTelemetry'] }` in the route definition to check authorization for an
+ // API action, however, we want to check this conditionally based on the `unencrypted` parameter. In this case we need to use the
+ // security API directly to check privileges for this action. Note that the 'decryptedTelemetry' API privilege string is only
+ // granted to users that have "Global All" or "Global Read" privileges in Kibana.
+ const { checkPrivilegesWithRequest, actions } = security.authz;
+ const privileges = { kibana: actions.api.get('decryptedTelemetry') };
+ const { hasAllRequested } = await checkPrivilegesWithRequest(req).globally(privileges);
+ if (!hasAllRequested) {
+ return res.forbidden();
}
+ }
- try {
- const statsConfig: StatsGetterConfig = {
- unencrypted,
- refreshCache: unencrypted || refreshCache,
- };
+ try {
+ const statsConfig: StatsGetterConfig = {
+ unencrypted,
+ refreshCache: unencrypted || refreshCache,
+ };
- const body: v2.UnencryptedTelemetryPayload = await telemetryCollectionManager.getStats(
- statsConfig
- );
- return res.ok({ body });
- } catch (err) {
- if (isDev) {
- // don't ignore errors when running in dev mode
- throw err;
- }
- if (unencrypted && err.status === 403) {
- return res.forbidden();
- }
- // ignore errors and return empty set
- return res.ok({ body: [] });
+ const body: v2.UnencryptedTelemetryPayload = await telemetryCollectionManager.getStats(
+ statsConfig
+ );
+ return res.ok({ body });
+ } catch (err) {
+ if (isDev) {
+ // don't ignore errors when running in dev mode
+ throw err;
}
+ if (unencrypted && err.status === 403) {
+ return res.forbidden();
+ }
+ // ignore errors and return empty set
+ return res.ok({ body: [] });
}
- );
+ };
+
+ const v2Validations = {
+ request: {
+ body: schema.object({
+ unencrypted: schema.boolean({ defaultValue: false }),
+ refreshCache: schema.boolean({ defaultValue: false }),
+ }),
+ },
+ };
+
+ router.versioned
+ .post({
+ access: 'internal',
+ path: FetchSnapshotTelemetry,
+ })
+ // Just because it used to be /v2/, we are creating identical v1 and v2.
+ .addVersion({ version: '1', validate: v2Validations }, v2Handler)
+ .addVersion({ version: '2', validate: v2Validations }, v2Handler);
}
diff --git a/src/plugins/telemetry/server/routes/telemetry_user_has_seen_notice.ts b/src/plugins/telemetry/server/routes/telemetry_user_has_seen_notice.ts
index d9cb0b981b0a9..b59ada443054d 100644
--- a/src/plugins/telemetry/server/routes/telemetry_user_has_seen_notice.ts
+++ b/src/plugins/telemetry/server/routes/telemetry_user_has_seen_notice.ts
@@ -7,6 +7,9 @@
*/
import type { IRouter } from '@kbn/core/server';
+import { RequestHandler } from '@kbn/core-http-server';
+import { RequestHandlerContext } from '@kbn/core/server';
+import { UserHasSeenNoticeRoute } from '../../common/routes';
import { TELEMETRY_SAVED_OBJECT_TYPE } from '../saved_objects';
import { v2 } from '../../common/types';
import {
@@ -16,38 +19,42 @@ import {
} from '../saved_objects';
export function registerTelemetryUserHasSeenNotice(router: IRouter, currentKibanaVersion: string) {
- router.put(
- {
- path: '/api/telemetry/v2/userHasSeenNotice',
- validate: false,
- },
- async (context, req, res) => {
- const soClient = (await context.core).savedObjects.getClient({
- includedHiddenTypes: [TELEMETRY_SAVED_OBJECT_TYPE],
- });
- const telemetrySavedObject = await getTelemetrySavedObject(soClient);
+ const v2Handler: RequestHandler = async (
+ context,
+ req,
+ res
+ ) => {
+ const soClient = (await context.core).savedObjects.getClient({
+ includedHiddenTypes: [TELEMETRY_SAVED_OBJECT_TYPE],
+ });
+ const telemetrySavedObject = await getTelemetrySavedObject(soClient);
- // update the object with a flag stating that the opt-in notice has been seen
- const updatedAttributes: TelemetrySavedObjectAttributes = {
- ...telemetrySavedObject,
- userHasSeenNotice: true,
- // We need to store that the user was notified in this version.
- // Otherwise, it'll continuously show the banner if previously opted-out.
- lastVersionChecked: currentKibanaVersion,
- };
- await updateTelemetrySavedObject(soClient, updatedAttributes);
+ // update the object with a flag stating that the opt-in notice has been seen
+ const updatedAttributes: TelemetrySavedObjectAttributes = {
+ ...telemetrySavedObject,
+ userHasSeenNotice: true,
+ // We need to store that the user was notified in this version.
+ // Otherwise, it'll continuously show the banner if previously opted-out.
+ lastVersionChecked: currentKibanaVersion,
+ };
+ await updateTelemetrySavedObject(soClient, updatedAttributes);
- const body: v2.Telemetry = {
- allowChangingOptInStatus: updatedAttributes.allowChangingOptInStatus,
- enabled: updatedAttributes.enabled,
- lastReported: updatedAttributes.lastReported,
- lastVersionChecked: updatedAttributes.lastVersionChecked,
- reportFailureCount: updatedAttributes.reportFailureCount,
- reportFailureVersion: updatedAttributes.reportFailureVersion,
- sendUsageFrom: updatedAttributes.sendUsageFrom,
- userHasSeenNotice: updatedAttributes.userHasSeenNotice,
- };
- return res.ok({ body });
- }
- );
+ const body: v2.Telemetry = {
+ allowChangingOptInStatus: updatedAttributes.allowChangingOptInStatus,
+ enabled: updatedAttributes.enabled,
+ lastReported: updatedAttributes.lastReported,
+ lastVersionChecked: updatedAttributes.lastVersionChecked,
+ reportFailureCount: updatedAttributes.reportFailureCount,
+ reportFailureVersion: updatedAttributes.reportFailureVersion,
+ sendUsageFrom: updatedAttributes.sendUsageFrom,
+ userHasSeenNotice: updatedAttributes.userHasSeenNotice,
+ };
+ return res.ok({ body });
+ };
+
+ router.versioned
+ .put({ access: 'internal', path: UserHasSeenNoticeRoute })
+ // Just because it used to be /v2/, we are creating identical v1 and v2.
+ .addVersion({ version: '1', validate: false }, v2Handler)
+ .addVersion({ version: '2', validate: false }, v2Handler);
}
diff --git a/src/plugins/telemetry/tsconfig.json b/src/plugins/telemetry/tsconfig.json
index 7da7e89bae02f..638bfb4f722a7 100644
--- a/src/plugins/telemetry/tsconfig.json
+++ b/src/plugins/telemetry/tsconfig.json
@@ -34,6 +34,7 @@
"@kbn/std",
"@kbn/core-http-browser-mocks",
"@kbn/core-http-browser",
+ "@kbn/core-http-server",
],
"exclude": [
"target/**/*",
diff --git a/test/api_integration/apis/data_views/swap_references/main.ts b/test/api_integration/apis/data_views/swap_references/main.ts
index 93247f090a9da..404d9e58ab477 100644
--- a/test/api_integration/apis/data_views/swap_references/main.ts
+++ b/test/api_integration/apis/data_views/swap_references/main.ts
@@ -20,6 +20,7 @@ export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const title = 'logs-*';
const prevDataViewId = '91200a00-9efd-11e7-acb3-3dab96693fab';
+ const PREVIEW_PATH = `${DATA_VIEW_SWAP_REFERENCES_PATH}/_preview`;
let dataViewId = '';
describe('main', () => {
@@ -49,23 +50,23 @@ export default function ({ getService }: FtrProviderContext) {
it('can preview', async () => {
const res = await supertest
- .post(DATA_VIEW_SWAP_REFERENCES_PATH)
+ .post(PREVIEW_PATH)
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
.send({
- from_id: prevDataViewId,
- to_id: dataViewId,
+ fromId: prevDataViewId,
+ toId: dataViewId,
});
expect(res).to.have.property('status', 200);
});
it('can preview specifying type', async () => {
const res = await supertest
- .post(DATA_VIEW_SWAP_REFERENCES_PATH)
+ .post(PREVIEW_PATH)
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
.send({
- from_id: prevDataViewId,
- from_type: 'index-pattern',
- to_id: dataViewId,
+ fromId: prevDataViewId,
+ fromType: 'index-pattern',
+ toId: dataViewId,
});
expect(res).to.have.property('status', 200);
});
@@ -75,13 +76,11 @@ export default function ({ getService }: FtrProviderContext) {
.post(DATA_VIEW_SWAP_REFERENCES_PATH)
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
.send({
- from_id: prevDataViewId,
- to_id: dataViewId,
- preview: false,
+ fromId: prevDataViewId,
+ toId: dataViewId,
});
expect(res).to.have.property('status', 200);
expect(res.body.result.length).to.equal(1);
- expect(res.body.preview).to.equal(false);
expect(res.body.result[0].id).to.equal('dd7caf20-9efd-11e7-acb3-3dab96693fab');
expect(res.body.result[0].type).to.equal('visualization');
});
@@ -91,13 +90,14 @@ export default function ({ getService }: FtrProviderContext) {
.post(DATA_VIEW_SWAP_REFERENCES_PATH)
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
.send({
- from_id: prevDataViewId,
- to_id: dataViewId,
- preview: false,
+ fromId: prevDataViewId,
+ toId: dataViewId,
delete: true,
});
expect(res).to.have.property('status', 200);
expect(res.body.result.length).to.equal(1);
+ expect(res.body.deleteStatus.remainingRefs).to.equal(0);
+ expect(res.body.deleteStatus.deletePerformed).to.equal(true);
const res2 = await supertest
.get(SPECIFIC_DATA_VIEW_PATH.replace('{id}', prevDataViewId))
@@ -118,13 +118,29 @@ export default function ({ getService }: FtrProviderContext) {
);
});
+ it("won't delete if reference remains", async () => {
+ const res = await supertest
+ .post(DATA_VIEW_SWAP_REFERENCES_PATH)
+ .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
+ .send({
+ fromId: '8963ca30-3224-11e8-a572-ffca06da1357',
+ toId: '91200a00-9efd-11e7-acb3-3dab96693fab',
+ forId: ['960372e0-3224-11e8-a572-ffca06da1357'],
+ delete: true,
+ });
+ expect(res).to.have.property('status', 200);
+ expect(res.body.result.length).to.equal(1);
+ expect(res.body.deleteStatus.remainingRefs).to.equal(1);
+ expect(res.body.deleteStatus.deletePerformed).to.equal(false);
+ });
+
it('can limit by id', async () => {
// confirm this will find two items
const res = await supertest
- .post(DATA_VIEW_SWAP_REFERENCES_PATH)
+ .post(PREVIEW_PATH)
.send({
- from_id: '8963ca30-3224-11e8-a572-ffca06da1357',
- to_id: '91200a00-9efd-11e7-acb3-3dab96693fab',
+ fromId: '8963ca30-3224-11e8-a572-ffca06da1357',
+ toId: '91200a00-9efd-11e7-acb3-3dab96693fab',
})
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION);
expect(res).to.have.property('status', 200);
@@ -134,10 +150,9 @@ export default function ({ getService }: FtrProviderContext) {
const res2 = await supertest
.post(DATA_VIEW_SWAP_REFERENCES_PATH)
.send({
- from_id: '8963ca30-3224-11e8-a572-ffca06da1357',
- to_id: '91200a00-9efd-11e7-acb3-3dab96693fab',
- for_id: ['960372e0-3224-11e8-a572-ffca06da1357'],
- preview: false,
+ fromId: '8963ca30-3224-11e8-a572-ffca06da1357',
+ toId: '91200a00-9efd-11e7-acb3-3dab96693fab',
+ forId: ['960372e0-3224-11e8-a572-ffca06da1357'],
})
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION);
expect(res2).to.have.property('status', 200);
@@ -147,10 +162,10 @@ export default function ({ getService }: FtrProviderContext) {
it('can limit by type', async () => {
// confirm this will find two items
const res = await supertest
- .post(DATA_VIEW_SWAP_REFERENCES_PATH)
+ .post(PREVIEW_PATH)
.send({
- from_id: '8963ca30-3224-11e8-a572-ffca06da1357',
- to_id: '91200a00-9efd-11e7-acb3-3dab96693fab',
+ fromId: '8963ca30-3224-11e8-a572-ffca06da1357',
+ toId: '91200a00-9efd-11e7-acb3-3dab96693fab',
})
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION);
expect(res).to.have.property('status', 200);
@@ -160,10 +175,9 @@ export default function ({ getService }: FtrProviderContext) {
const res2 = await supertest
.post(DATA_VIEW_SWAP_REFERENCES_PATH)
.send({
- from_id: '8963ca30-3224-11e8-a572-ffca06da1357',
- to_id: '91200a00-9efd-11e7-acb3-3dab96693fab',
- for_type: 'search',
- preview: false,
+ fromId: '8963ca30-3224-11e8-a572-ffca06da1357',
+ toId: '91200a00-9efd-11e7-acb3-3dab96693fab',
+ forType: 'search',
})
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION);
expect(res2).to.have.property('status', 200);
diff --git a/test/api_integration/apis/telemetry/opt_in.ts b/test/api_integration/apis/telemetry/opt_in.ts
index 943d7534acc0a..c7d8a42c6e392 100644
--- a/test/api_integration/apis/telemetry/opt_in.ts
+++ b/test/api_integration/apis/telemetry/opt_in.ts
@@ -11,6 +11,10 @@ import expect from '@kbn/expect';
import SuperTest from 'supertest';
import type { KbnClient } from '@kbn/test';
import type { TelemetrySavedObjectAttributes } from '@kbn/telemetry-plugin/server/saved_objects';
+import {
+ ELASTIC_HTTP_VERSION_HEADER,
+ X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
+} from '@kbn/core-http-common';
import type { FtrProviderContext } from '../../ftr_provider_context';
export default function optInTest({ getService }: FtrProviderContext) {
@@ -18,7 +22,7 @@ export default function optInTest({ getService }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
const esArchiver = getService('esArchiver');
- describe('/api/telemetry/v2/optIn API', () => {
+ describe('/internal/telemetry/optIn API', () => {
let defaultAttributes: TelemetrySavedObjectAttributes;
let kibanaVersion: string;
before(async () => {
@@ -88,8 +92,10 @@ async function postTelemetryV2OptIn(
statusCode: number
): Promise {
const { body } = await supertest
- .post('/api/telemetry/v2/optIn')
+ .post('/internal/telemetry/optIn')
.set('kbn-xsrf', 'xxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ enabled: value })
.expect(statusCode);
diff --git a/test/api_integration/apis/telemetry/telemetry_config.ts b/test/api_integration/apis/telemetry/telemetry_config.ts
index f6dd12b0c2a9d..a9a04a3986ba7 100644
--- a/test/api_integration/apis/telemetry/telemetry_config.ts
+++ b/test/api_integration/apis/telemetry/telemetry_config.ts
@@ -7,6 +7,10 @@
*/
import { AxiosError } from 'axios';
+import {
+ ELASTIC_HTTP_VERSION_HEADER,
+ X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
+} from '@kbn/core-http-common';
import type { FtrProviderContext } from '../../ftr_provider_context';
const TELEMETRY_SO_TYPE = 'telemetry';
@@ -16,110 +20,146 @@ export default function telemetryConfigTest({ getService }: FtrProviderContext)
const kbnClient = getService('kibanaServer');
const supertest = getService('supertest');
- describe('/api/telemetry/v2/config API Telemetry config', () => {
- before(async () => {
- try {
- await kbnClient.savedObjects.delete({ type: TELEMETRY_SO_TYPE, id: TELEMETRY_SO_ID });
- } catch (err) {
- const is404Error = err instanceof AxiosError && err.response?.status === 404;
- if (!is404Error) {
- throw err;
- }
- }
- });
-
- it('GET should get the default config', async () => {
- await supertest.get('/api/telemetry/v2/config').set('kbn-xsrf', 'xxx').expect(200, {
- allowChangingOptInStatus: true,
- optIn: null, // the config.js for this FTR sets it to `false`, we are bound to ask again.
- sendUsageFrom: 'server',
- telemetryNotifyUserAboutOptInDefault: false, // it's not opted-in by default (that's what this flag is about)
- });
- });
-
- it('GET should get `true` when opted-in', async () => {
- // Opt-in
- await supertest
- .post('/api/telemetry/v2/optIn')
- .set('kbn-xsrf', 'xxx')
- .send({ enabled: true })
- .expect(200);
-
- await supertest.get('/api/telemetry/v2/config').set('kbn-xsrf', 'xxx').expect(200, {
- allowChangingOptInStatus: true,
- optIn: true,
- sendUsageFrom: 'server',
- telemetryNotifyUserAboutOptInDefault: false,
- });
- });
-
- it('GET should get false when opted-out', async () => {
- // Opt-in
- await supertest
- .post('/api/telemetry/v2/optIn')
- .set('kbn-xsrf', 'xxx')
- .send({ enabled: false })
- .expect(200);
-
- await supertest.get('/api/telemetry/v2/config').set('kbn-xsrf', 'xxx').expect(200, {
- allowChangingOptInStatus: true,
- optIn: false,
- sendUsageFrom: 'server',
- telemetryNotifyUserAboutOptInDefault: false,
- });
- });
-
- describe('From a previous version', function () {
- this.tags(['skipCloud']);
-
- // Get current values
- let attributes: Record;
- let currentVersion: string;
- let previousMinor: string;
-
- before(async () => {
- [{ attributes }, currentVersion] = await Promise.all([
- kbnClient.savedObjects.get({ type: TELEMETRY_SO_TYPE, id: TELEMETRY_SO_ID }),
- kbnClient.version.get(),
- ]);
-
- const [major, minor, patch] = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)/)!.map(parseInt);
- previousMinor = `${minor === 0 ? major - 1 : major}.${
- minor === 0 ? minor : minor - 1
- }.${patch}`;
- });
+ describe('API Telemetry config', () => {
+ ['/api/telemetry/v2/config', '/internal/telemetry/config'].forEach((api) => {
+ describe(`GET ${api}`, () => {
+ const apiVersion = api === '/api/telemetry/v2/config' ? '2023-10-31' : '2';
+ before(async () => {
+ try {
+ await kbnClient.savedObjects.delete({ type: TELEMETRY_SO_TYPE, id: TELEMETRY_SO_ID });
+ } catch (err) {
+ const is404Error = err instanceof AxiosError && err.response?.status === 404;
+ if (!is404Error) {
+ throw err;
+ }
+ }
+ });
- it('GET should get `true` when opted-in in the current version', async () => {
- // Opt-in from a previous version
- await kbnClient.savedObjects.create({
- overwrite: true,
- type: TELEMETRY_SO_TYPE,
- id: TELEMETRY_SO_ID,
- attributes: { ...attributes, enabled: true, lastVersionChecked: previousMinor },
+ it('GET should get the default config', async () => {
+ await supertest
+ .get(api)
+ .set('kbn-xsrf', 'xxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, apiVersion)
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
+ .expect(200, {
+ allowChangingOptInStatus: true,
+ optIn: null, // the config.js for this FTR sets it to `false`, we are bound to ask again.
+ sendUsageFrom: 'server',
+ telemetryNotifyUserAboutOptInDefault: false, // it's not opted-in by default (that's what this flag is about)
+ });
});
- await supertest.get('/api/telemetry/v2/config').set('kbn-xsrf', 'xxx').expect(200, {
- allowChangingOptInStatus: true,
- optIn: true,
- sendUsageFrom: 'server',
- telemetryNotifyUserAboutOptInDefault: false,
+ it('GET should get `true` when opted-in', async () => {
+ // Opt-in
+ await supertest
+ .post('/internal/telemetry/optIn')
+ .set('kbn-xsrf', 'xxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
+ .send({ enabled: true })
+ .expect(200);
+
+ await supertest
+ .get(api)
+ .set('kbn-xsrf', 'xxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, apiVersion)
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
+ .expect(200, {
+ allowChangingOptInStatus: true,
+ optIn: true,
+ sendUsageFrom: 'server',
+ telemetryNotifyUserAboutOptInDefault: false,
+ });
});
- });
- it('GET should get `null` when opted-out in a previous version', async () => {
- // Opt-out from previous version
- await kbnClient.savedObjects.create({
- overwrite: true,
- type: TELEMETRY_SO_TYPE,
- id: TELEMETRY_SO_ID,
- attributes: { ...attributes, enabled: false, lastVersionChecked: previousMinor },
+ it('GET should get false when opted-out', async () => {
+ // Opt-in
+ await supertest
+ .post('/internal/telemetry/optIn')
+ .set('kbn-xsrf', 'xxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
+ .send({ enabled: false })
+ .expect(200);
+
+ await supertest
+ .get(api)
+ .set('kbn-xsrf', 'xxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, apiVersion)
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
+ .expect(200, {
+ allowChangingOptInStatus: true,
+ optIn: false,
+ sendUsageFrom: 'server',
+ telemetryNotifyUserAboutOptInDefault: false,
+ });
});
- await supertest.get('/api/telemetry/v2/config').set('kbn-xsrf', 'xxx').expect(200, {
- allowChangingOptInStatus: true,
- optIn: null,
- sendUsageFrom: 'server',
- telemetryNotifyUserAboutOptInDefault: false,
+ describe('From a previous version', function () {
+ this.tags(['skipCloud']);
+
+ // Get current values
+ let attributes: Record;
+ let currentVersion: string;
+ let previousMinor: string;
+
+ before(async () => {
+ [{ attributes }, currentVersion] = await Promise.all([
+ kbnClient.savedObjects.get({ type: TELEMETRY_SO_TYPE, id: TELEMETRY_SO_ID }),
+ kbnClient.version.get(),
+ ]);
+
+ const [major, minor, patch] = currentVersion
+ .match(/^(\d+)\.(\d+)\.(\d+)/)!
+ .map(parseInt);
+ previousMinor = `${minor === 0 ? major - 1 : major}.${
+ minor === 0 ? minor : minor - 1
+ }.${patch}`;
+ });
+
+ it('GET should get `true` when opted-in in the current version', async () => {
+ // Opt-in from a previous version
+ await kbnClient.savedObjects.create({
+ overwrite: true,
+ type: TELEMETRY_SO_TYPE,
+ id: TELEMETRY_SO_ID,
+ attributes: { ...attributes, enabled: true, lastVersionChecked: previousMinor },
+ });
+
+ await supertest
+ .get(api)
+ .set('kbn-xsrf', 'xxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, apiVersion)
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
+ .expect(200, {
+ allowChangingOptInStatus: true,
+ optIn: true,
+ sendUsageFrom: 'server',
+ telemetryNotifyUserAboutOptInDefault: false,
+ });
+ });
+
+ it('GET should get `null` when opted-out in a previous version', async () => {
+ // Opt-out from previous version
+ await kbnClient.savedObjects.create({
+ overwrite: true,
+ type: TELEMETRY_SO_TYPE,
+ id: TELEMETRY_SO_ID,
+ attributes: { ...attributes, enabled: false, lastVersionChecked: previousMinor },
+ });
+
+ await supertest
+ .get(api)
+ .set('kbn-xsrf', 'xxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, apiVersion)
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
+ .expect(200, {
+ allowChangingOptInStatus: true,
+ optIn: null,
+ sendUsageFrom: 'server',
+ telemetryNotifyUserAboutOptInDefault: false,
+ });
+ });
});
});
});
diff --git a/test/api_integration/apis/telemetry/telemetry_last_reported.ts b/test/api_integration/apis/telemetry/telemetry_last_reported.ts
index e553fa0218aa1..6d077dd2857d9 100644
--- a/test/api_integration/apis/telemetry/telemetry_last_reported.ts
+++ b/test/api_integration/apis/telemetry/telemetry_last_reported.ts
@@ -7,23 +7,37 @@
*/
import expect from '@kbn/expect';
+import {
+ ELASTIC_HTTP_VERSION_HEADER,
+ X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
+} from '@kbn/core-http-common';
import type { FtrProviderContext } from '../../ftr_provider_context';
export default function optInTest({ getService }: FtrProviderContext) {
const client = getService('kibanaServer');
const supertest = getService('supertest');
- describe('/api/telemetry/v2/last_reported API Telemetry lastReported', () => {
+ describe('/internal/telemetry/last_reported API Telemetry lastReported', () => {
before(async () => {
await client.savedObjects.delete({ type: 'telemetry', id: 'telemetry' });
});
it('GET should return undefined when there is no stored telemetry.lastReported value', async () => {
- await supertest.get('/api/telemetry/v2/last_reported').set('kbn-xsrf', 'xxx').expect(200, {});
+ await supertest
+ .get('/internal/telemetry/last_reported')
+ .set('kbn-xsrf', 'xxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
+ .expect(200, {});
});
it('PUT should update telemetry.lastReported to now', async () => {
- await supertest.put('/api/telemetry/v2/last_reported').set('kbn-xsrf', 'xxx').expect(200);
+ await supertest
+ .put('/internal/telemetry/last_reported')
+ .set('kbn-xsrf', 'xxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
+ .expect(200);
const {
attributes: { lastReported },
@@ -46,8 +60,10 @@ export default function optInTest({ getService }: FtrProviderContext) {
expect(lastReported).to.be.a('number');
await supertest
- .get('/api/telemetry/v2/last_reported')
+ .get('/internal/telemetry/last_reported')
.set('kbn-xsrf', 'xxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.expect(200, { lastReported });
});
});
diff --git a/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts b/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts
index 53b0d2cadca64..5310e32b87fed 100644
--- a/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts
+++ b/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts
@@ -7,17 +7,26 @@
*/
import expect from '@kbn/expect';
+import {
+ ELASTIC_HTTP_VERSION_HEADER,
+ X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
+} from '@kbn/core-http-common';
import type { FtrProviderContext } from '../../ftr_provider_context';
export default function optInTest({ getService }: FtrProviderContext) {
const client = getService('kibanaServer');
const supertest = getService('supertest');
- describe('/api/telemetry/v2/userHasSeenNotice API Telemetry User has seen OptIn Notice', () => {
+ describe('/internal/telemetry/userHasSeenNotice API Telemetry User has seen OptIn Notice', () => {
it('should update telemetry setting field via PUT', async () => {
await client.savedObjects.delete({ type: 'telemetry', id: 'telemetry' });
- await supertest.put('/api/telemetry/v2/userHasSeenNotice').set('kbn-xsrf', 'xxx').expect(200);
+ await supertest
+ .put('/internal/telemetry/userHasSeenNotice')
+ .set('kbn-xsrf', 'xxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
+ .expect(200);
const {
attributes: { userHasSeenNotice },
diff --git a/test/functional/apps/discover/group2/_data_grid_footer.ts b/test/functional/apps/discover/group2/_data_grid_footer.ts
new file mode 100644
index 0000000000000..a1d2ff0294d23
--- /dev/null
+++ b/test/functional/apps/discover/group2/_data_grid_footer.ts
@@ -0,0 +1,226 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import expect from '@kbn/expect';
+import { FtrProviderContext } from '../ftr_provider_context';
+
+const FOOTER_SELECTOR = 'discoverTableFooter';
+const LOAD_MORE_SELECTOR = 'dscGridSampleSizeFetchMoreLink';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const esArchiver = getService('esArchiver');
+ const kibanaServer = getService('kibanaServer');
+ const dataGrid = getService('dataGrid');
+ const PageObjects = getPageObjects(['common', 'discover', 'timePicker', 'unifiedFieldList']);
+ const defaultSettings = { defaultIndex: 'logstash-*' };
+ const testSubjects = getService('testSubjects');
+ const retry = getService('retry');
+ const security = getService('security');
+
+ describe('discover data grid footer', function () {
+ describe('time field with date type', function () {
+ before(async () => {
+ await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']);
+ await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
+ await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover');
+ });
+
+ after(async () => {
+ await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover');
+ await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional');
+ await kibanaServer.savedObjects.cleanStandardList();
+ await kibanaServer.uiSettings.replace({});
+ });
+
+ beforeEach(async function () {
+ await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings();
+ await kibanaServer.uiSettings.update(defaultSettings);
+ await PageObjects.common.navigateToApp('discover');
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+ });
+
+ it('should show footer only for the last page and allow to load more', async () => {
+ // footer is not shown
+ await testSubjects.missingOrFail(FOOTER_SELECTOR);
+
+ // go to next page
+ await testSubjects.click('pagination-button-next');
+ // footer is not shown yet
+ await retry.try(async function () {
+ await testSubjects.missingOrFail(FOOTER_SELECTOR);
+ });
+
+ // go to the last page
+ await testSubjects.click('pagination-button-4');
+ // footer is shown now
+ await retry.try(async function () {
+ await testSubjects.existOrFail(FOOTER_SELECTOR);
+ });
+ expect(await testSubjects.getVisibleText(FOOTER_SELECTOR)).to.be(
+ 'Search results are limited to 500 documents.\nLoad more'
+ );
+
+ // there is no other pages to see
+ await testSubjects.missingOrFail('pagination-button-5');
+
+ // press "Load more"
+ await testSubjects.click(LOAD_MORE_SELECTOR);
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+
+ // more pages appeared and the footer is gone
+ await retry.try(async function () {
+ await testSubjects.missingOrFail(FOOTER_SELECTOR);
+ });
+
+ // go to the last page
+ await testSubjects.click('pagination-button-9');
+ expect(await testSubjects.getVisibleText(FOOTER_SELECTOR)).to.be(
+ 'Search results are limited to 1,000 documents.\nLoad more'
+ );
+
+ // press "Load more"
+ await testSubjects.click(LOAD_MORE_SELECTOR);
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+
+ // more pages appeared and the footer is gone
+ await retry.try(async function () {
+ await testSubjects.missingOrFail(FOOTER_SELECTOR);
+ });
+
+ // go to the last page
+ await testSubjects.click('pagination-button-14');
+ expect(await testSubjects.getVisibleText(FOOTER_SELECTOR)).to.be(
+ 'Search results are limited to 1,500 documents.\nLoad more'
+ );
+ });
+
+ it('should disable "Load more" button when refresh interval is on', async () => {
+ // go to the last page
+ await testSubjects.click('pagination-button-4');
+ await retry.try(async function () {
+ await testSubjects.existOrFail(FOOTER_SELECTOR);
+ });
+
+ expect(await testSubjects.isEnabled(LOAD_MORE_SELECTOR)).to.be(true);
+
+ // enable the refresh interval
+ await PageObjects.timePicker.startAutoRefresh(10);
+
+ // the button is disabled now
+ await retry.waitFor('disabled state', async function () {
+ return (await testSubjects.isEnabled(LOAD_MORE_SELECTOR)) === false;
+ });
+
+ // disable the refresh interval
+ await PageObjects.timePicker.pauseAutoRefresh();
+
+ // the button is enabled again
+ await retry.waitFor('enabled state', async function () {
+ return (await testSubjects.isEnabled(LOAD_MORE_SELECTOR)) === true;
+ });
+ });
+ });
+
+ describe('time field with date nano type', function () {
+ before(async () => {
+ await security.testUser.setRoles(['kibana_admin', 'kibana_date_nanos']);
+ await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/date_nanos');
+ await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/date_nanos');
+ });
+
+ after(async () => {
+ await esArchiver.unload('test/functional/fixtures/es_archiver/date_nanos');
+ await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/date_nanos');
+ await kibanaServer.savedObjects.cleanStandardList();
+ await kibanaServer.uiSettings.replace({});
+ });
+
+ beforeEach(async function () {
+ await PageObjects.common.setTime({
+ from: 'Sep 10, 2015 @ 00:00:00.000',
+ to: 'Sep 30, 2019 @ 00:00:00.000',
+ });
+ await kibanaServer.uiSettings.update({
+ defaultIndex: 'date-nanos',
+ 'discover:sampleSize': 4,
+ 'discover:sampleRowsPerPage': 2,
+ });
+ await PageObjects.common.navigateToApp('discover');
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+ });
+
+ it('should work for date nanos too', async () => {
+ await PageObjects.unifiedFieldList.clickFieldListItemAdd('_id');
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+
+ expect(await dataGrid.getRowsText()).to.eql([
+ 'Sep 22, 2019 @ 23:50:13.253123345AU_x3-TaGFA8no6QjiSJ',
+ 'Sep 18, 2019 @ 06:50:13.000000104AU_x3-TaGFA8no6Qjis104Z',
+ ]);
+
+ // footer is not shown
+ await testSubjects.missingOrFail(FOOTER_SELECTOR);
+
+ // go to the last page
+ await testSubjects.click('pagination-button-1');
+ await retry.try(async function () {
+ await testSubjects.existOrFail(FOOTER_SELECTOR);
+ });
+ expect(await testSubjects.getVisibleText(FOOTER_SELECTOR)).to.be(
+ 'Search results are limited to 4 documents.\nLoad more'
+ );
+ expect(await dataGrid.getRowsText()).to.eql([
+ 'Sep 18, 2019 @ 06:50:13.000000103BU_x3-TaGFA8no6Qjis103Z',
+ 'Sep 18, 2019 @ 06:50:13.000000102AU_x3-TaGFA8no6Qji102Z',
+ ]);
+
+ // there is no other pages to see yet
+ await testSubjects.missingOrFail('pagination-button-2');
+
+ // press "Load more"
+ await testSubjects.click(LOAD_MORE_SELECTOR);
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+
+ // more pages appeared and the footer is gone
+ await retry.try(async function () {
+ await testSubjects.missingOrFail(FOOTER_SELECTOR);
+ });
+
+ // go to the last page
+ await testSubjects.click('pagination-button-3');
+ expect(await testSubjects.getVisibleText(FOOTER_SELECTOR)).to.be(
+ 'Search results are limited to 8 documents.\nLoad more'
+ );
+
+ expect(await dataGrid.getRowsText()).to.eql([
+ 'Sep 18, 2019 @ 06:50:13.000000000CU_x3-TaGFA8no6QjiSX000Z',
+ 'Sep 18, 2019 @ 06:50:12.999999999AU_x3-TaGFA8no6Qj999Z',
+ ]);
+
+ // press "Load more"
+ await testSubjects.click(LOAD_MORE_SELECTOR);
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+
+ // more pages appeared and the footer is gone
+ await retry.try(async function () {
+ await testSubjects.missingOrFail(FOOTER_SELECTOR);
+ });
+
+ // go to the last page
+ await testSubjects.click('pagination-button-4');
+ await retry.try(async function () {
+ await testSubjects.missingOrFail(FOOTER_SELECTOR);
+ });
+
+ expect(await dataGrid.getRowsText()).to.eql([
+ 'Sep 19, 2015 @ 06:50:13.000100001AU_x3-TaGFA8no000100001Z',
+ ]);
+ });
+ });
+ });
+}
diff --git a/test/functional/apps/discover/group2/index.ts b/test/functional/apps/discover/group2/index.ts
index d6a0aeb9cd9ec..17562157f444e 100644
--- a/test/functional/apps/discover/group2/index.ts
+++ b/test/functional/apps/discover/group2/index.ts
@@ -30,6 +30,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./_data_grid_doc_table'));
loadTestFile(require.resolve('./_data_grid_copy_to_clipboard'));
loadTestFile(require.resolve('./_data_grid_pagination'));
+ loadTestFile(require.resolve('./_data_grid_footer'));
loadTestFile(require.resolve('./_adhoc_data_views'));
loadTestFile(require.resolve('./_sql_view'));
loadTestFile(require.resolve('./_indexpattern_with_unmapped_fields'));
diff --git a/test/plugin_functional/test_suites/telemetry/telemetry.ts b/test/plugin_functional/test_suites/telemetry/telemetry.ts
index cbba01a9ddcb5..a998e139eb5c6 100644
--- a/test/plugin_functional/test_suites/telemetry/telemetry.ts
+++ b/test/plugin_functional/test_suites/telemetry/telemetry.ts
@@ -8,6 +8,10 @@
import expect from '@kbn/expect';
import { KBN_SCREENSHOT_MODE_ENABLED_KEY } from '@kbn/screenshot-mode-plugin/public';
+import {
+ ELASTIC_HTTP_VERSION_HEADER,
+ X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
+} from '@kbn/core-http-common';
import { PluginFunctionalProviderContext } from '../../services';
const TELEMETRY_SO_TYPE = 'telemetry';
@@ -83,8 +87,10 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
it('does not show the banner if opted-in', async () => {
await supertest
- .post('/api/telemetry/v2/optIn')
+ .post('/internal/telemetry/optIn')
.set('kbn-xsrf', 'xxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ enabled: true })
.expect(200);
@@ -95,8 +101,10 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
it('does not show the banner if opted-out in this version', async () => {
await supertest
- .post('/api/telemetry/v2/optIn')
+ .post('/internal/telemetry/optIn')
.set('kbn-xsrf', 'xxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ enabled: false })
.expect(200);
diff --git a/x-pack/dev-tools/api_debug/apis/telemetry/index.js b/x-pack/dev-tools/api_debug/apis/telemetry/index.js
index bd9ffb5ed6c0c..1b2e622d91c77 100644
--- a/x-pack/dev-tools/api_debug/apis/telemetry/index.js
+++ b/x-pack/dev-tools/api_debug/apis/telemetry/index.js
@@ -8,6 +8,6 @@
export const name = 'telemetry';
export const description = 'Get the clusters stats from the Kibana server';
export const method = 'POST';
-export const path = '/api/telemetry/v2/clusters/_stats';
+export const path = '/internal/telemetry/clusters/_stats';
export const body = { unencrypted: true, refreshCache: true };
diff --git a/x-pack/plugins/fleet/common/constants/secrets.ts b/x-pack/plugins/fleet/common/constants/secrets.ts
index 06d370ff81323..7626c36f5c902 100644
--- a/x-pack/plugins/fleet/common/constants/secrets.ts
+++ b/x-pack/plugins/fleet/common/constants/secrets.ts
@@ -6,3 +6,5 @@
*/
export const SECRETS_ENDPOINT_PATH = '/_fleet/secret';
+
+export const SECRETS_MINIMUM_FLEET_SERVER_VERSION = '8.10.0';
diff --git a/x-pack/plugins/fleet/common/services/agent_policies_helpers.ts b/x-pack/plugins/fleet/common/services/agent_policies_helpers.ts
index 52ce24634886e..1ebe49ef4ed39 100644
--- a/x-pack/plugins/fleet/common/services/agent_policies_helpers.ts
+++ b/x-pack/plugins/fleet/common/services/agent_policies_helpers.ts
@@ -5,8 +5,13 @@
* 2.0.
*/
-import type { AgentPolicy } from '../types';
-import { FLEET_SERVER_PACKAGE, FLEET_APM_PACKAGE, FLEET_SYNTHETICS_PACKAGE } from '../constants';
+import type { NewAgentPolicy, AgentPolicy } from '../types';
+import {
+ FLEET_SERVER_PACKAGE,
+ FLEET_APM_PACKAGE,
+ FLEET_SYNTHETICS_PACKAGE,
+ FLEET_ENDPOINT_PACKAGE,
+} from '../constants';
export function policyHasFleetServer(agentPolicy: AgentPolicy) {
if (!agentPolicy.package_policies) {
@@ -26,6 +31,10 @@ export function policyHasSyntheticsIntegration(agentPolicy: AgentPolicy) {
return policyHasIntegration(agentPolicy, FLEET_SYNTHETICS_PACKAGE);
}
+export function policyHasEndpointSecurity(agentPolicy: Partial) {
+ return policyHasIntegration(agentPolicy as AgentPolicy, FLEET_ENDPOINT_PACKAGE);
+}
+
function policyHasIntegration(agentPolicy: AgentPolicy, packageName: string) {
if (!agentPolicy.package_policies) {
return false;
diff --git a/x-pack/plugins/fleet/common/services/agent_policy_config.test.ts b/x-pack/plugins/fleet/common/services/agent_policy_config.test.ts
index 70ee2ae631a3c..e9bd5aa23b5d9 100644
--- a/x-pack/plugins/fleet/common/services/agent_policy_config.test.ts
+++ b/x-pack/plugins/fleet/common/services/agent_policy_config.test.ts
@@ -5,15 +5,15 @@
* 2.0.
*/
+import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock';
import { pick } from 'lodash';
-import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock';
+import { createAgentPolicyMock } from '../mocks';
import {
isAgentPolicyValidForLicense,
unsetAgentPolicyAccordingToLicenseLevel,
} from './agent_policy_config';
-import { generateNewAgentPolicyWithDefaults } from './generate_new_agent_policy';
describe('agent policy config and licenses', () => {
const Platinum = licenseMock.createLicense({ license: { type: 'platinum', mode: 'platinum' } });
@@ -34,13 +34,13 @@ describe('agent policy config and licenses', () => {
});
describe('unsetAgentPolicyAccordingToLicenseLevel', () => {
it('resets all paid features to default if license is gold', () => {
- const defaults = pick(generateNewAgentPolicyWithDefaults(), 'is_protected');
+ const defaults = pick(createAgentPolicyMock(), 'is_protected');
const partialPolicy = { is_protected: true };
const retPolicy = unsetAgentPolicyAccordingToLicenseLevel(partialPolicy, Gold);
expect(retPolicy).toEqual(defaults);
});
it('does not change paid features if license is platinum', () => {
- const expected = pick(generateNewAgentPolicyWithDefaults(), 'is_protected');
+ const expected = pick(createAgentPolicyMock(), 'is_protected');
const partialPolicy = { is_protected: false };
const expected2 = { is_protected: true };
const partialPolicy2 = { is_protected: true };
diff --git a/x-pack/plugins/fleet/common/services/generate_new_agent_policy.test.ts b/x-pack/plugins/fleet/common/services/generate_new_agent_policy.test.ts
index 97e63be4bd701..bc4a6b55f75ee 100644
--- a/x-pack/plugins/fleet/common/services/generate_new_agent_policy.test.ts
+++ b/x-pack/plugins/fleet/common/services/generate_new_agent_policy.test.ts
@@ -27,7 +27,6 @@ describe('generateNewAgentPolicyWithDefaults', () => {
description: 'test description',
namespace: 'test-namespace',
monitoring_enabled: ['logs'],
- is_protected: true,
});
expect(newAgentPolicy).toEqual({
@@ -36,7 +35,7 @@ describe('generateNewAgentPolicyWithDefaults', () => {
namespace: 'test-namespace',
monitoring_enabled: ['logs'],
inactivity_timeout: 1209600,
- is_protected: true,
+ is_protected: false,
});
});
});
diff --git a/x-pack/plugins/fleet/common/services/index.ts b/x-pack/plugins/fleet/common/services/index.ts
index abf06ac54b07c..04f74404ba382 100644
--- a/x-pack/plugins/fleet/common/services/index.ts
+++ b/x-pack/plugins/fleet/common/services/index.ts
@@ -66,6 +66,7 @@ export { agentStatusesToSummary } from './agent_statuses_to_summary';
export {
policyHasFleetServer,
policyHasAPMIntegration,
+ policyHasEndpointSecurity,
policyHasSyntheticsIntegration,
} from './agent_policies_helpers';
diff --git a/x-pack/plugins/fleet/common/types/models/settings.ts b/x-pack/plugins/fleet/common/types/models/settings.ts
index 01f95146e3621..e4175ae3bbfaf 100644
--- a/x-pack/plugins/fleet/common/types/models/settings.ts
+++ b/x-pack/plugins/fleet/common/types/models/settings.ts
@@ -14,4 +14,5 @@ export interface BaseSettings {
export interface Settings extends BaseSettings {
id: string;
preconfigured_fields?: Array<'fleet_server_hosts'>;
+ secret_storage_requirements_met?: boolean;
}
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.test.tsx
index 019395c0cb5f6..fe21159ab347b 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.test.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.test.tsx
@@ -17,11 +17,13 @@ import { allowedExperimentalValues } from '../../../../../../../common/experimen
import { ExperimentalFeaturesService } from '../../../../../../services/experimental_features';
-import type { NewAgentPolicy, AgentPolicy } from '../../../../../../../common/types';
+import { createAgentPolicyMock, createPackagePolicyMock } from '../../../../../../../common/mocks';
+import type { AgentPolicy, NewAgentPolicy } from '../../../../../../../common/types';
import { useLicense } from '../../../../../../hooks/use_license';
import type { LicenseService } from '../../../../../../../common/services';
+import { generateNewAgentPolicyWithDefaults } from '../../../../../../../common/services';
import type { ValidationResults } from '../agent_policy_validation';
@@ -34,12 +36,7 @@ const mockedUseLicence = useLicense as jest.MockedFunction;
describe('Agent policy advanced options content', () => {
let testRender: TestRenderer;
let renderResult: RenderResult;
-
- const mockAgentPolicy: Partial = {
- name: 'some-agent-policy',
- is_managed: false,
- };
-
+ let mockAgentPolicy: Partial;
const mockUpdateAgentPolicy = jest.fn();
const mockValidation = jest.fn() as unknown as ValidationResults;
const usePlatinumLicense = () =>
@@ -48,16 +45,34 @@ describe('Agent policy advanced options content', () => {
isPlatinum: () => true,
} as unknown as LicenseService);
- const render = ({ isProtected = false, policyId = 'agent-policy-1' } = {}) => {
+ const render = ({
+ isProtected = false,
+ policyId = 'agent-policy-1',
+ newAgentPolicy = false,
+ packagePolicy = [createPackagePolicyMock()],
+ } = {}) => {
// remove when feature flag is removed
ExperimentalFeaturesService.init({
...allowedExperimentalValues,
agentTamperProtectionEnabled: true,
});
+ if (newAgentPolicy) {
+ mockAgentPolicy = generateNewAgentPolicyWithDefaults();
+ } else {
+ mockAgentPolicy = {
+ ...createAgentPolicyMock(),
+ package_policies: packagePolicy,
+ id: policyId,
+ };
+ }
+
renderResult = testRender.render(
@@ -118,5 +133,33 @@ describe('Agent policy advanced options content', () => {
});
expect(mockUpdateAgentPolicy).toHaveBeenCalledWith({ is_protected: true });
});
+ describe('when the defend integration is not installed', () => {
+ beforeEach(() => {
+ usePlatinumLicense();
+ render({
+ packagePolicy: [
+ {
+ ...createPackagePolicyMock(),
+ package: { name: 'not-endpoint', title: 'Not Endpoint', version: '0.1.0' },
+ },
+ ],
+ isProtected: true,
+ });
+ });
+ it('should disable the switch and uninstall command link', () => {
+ expect(renderResult.getByTestId('tamperProtectionSwitch')).toBeDisabled();
+ expect(renderResult.getByTestId('uninstallCommandLink')).toBeDisabled();
+ });
+ it('should show an icon tip explaining why the switch is disabled', () => {
+ expect(renderResult.getByTestId('tamperMissingIntegrationTooltip')).toBeTruthy();
+ });
+ });
+ describe('when the user is creating a new agent policy', () => {
+ it('should be disabled, since it has no package policies and therefore elastic defend integration is not installed', async () => {
+ usePlatinumLicense();
+ render({ newAgentPolicy: true });
+ expect(renderResult.getByTestId('tamperProtectionSwitch')).toBeDisabled();
+ });
+ });
});
});
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx
index 8811a7d97ed61..49288da22c935 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { useState } from 'react';
+import React, { useState, useMemo } from 'react';
import {
EuiDescribedFormGroup,
EuiFormRow,
@@ -46,6 +46,8 @@ import type { ValidationResults } from '../agent_policy_validation';
import { ExperimentalFeaturesService, policyHasFleetServer } from '../../../../services';
+import { policyHasEndpointSecurity as hasElasticDefend } from '../../../../../../../common/services';
+
import {
useOutputOptions,
useDownloadSourcesOptions,
@@ -106,6 +108,7 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent =
const { agentTamperProtectionEnabled } = ExperimentalFeaturesService.get();
const licenseService = useLicense();
const [isUninstallCommandFlyoutOpen, setIsUninstallCommandFlyoutOpen] = useState(false);
+ const policyHasElasticDefend = useMemo(() => hasElasticDefend(agentPolicy), [agentPolicy]);
return (
<>
@@ -317,13 +320,34 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent =
}
>
+ {' '}
+ {!policyHasElasticDefend && (
+
+
+
+ )}
+ >
+ }
checked={agentPolicy.is_protected ?? false}
onChange={(e) => {
updateAgentPolicy({ is_protected: e.target.checked });
}}
+ disabled={!policyHasElasticDefend}
data-test-subj="tamperProtectionSwitch"
/>
{agentPolicy.id && (
@@ -333,7 +357,7 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent =
onClick={() => {
setIsUninstallCommandFlyoutOpen(true);
}}
- disabled={agentPolicy.is_protected === false}
+ disabled={!agentPolicy.is_protected || !policyHasElasticDefend}
data-test-subj="uninstallCommandLink"
>
{i18n.translate('xpack.fleet.agentPolicyForm.tamperingUninstallLink', {
diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts
index 807eef8ba9917..74af2fe533a9b 100644
--- a/x-pack/plugins/fleet/server/constants/index.ts
+++ b/x-pack/plugins/fleet/server/constants/index.ts
@@ -79,6 +79,7 @@ export {
MESSAGE_SIGNING_SERVICE_API_ROUTES,
// secrets
SECRETS_ENDPOINT_PATH,
+ SECRETS_MINIMUM_FLEET_SERVER_VERSION,
} from '../../common/constants';
export {
diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts
index b7013bf43ff84..d4f41c0348311 100644
--- a/x-pack/plugins/fleet/server/saved_objects/index.ts
+++ b/x-pack/plugins/fleet/server/saved_objects/index.ts
@@ -90,6 +90,7 @@ const getSavedObjectTypes = (): { [key: string]: SavedObjectsType } => ({
fleet_server_hosts: { type: 'keyword' },
has_seen_add_data_notice: { type: 'boolean', index: false },
prerelease_integrations_enabled: { type: 'boolean' },
+ secret_storage_requirements_met: { type: 'boolean' },
},
},
migrations: {
diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts
index 6958ea80c00d6..44635eee45200 100644
--- a/x-pack/plugins/fleet/server/services/agent_policy.ts
+++ b/x-pack/plugins/fleet/server/services/agent_policy.ts
@@ -22,6 +22,8 @@ import type { BulkResponseItem } from '@elastic/elasticsearch/lib/api/typesWithB
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants';
+import { policyHasEndpointSecurity } from '../../common/services';
+
import { populateAssignedAgentsCount } from '../routes/agent_policy/handlers';
import type { HTTPAuthorizationHeader } from '../../common/http_authorization_header';
@@ -113,7 +115,10 @@ class AgentPolicyService {
id: string,
agentPolicy: Partial,
user?: AuthenticatedUser,
- options: { bumpRevision: boolean } = { bumpRevision: true }
+ options: { bumpRevision: boolean; removeProtection: boolean } = {
+ bumpRevision: true,
+ removeProtection: false,
+ }
): Promise {
auditLoggingService.writeCustomSoAuditLog({
action: 'update',
@@ -136,6 +141,12 @@ class AgentPolicyService {
);
}
+ const logger = appContextService.getLogger();
+
+ if (options.removeProtection) {
+ logger.warn(`Setting tamper protection for Agent Policy ${id} to false`);
+ }
+
await validateOutputForPolicy(
soClient,
agentPolicy,
@@ -145,11 +156,14 @@ class AgentPolicyService {
await soClient.update(SAVED_OBJECT_TYPE, id, {
...agentPolicy,
...(options.bumpRevision ? { revision: existingAgentPolicy.revision + 1 } : {}),
+ ...(options.removeProtection
+ ? { is_protected: false }
+ : { is_protected: agentPolicy.is_protected }),
updated_at: new Date().toISOString(),
updated_by: user ? user.username : 'system',
});
- if (options.bumpRevision) {
+ if (options.bumpRevision || options.removeProtection) {
await this.triggerAgentPolicyUpdatedEvent(soClient, esClient, 'updated', id);
}
@@ -239,6 +253,14 @@ class AgentPolicyService {
this.checkTamperProtectionLicense(agentPolicy);
+ const logger = appContextService.getLogger();
+
+ if (agentPolicy?.is_protected) {
+ logger.warn(
+ 'Agent policy requires Elastic Defend integration to set tamper protection to true'
+ );
+ }
+
await this.requireUniqueName(soClient, agentPolicy);
await validateOutputForPolicy(soClient, agentPolicy);
@@ -253,7 +275,7 @@ class AgentPolicyService {
updated_at: new Date().toISOString(),
updated_by: options?.user?.username || 'system',
schema_version: FLEET_AGENT_POLICIES_SCHEMA_VERSION,
- is_protected: agentPolicy.is_protected ?? false,
+ is_protected: false,
} as AgentPolicy,
options
);
@@ -491,6 +513,16 @@ class AgentPolicyService {
this.checkTamperProtectionLicense(agentPolicy);
+ const logger = appContextService.getLogger();
+
+ if (agentPolicy?.is_protected && !policyHasEndpointSecurity(existingAgentPolicy)) {
+ logger.warn(
+ 'Agent policy requires Elastic Defend integration to set tamper protection to true'
+ );
+ // force agent policy to be false if elastic defend is not present
+ agentPolicy.is_protected = false;
+ }
+
if (existingAgentPolicy.is_managed && !options?.force) {
Object.entries(agentPolicy)
.filter(([key]) => !KEY_EDITABLE_FOR_MANAGED_POLICIES.includes(key))
@@ -586,9 +618,12 @@ class AgentPolicyService {
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
id: string,
- options?: { user?: AuthenticatedUser }
+ options?: { user?: AuthenticatedUser; removeProtection?: boolean }
): Promise {
- const res = await this._update(soClient, esClient, id, {}, options?.user);
+ const res = await this._update(soClient, esClient, id, {}, options?.user, {
+ bumpRevision: true,
+ removeProtection: options?.removeProtection ?? false,
+ });
return res;
}
diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts
index fdce358049006..b6a124d3c32ab 100644
--- a/x-pack/plugins/fleet/server/services/agents/crud.ts
+++ b/x-pack/plugins/fleet/server/services/agents/crud.ts
@@ -444,6 +444,66 @@ export async function getAgentsById(
);
}
+// given a list of agentPolicyIds, return a map of agent version => count of agents
+// this is used to get all fleet server versions
+export async function getAgentVersionsForAgentPolicyIds(
+ esClient: ElasticsearchClient,
+ agentPolicyIds: string[]
+): Promise> {
+ const versionCount: Record = {};
+
+ if (!agentPolicyIds.length) {
+ return versionCount;
+ }
+
+ try {
+ const res = esClient.search<
+ FleetServerAgent,
+ Record<'agent_versions', { buckets: Array<{ key: string; doc_count: number }> }>
+ >({
+ size: 0,
+ track_total_hits: false,
+ body: {
+ query: {
+ bool: {
+ filter: [
+ {
+ terms: {
+ policy_id: agentPolicyIds,
+ },
+ },
+ ],
+ },
+ },
+ aggs: {
+ agent_versions: {
+ terms: {
+ field: 'local_metadata.elastic.agent.version.keyword',
+ size: 1000,
+ },
+ },
+ },
+ },
+ index: AGENTS_INDEX,
+ ignore_unavailable: true,
+ });
+
+ const { aggregations } = await res;
+
+ if (aggregations && aggregations.agent_versions) {
+ aggregations.agent_versions.buckets.forEach((bucket) => {
+ versionCount[bucket.key] = bucket.doc_count;
+ });
+ }
+ } catch (error) {
+ if (error.statusCode !== 404) {
+ throw error;
+ }
+ }
+
+ return versionCount;
+}
+
export async function getAgentByAccessAPIKeyId(
esClient: ElasticsearchClient,
soClient: SavedObjectsClientContract,
diff --git a/x-pack/plugins/fleet/server/services/fleet_server/index.ts b/x-pack/plugins/fleet/server/services/fleet_server/index.ts
index 6ba6dfcc24910..3690e86a71f48 100644
--- a/x-pack/plugins/fleet/server/services/fleet_server/index.ts
+++ b/x-pack/plugins/fleet/server/services/fleet_server/index.ts
@@ -5,10 +5,14 @@
* 2.0.
*/
-import type { ElasticsearchClient } from '@kbn/core/server';
+import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server';
+import semverGte from 'semver/functions/gte';
+import semverCoerce from 'semver/functions/coerce';
import { FLEET_SERVER_SERVERS_INDEX } from '../../constants';
+import { getAgentVersionsForAgentPolicyIds } from '../agents';
+import { packagePolicyService } from '../package_policy';
/**
* Check if at least one fleet server is connected
*/
@@ -23,3 +27,42 @@ export async function hasFleetServers(esClient: ElasticsearchClient) {
return (res.hits.total as number) > 0;
}
+
+export async function allFleetServerVersionsAreAtLeast(
+ esClient: ElasticsearchClient,
+ soClient: SavedObjectsClientContract,
+ version: string
+): Promise {
+ let hasMore = true;
+ const policyIds = new Set();
+ let page = 1;
+ while (hasMore) {
+ const res = await packagePolicyService.list(soClient, {
+ page: page++,
+ perPage: 20,
+ kuery: 'ingest-package-policies.package.name:fleet_server',
+ });
+
+ for (const item of res.items) {
+ policyIds.add(item.policy_id);
+ }
+
+ if (res.items.length === 0) {
+ hasMore = false;
+ }
+ }
+
+ const versionCounts = await getAgentVersionsForAgentPolicyIds(esClient, [...policyIds]);
+ const versions = Object.keys(versionCounts);
+
+ // there must be at least one fleet server agent for this check to pass
+ if (versions.length === 0) {
+ return false;
+ }
+
+ return _allVersionsAreAtLeast(version, versions);
+}
+
+function _allVersionsAreAtLeast(version: string, versions: string[]) {
+ return versions.every((v) => semverGte(semverCoerce(v)!, version));
+}
diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts
index 72fddd0e5b674..6d47be75b4fd6 100644
--- a/x-pack/plugins/fleet/server/services/package_policy.ts
+++ b/x-pack/plugins/fleet/server/services/package_policy.ts
@@ -117,6 +117,7 @@ import {
extractAndUpdateSecrets,
extractAndWriteSecrets,
deleteSecretsIfNotReferenced as deleteSecrets,
+ isSecretStorageEnabled,
} from './secrets';
export type InputsOverride = Partial & {
@@ -243,8 +244,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
}
validatePackagePolicyOrThrow(enrichedPackagePolicy, pkgInfo);
- const { secretsStorage: secretsStorageEnabled } = appContextService.getExperimentalFeatures();
- if (secretsStorageEnabled) {
+ if (await isSecretStorageEnabled(esClient, soClient)) {
const secretsRes = await extractAndWriteSecrets({
packagePolicy: { ...enrichedPackagePolicy, inputs },
packageInfo: pkgInfo,
@@ -747,8 +747,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
});
validatePackagePolicyOrThrow(packagePolicy, pkgInfo);
- const { secretsStorage: secretsStorageEnabled } = appContextService.getExperimentalFeatures();
- if (secretsStorageEnabled) {
+ if (await isSecretStorageEnabled(esClient, soClient)) {
const secretsRes = await extractAndUpdateSecrets({
oldPackagePolicy,
packagePolicyUpdate: { ...restOfPackagePolicy, inputs },
@@ -913,9 +912,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
);
if (pkgInfo) {
validatePackagePolicyOrThrow(packagePolicy, pkgInfo);
- const { secretsStorage: secretsStorageEnabled } =
- appContextService.getExperimentalFeatures();
- if (secretsStorageEnabled) {
+ if (await isSecretStorageEnabled(esClient, soClient)) {
const secretsRes = await extractAndUpdateSecrets({
oldPackagePolicy,
packagePolicyUpdate: { ...restOfPackagePolicy, inputs },
@@ -1161,13 +1158,22 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
...new Set(result.filter((r) => r.success && r.policy_id).map((r) => r.policy_id!)),
];
+ const agentPoliciesWithEndpointPackagePolicies = result.reduce((acc, cur) => {
+ if (cur.success && cur.policy_id && cur.package?.name === 'endpoint') {
+ return acc.add(cur.policy_id);
+ }
+ return acc;
+ }, new Set());
+
const agentPolicies = await agentPolicyService.getByIDs(soClient, uniquePolicyIdsR);
for (const policyId of uniquePolicyIdsR) {
const agentPolicy = agentPolicies.find((p) => p.id === policyId);
if (agentPolicy) {
+ // is the agent policy attached to package policy with endpoint
await agentPolicyService.bumpRevision(soClient, esClient, policyId, {
user: options?.user,
+ removeProtection: agentPoliciesWithEndpointPackagePolicies.has(policyId),
});
}
}
diff --git a/x-pack/plugins/fleet/server/services/secrets.ts b/x-pack/plugins/fleet/server/services/secrets.ts
index 7e6efde6c11b0..886e7d7243172 100644
--- a/x-pack/plugins/fleet/server/services/secrets.ts
+++ b/x-pack/plugins/fleet/server/services/secrets.ts
@@ -34,7 +34,7 @@ import type {
} from '../types';
import { FleetError } from '../errors';
-import { SECRETS_ENDPOINT_PATH } from '../constants';
+import { SECRETS_ENDPOINT_PATH, SECRETS_MINIMUM_FLEET_SERVER_VERSION } from '../constants';
import { retryTransientEsErrors } from './epm/elasticsearch/retry';
@@ -42,6 +42,8 @@ import { auditLoggingService } from './audit_logging';
import { appContextService } from './app_context';
import { packagePolicyService } from './package_policy';
+import { settingsService } from '.';
+import { allFleetServerVersionsAreAtLeast } from './fleet_server';
export async function createSecrets(opts: {
esClient: ElasticsearchClient;
@@ -270,10 +272,21 @@ export async function extractAndUpdateSecrets(opts: {
...createdSecrets.map(({ id }) => ({ id })),
];
+ const secretsToDelete: PolicySecretReference[] = [];
+
+ toDelete.forEach((secretPath) => {
+ // check if the previous secret is actually a secret refrerence
+ // it may be that secrets were not enabled at the time of creation
+ // in which case they are just stored as plain text
+ if (secretPath.value.value.isSecretRef) {
+ secretsToDelete.push({ id: secretPath.value.value.id });
+ }
+ });
+
return {
packagePolicyUpdate: policyWithSecretRefs,
secretReferences,
- secretsToDelete: toDelete.map((secretPath) => ({ id: secretPath.value.value.id })),
+ secretsToDelete,
};
}
@@ -344,6 +357,58 @@ export function getPolicySecretPaths(
return [...packageLevelVarPaths, ...inputSecretPaths];
}
+export async function isSecretStorageEnabled(
+ esClient: ElasticsearchClient,
+ soClient: SavedObjectsClientContract
+): Promise {
+ const logger = appContextService.getLogger();
+
+ // first check if the feature flag is enabled, if not secrets are disabled
+ const { secretsStorage: secretsStorageEnabled } = appContextService.getExperimentalFeatures();
+ if (!secretsStorageEnabled) {
+ logger.debug('Secrets storage is disabled by feature flag');
+ return false;
+ }
+
+ // if serverless then secrets will always be supported
+ const isFleetServerStandalone =
+ appContextService.getConfig()?.internal?.fleetServerStandalone ?? false;
+
+ if (isFleetServerStandalone) {
+ logger.trace('Secrets storage is enabled as fleet server is standalone');
+ return true;
+ }
+
+ // now check the flag in settings to see if the fleet server requirement has already been met
+ // once the requirement has been met, secrets are always on
+ const settings = await settingsService.getSettings(soClient);
+
+ if (settings.secret_storage_requirements_met) {
+ logger.debug('Secrets storage already met, turned on is settings');
+ return true;
+ }
+
+ // otherwise check if we have the minimum fleet server version and enable secrets if so
+ if (
+ await allFleetServerVersionsAreAtLeast(esClient, soClient, SECRETS_MINIMUM_FLEET_SERVER_VERSION)
+ ) {
+ logger.debug('Enabling secrets storage as minimum fleet server version has been met');
+ try {
+ await settingsService.saveSettings(soClient, {
+ secret_storage_requirements_met: true,
+ });
+ } catch (err) {
+ // we can suppress this error as it will be retried on the next function call
+ logger.warn(`Failed to save settings after enabling secrets storage: ${err.message}`);
+ }
+
+ return true;
+ }
+
+ logger.info('Secrets storage is disabled as minimum fleet server version has not been met');
+ return false;
+}
+
function _getPackageLevelSecretPaths(
packagePolicy: NewPackagePolicy,
packageInfo: PackageInfo
diff --git a/x-pack/plugins/fleet/server/types/so_attributes.ts b/x-pack/plugins/fleet/server/types/so_attributes.ts
index 941f27cde7c4d..5706a7ec12ff3 100644
--- a/x-pack/plugins/fleet/server/types/so_attributes.ts
+++ b/x-pack/plugins/fleet/server/types/so_attributes.ts
@@ -202,6 +202,7 @@ export interface SettingsSOAttributes {
prerelease_integrations_enabled: boolean;
has_seen_add_data_notice?: boolean;
fleet_server_hosts?: string[];
+ secret_storage_requirements_met?: boolean;
}
export interface DownloadSourceSOAttributes {
diff --git a/x-pack/plugins/observability/dev_docs/composite_slo.md b/x-pack/plugins/observability/dev_docs/composite_slo.md
index f3018e33d46dd..4e34933c8560e 100644
--- a/x-pack/plugins/observability/dev_docs/composite_slo.md
+++ b/x-pack/plugins/observability/dev_docs/composite_slo.md
@@ -28,7 +28,7 @@ curl --request POST \
],
"timeWindow": {
"duration": "7d",
- "isRolling": true
+ "type": "rolling"
},
"budgetingMethod": "occurrences",
"objective": {
diff --git a/x-pack/plugins/observability/dev_docs/slo.md b/x-pack/plugins/observability/dev_docs/slo.md
index ab605e51ffd27..13e74fef228e8 100644
--- a/x-pack/plugins/observability/dev_docs/slo.md
+++ b/x-pack/plugins/observability/dev_docs/slo.md
@@ -23,9 +23,9 @@ The **custom Metric** SLI requires an index pattern, an optional filter query, a
We support **calendar aligned** and **rolling** time windows. Any duration greater than 1 day can be used: days, weeks, months, quarters, years.
-**Rolling time window:** Requires a duration, e.g. `1w` for one week, and `isRolling: true`. SLOs defined with such time window, will only considere the SLI data from the last duration period as a moving window.
+**Rolling time window:** Requires a duration, e.g. `1w` for one week, and `type: rolling`. SLOs defined with such time window, will only considere the SLI data from the last duration period as a moving window.
-**Calendar aligned time window:** Requires a duration, limited to `1M` for monthly or `1w` for weekly, and `isCalendar: true`.
+**Calendar aligned time window:** Requires a duration, limited to `1M` for monthly or `1w` for weekly, and `type: calendarAligned`.
### Budgeting method
@@ -46,8 +46,8 @@ If a **timeslices** budgeting method is used, we also need to define the **times
The default settings should be sufficient for most users, but if needed, the following properties can be overwritten:
-- **syncDelay**: The ingest delay in the source data
-- **frequency**: How often do we query the source data
+- **syncDelay**: The ingest delay in the source data, defaults to `1m`
+- **frequency**: How often do we query the source data, defaults to `1m`
## Example
@@ -77,7 +77,7 @@ curl --request POST \
},
"timeWindow": {
"duration": "30d",
- "isRolling": true
+ "type": "rolling"
},
"budgetingMethod": "occurrences",
"objective": {
@@ -112,7 +112,7 @@ curl --request POST \
},
"timeWindow": {
"duration": "1M",
- "isCalendar": true
+ "type": "calendarAligned"
},
"budgetingMethod": "occurrences",
"objective": {
@@ -146,8 +146,8 @@ curl --request POST \
}
},
"timeWindow": {
- "duration": "1w",
- "isRolling": true
+ "duration": "7d",
+ "type": "rolling"
},
"budgetingMethod": "timeslices",
"objective": {
@@ -187,7 +187,7 @@ curl --request POST \
},
"timeWindow": {
"duration": "7d",
- "isRolling": true
+ "type": "rolling"
},
"budgetingMethod": "occurrences",
"objective": {
@@ -223,7 +223,7 @@ curl --request POST \
},
"timeWindow": {
"duration": "7d",
- "isRolling": true
+ "type": "rolling"
},
"budgetingMethod": "timeslices",
"objective": {
@@ -261,7 +261,7 @@ curl --request POST \
},
"timeWindow": {
"duration": "1w",
- "isCalendar": true
+ "type": "calendarAligned"
},
"budgetingMethod": "timeslices",
"objective": {
@@ -300,7 +300,7 @@ curl --request POST \
},
"timeWindow": {
"duration": "7d",
- "isRolling": true
+ "type": "rolling"
},
"budgetingMethod": "occurrences",
"objective": {
@@ -355,7 +355,7 @@ curl --request POST \
},
"timeWindow": {
"duration": "7d",
- "isRolling": true
+ "type": "rolling"
},
"budgetingMethod": "occurrences",
"objective": {
diff --git a/x-pack/plugins/observability_ai_assistant/README.md b/x-pack/plugins/observability_ai_assistant/README.md
index 1e2a19618825e..8487cbf13ba10 100644
--- a/x-pack/plugins/observability_ai_assistant/README.md
+++ b/x-pack/plugins/observability_ai_assistant/README.md
@@ -1,3 +1,73 @@
-# Observability AI Assistant plugin
+### **1. Observability AI Assistant Overview**
-This plugin provides the Observability AI Assistant service and UI components.
+#### **1.1. Introduction**
+
+This document gives an overview of the features of the Observability AI Assistant at the time of writing, and how to use them. At a high level, the Observability AI Assistant offers contextual insights, and a chat functionality that we enrich with function calling, allowing the LLM to hook into the user's data. We also allow the LLM to store things it considers new information as embeddings into Elasticsearch, and query this knowledge base when it decides it needs more information, using ELSER.
+
+#### **1.1. Configuration**
+
+Users can connect to an LLM using [connectors](https://www.elastic.co/guide/en/kibana/current/action-types.html) - specifically the [Generative AI connector](https://www.elastic.co/guide/en/kibana/current/gen-ai-action-type.html), which currently supports both OpenAI and Azure OpenAI as providers. The connector is Enterprise-only. Users can also leverage [preconfigured connectors](https://www.elastic.co/guide/en/kibana/current/pre-configured-connectors.html), in which case the following should be added to `kibana.yml`:
+
+```yaml
+xpack.actions.preconfigured:
+ open-ai:
+ actionTypeId: .gen-ai
+ name: OpenAI
+ config:
+ apiUrl: https://api.openai.com/v1/chat/completions
+ apiProvider: OpenAI
+ secrets:
+ apiKey:
+ azure-open-ai:
+ actionTypeId: .gen-ai
+ name: Azure OpenAI
+ config:
+ apiUrl: https://.openai.azure.com/openai/deployments//chat/completions?api-version=
+ apiProvider: Azure OpenAI
+ secrets:
+ apiKey:
+```
+
+**Note**: The configured deployed model should support [function calling](https://platform.openai.com/docs/guides/gpt/function-calling). For OpenAI, this is usually the case. For Azure, the minimum `apiVersion` is `2023-07-01-preview`. We also recommend a model with a pretty sizable token context length.
+
+#### **1.2. Feature controls**
+
+Access to the Observability AI Assistant and its APIs is managed through [Kibana privileges](https://www.elastic.co/guide/en/kibana/current/kibana-privileges.html).
+
+The feature privilege is only available to those with an Enterprise licene.
+
+#### **1.2. Access Points**
+
+- **1.2.1. Contextual insights**
+
+In several places in the Observability apps, the AI Assistant can generate content that helps users understand what they are looking at. We call these contextual insights. Some examples:
+
+- In Profiling, the AI Assistant explains a displayed function and suggests optimisation opportunities
+- In APM, it explains the meaning of a specific error or exception and offers common causes and possible impact
+- In Alerting, the AI Assistant takes the results of the log spike analysis, and tries to find a root cause for the spike
+
+The user can then also continue the conversation in a flyout by clicking "Start chat".
+
+- **1.2.2. Action Menu Button**
+
+All Observability apps also have a button in the top action menu, to open the AI Assistant and start a conversation.
+
+- **1.2.3. Standalone page**
+
+Users can also access existing conversations and create a new one by navigating to `/app/observabilityAIAssistant/conversations/new`. They can also find this link in the search bar.
+
+#### **1.3. Chat**
+
+Conversations with the AI Assistant are powered by three foundational components: the LLM (currently only OpenAI flavors), the knowledge base, and function calling.
+
+The LLM essentially sits between the product and the user. Its purpose is to interpret both the messages from the user and the response from the functions called, and offer its conclusions and suggest next steps. It can suggest functions on its own, and it has read and write access to the knowledge base.
+
+The knowledge base is an Elasticsearch index, with an inference processor powered by ELSER. Kibana developers can preload embeddings into this index, and users can access them too, via plain Elasticsearch APIs or specific Kibana APIs. Additionally, the LLM can query the knowledge base for additional context and store things it has learned from a conversation.
+
+Both the user and the LLM are able to suggest functions, that are executed on behalf (and with the privileges of) the user. Functions allow both the user and the LLM to include relevant context into the conversation. This context can be text, data, or a visual component, like a timeseries graph. Some of the functions that are available are:
+
+- `recall` and `summarise`: these functions query (with a semantic search) or write to (with a summarisation) the knowledge database. This allows the LLM to create a (partly) user-specific working memory, and access predefined embeddings that help improve its understanding of the Elastic platform.
+- `lens`: a function that can be used to create Lens visualisations using Formulas.
+- `get_apm_timeseries`, `get_apm_service_summary`, `get_apm_downstream_dependencies` and `get_apm_error_document`: a set of APM functions, some with visual components, that are helpful in performing root cause analysis.
+
+Function calling is completely transparent to the user - they can edit function suggestions from the LLM, or inspect a function response (but not edit it), or they can request a function themselves.
diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts
index 47c7cb7b39f70..3dd1f8b188178 100644
--- a/x-pack/plugins/security_solution/common/constants.ts
+++ b/x-pack/plugins/security_solution/common/constants.ts
@@ -20,6 +20,7 @@ export { SecurityPageName } from '@kbn/security-solution-navigation';
*/
export const APP_ID = 'securitySolution' as const;
export const APP_UI_ID = 'securitySolutionUI' as const;
+export const ASSISTANT_FEATURE_ID = 'securitySolutionAssistant' as const;
export const CASES_FEATURE_ID = 'securitySolutionCases' as const;
export const SERVER_APP_ID = 'siem' as const;
export const APP_NAME = 'Security' as const;
diff --git a/x-pack/plugins/security_solution/common/types/app_features.ts b/x-pack/plugins/security_solution/common/types/app_features.ts
index 80734f9f23992..a8c65aeadfc8a 100644
--- a/x-pack/plugins/security_solution/common/types/app_features.ts
+++ b/x-pack/plugins/security_solution/common/types/app_features.ts
@@ -55,6 +55,13 @@ export enum AppFeatureSecurityKey {
osqueryAutomatedResponseActions = 'osquery_automated_response_actions',
}
+export enum AppFeatureAssistantKey {
+ /**
+ * Enables Elastic AI Assistant
+ */
+ assistant = 'assistant',
+}
+
export enum AppFeatureCasesKey {
/**
* Enables Cases Connectors
@@ -63,9 +70,13 @@ export enum AppFeatureCasesKey {
}
// Merges the two enums.
-export type AppFeatureKey = AppFeatureSecurityKey | AppFeatureCasesKey;
+export type AppFeatureKey = AppFeatureSecurityKey | AppFeatureCasesKey | AppFeatureAssistantKey;
export type AppFeatureKeys = AppFeatureKey[];
// We need to merge the value and the type and export both to replicate how enum works.
-export const AppFeatureKey = { ...AppFeatureSecurityKey, ...AppFeatureCasesKey };
+export const AppFeatureKey = {
+ ...AppFeatureSecurityKey,
+ ...AppFeatureCasesKey,
+ ...AppFeatureAssistantKey,
+};
export const ALL_APP_FEATURE_KEYS = Object.freeze(Object.values(AppFeatureKey));
diff --git a/x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/read_only_view.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/read_only_view.cy.ts
index 5cbb1da916440..6464b782ae675 100644
--- a/x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/read_only_view.cy.ts
+++ b/x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/read_only_view.cy.ts
@@ -13,7 +13,7 @@ import { login, visitWithoutDateRange } from '../../../tasks/login';
import { goToExceptionsTab, goToAlertsTab } from '../../../tasks/rule_details';
import { goToRuleDetails } from '../../../tasks/alerts_detection_rules';
import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation';
-import { deleteAlertsAndRules } from '../../../tasks/common';
+import { cleanKibana, deleteAlertsAndRules } from '../../../tasks/common';
import {
NO_EXCEPTIONS_EXIST_PROMPT,
EXCEPTION_ITEM_VIEWER_CONTAINER,
@@ -31,7 +31,7 @@ describe('Exceptions viewer read only', () => {
const exceptionList = getExceptionList();
before(() => {
- cy.task('esArchiverResetKibana');
+ cleanKibana();
// create rule with exceptions
createExceptionList(exceptionList, exceptionList.list_id).then((response) => {
createRule(
@@ -56,6 +56,7 @@ describe('Exceptions viewer read only', () => {
login(ROLES.reader);
visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL, ROLES.reader);
goToRuleDetails();
+ cy.url().should('contain', 'app/security/rules/id');
goToExceptionsTab();
});
diff --git a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts
index ec8328cbc961f..ef8884f560dce 100644
--- a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts
+++ b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts
@@ -14,7 +14,7 @@ import {
import {
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ABOUT_SECTION_CONTENT,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ABOUT_SECTION_HEADER,
- DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_TREE,
+ DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_PREVIEW_CONTENT,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_DETAILS,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_TITLE,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_OPEN_RULE_PREVIEW_BUTTON,
@@ -25,8 +25,6 @@ import {
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITIES_CONTENT,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITIES_HEADER,
- DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITY_PANEL_CONTENT,
- DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITY_PANEL_HEADER,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_CONTENT,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_HEADER,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_VALUES,
@@ -40,17 +38,15 @@ import {
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_MITRE_ATTACK_TITLE,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_DETAILS,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_TITLE,
- DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW,
+ DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW_CONTENT,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_TABLE_FIELD_CELL,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_TABLE_VALUE_CELL,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_RESPONSE_SECTION_EMPTY_RESPONSE,
} from '../../../../screens/expandable_flyout/alert_details_right_panel_overview_tab';
import {
- clickCorrelationsViewAllButton,
- clickEntitiesViewAllButton,
+ navigateToCorrelationsDetails,
clickInvestigationGuideButton,
- clickPrevalenceViewAllButton,
- clickThreatIntelligenceViewAllButton,
+ navigateToPrevalenceDetails,
toggleOverviewTabAboutSection,
toggleOverviewTabInsightsSection,
toggleOverviewTabInvestigationSection,
@@ -138,20 +134,19 @@ describe('Alert details expandable flyout right panel overview tab', () => {
cy.log('analyzer graph preview');
- cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_TREE).scrollIntoView();
- cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_TREE).should('be.visible');
+ cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_PREVIEW_CONTENT).scrollIntoView();
+ cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_PREVIEW_CONTENT).should('be.visible');
cy.log('session view preview');
- cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW).scrollIntoView();
- cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW).should('be.visible');
+ cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW_CONTENT).scrollIntoView();
+ cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW_CONTENT).should('be.visible');
});
});
describe('investigation section', () => {
it('should display investigation section', () => {
toggleOverviewTabAboutSection();
- toggleOverviewTabInvestigationSection();
cy.log('header and content');
@@ -210,6 +205,7 @@ describe('Alert details expandable flyout right panel overview tab', () => {
describe('insights section', () => {
it('should display entities section', () => {
toggleOverviewTabAboutSection();
+ toggleOverviewTabInvestigationSection();
toggleOverviewTabInsightsSection();
cy.log('header and content');
@@ -219,22 +215,18 @@ describe('Alert details expandable flyout right panel overview tab', () => {
.should('be.visible')
.and('have.text', 'Entities');
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITIES_CONTENT).should('be.visible');
- cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITY_PANEL_HEADER).should(
- 'be.visible'
- );
- cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITY_PANEL_CONTENT).should(
- 'be.visible'
- );
+ cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITIES_HEADER).should('be.visible');
cy.log('should navigate to left panel Entities tab');
- clickEntitiesViewAllButton();
- cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT).should('be.visible');
+ // TODO: skipping this section as Cypress can't seem to find the element (though it's in the DOM)
+ // navigateToEntitiesDetails();
+ // cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT).should('be.visible');
});
- // TODO: skipping this due to flakiness
- it.skip('should display threat intelligence section', () => {
+ it('should display threat intelligence section', () => {
toggleOverviewTabAboutSection();
+ toggleOverviewTabInvestigationSection();
toggleOverviewTabInsightsSection();
cy.log('header and content');
@@ -266,8 +258,9 @@ describe('Alert details expandable flyout right panel overview tab', () => {
cy.log('should navigate to left panel Threat Intelligence tab');
- clickThreatIntelligenceViewAllButton();
- cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT).should('be.visible'); // TODO update when we can navigate to Threat Intelligence sub tab directly
+ // TODO: skipping this section as Cypress can't seem to find the element (though it's in the DOM)
+ // navigateToThreatIntelligenceDetails();
+ // cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT).should('be.visible'); // TODO update when we can navigate to Threat Intelligence sub tab directly
});
// TODO: skipping this due to flakiness
@@ -277,6 +270,7 @@ describe('Alert details expandable flyout right panel overview tab', () => {
createNewCaseFromExpandableFlyout();
toggleOverviewTabAboutSection();
+ toggleOverviewTabInvestigationSection();
toggleOverviewTabInsightsSection();
cy.log('header and content');
@@ -294,10 +288,6 @@ describe('Alert details expandable flyout right panel overview tab', () => {
.eq(0)
.should('be.visible')
.and('have.text', '1 alert related by ancestry');
- cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES)
- .eq(1)
- .should('be.visible')
- .and('have.text', '1 related case');
// cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES)
// .eq(2)
// .should('be.visible')
@@ -306,11 +296,15 @@ describe('Alert details expandable flyout right panel overview tab', () => {
.eq(2)
.should('be.visible')
.and('have.text', '1 alert related by session');
+ cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES)
+ .eq(1)
+ .should('be.visible')
+ .and('have.text', '1 related case');
});
cy.log('should navigate to left panel Correlations tab');
- clickCorrelationsViewAllButton();
+ navigateToCorrelationsDetails();
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT).should('be.visible'); // TODO update when we can navigate to Correlations sub tab directly
});
@@ -318,6 +312,7 @@ describe('Alert details expandable flyout right panel overview tab', () => {
// we need to generate enough data to have at least one field with prevalence
it.skip('should display prevalence section', () => {
toggleOverviewTabAboutSection();
+ toggleOverviewTabInvestigationSection();
toggleOverviewTabInsightsSection();
cy.log('header and content');
@@ -337,7 +332,7 @@ describe('Alert details expandable flyout right panel overview tab', () => {
cy.log('should navigate to left panel Prevalence tab');
- clickPrevalenceViewAllButton();
+ navigateToPrevalenceDetails();
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT).should('be.visible'); // TODO update when we can navigate to Prevalence sub tab directly
});
});
@@ -345,6 +340,7 @@ describe('Alert details expandable flyout right panel overview tab', () => {
describe('response section', () => {
it('should display empty message', () => {
toggleOverviewTabAboutSection();
+ toggleOverviewTabInvestigationSection();
toggleOverviewTabResponseSection();
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_RESPONSE_SECTION_EMPTY_RESPONSE).should(
diff --git a/x-pack/plugins/security_solution/cypress/screens/expandable_flyout/alert_details_right_panel_overview_tab.ts b/x-pack/plugins/security_solution/cypress/screens/expandable_flyout/alert_details_right_panel_overview_tab.ts
index 21fef179980d5..6bc2f1400e0ae 100644
--- a/x-pack/plugins/security_solution/cypress/screens/expandable_flyout/alert_details_right_panel_overview_tab.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/expandable_flyout/alert_details_right_panel_overview_tab.ts
@@ -9,30 +9,15 @@ import { getDataTestSubjectSelector } from '../../helpers/common';
import {
ABOUT_SECTION_CONTENT_TEST_ID,
ABOUT_SECTION_HEADER_TEST_ID,
- ANALYZER_TREE_TEST_ID,
DESCRIPTION_DETAILS_TEST_ID,
DESCRIPTION_TITLE_TEST_ID,
RULE_SUMMARY_BUTTON_TEST_ID,
- ENTITIES_CONTENT_TEST_ID,
- ENTITIES_HEADER_TEST_ID,
- ENTITIES_VIEW_ALL_BUTTON_TEST_ID,
- ENTITY_PANEL_CONTENT_TEST_ID,
- ENTITY_PANEL_HEADER_TEST_ID,
HIGHLIGHTED_FIELDS_DETAILS_TEST_ID,
HIGHLIGHTED_FIELDS_TITLE_TEST_ID,
INSIGHTS_CORRELATIONS_CONTENT_TEST_ID,
- INSIGHTS_CORRELATIONS_TITLE_TEST_ID,
- INSIGHTS_CORRELATIONS_VALUE_TEST_ID,
- INSIGHTS_CORRELATIONS_VIEW_ALL_BUTTON_TEST_ID,
INSIGHTS_HEADER_TEST_ID,
INSIGHTS_PREVALENCE_CONTENT_TEST_ID,
- INSIGHTS_PREVALENCE_TITLE_TEST_ID,
- INSIGHTS_PREVALENCE_VALUE_TEST_ID,
- INSIGHTS_PREVALENCE_VIEW_ALL_BUTTON_TEST_ID,
INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID,
- INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID,
- INSIGHTS_THREAT_INTELLIGENCE_VALUE_TEST_ID,
- INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID,
INVESTIGATION_GUIDE_BUTTON_TEST_ID,
INVESTIGATION_SECTION_CONTENT_TEST_ID,
INVESTIGATION_SECTION_HEADER_TEST_ID,
@@ -40,11 +25,20 @@ import {
MITRE_ATTACK_TITLE_TEST_ID,
REASON_DETAILS_TEST_ID,
REASON_TITLE_TEST_ID,
- SESSION_PREVIEW_TEST_ID,
VISUALIZATIONS_SECTION_HEADER_TEST_ID,
HIGHLIGHTED_FIELDS_CELL_TEST_ID,
RESPONSE_SECTION_HEADER_TEST_ID,
RESPONSE_EMPTY_TEST_ID,
+ INSIGHTS_ENTITIES_TITLE_LINK_TEST_ID,
+ INSIGHTS_ENTITIES_CONTENT_TEST_ID,
+ INSIGHTS_THREAT_INTELLIGENCE_TITLE_LINK_TEST_ID,
+ INSIGHTS_CORRELATIONS_TITLE_LINK_TEST_ID,
+ INSIGHTS_PREVALENCE_TITLE_LINK_TEST_ID,
+ INSIGHTS_THREAT_INTELLIGENCE_VALUE_TEST_ID,
+ INSIGHTS_CORRELATIONS_VALUE_TEST_ID,
+ ANALYZER_PREVIEW_CONTENT_TEST_ID,
+ SESSION_PREVIEW_CONTENT_TEST_ID,
+ INSIGHTS_PREVALENCE_VALUE_TEST_ID,
} from '../../../public/flyout/right/components/test_ids';
/* About section */
@@ -94,50 +88,49 @@ export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INVESTIGATION_GUIDE_BUTTON =
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_SECTION_HEADER =
getDataTestSubjectSelector(INSIGHTS_HEADER_TEST_ID);
+
+/* Insights Entities */
+
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITIES_HEADER =
- getDataTestSubjectSelector(ENTITIES_HEADER_TEST_ID);
+ getDataTestSubjectSelector(INSIGHTS_ENTITIES_TITLE_LINK_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITIES_CONTENT =
- getDataTestSubjectSelector(ENTITIES_CONTENT_TEST_ID);
-export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_VIEW_ALL_ENTITIES_BUTTON =
- getDataTestSubjectSelector(ENTITIES_VIEW_ALL_BUTTON_TEST_ID);
-export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITY_PANEL_HEADER =
- getDataTestSubjectSelector(ENTITY_PANEL_HEADER_TEST_ID);
-export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITY_PANEL_CONTENT =
- getDataTestSubjectSelector(ENTITY_PANEL_CONTENT_TEST_ID);
+ getDataTestSubjectSelector(INSIGHTS_ENTITIES_CONTENT_TEST_ID);
+
+/* Insights Threat Intelligence */
+
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_HEADER =
- getDataTestSubjectSelector(INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID);
+ getDataTestSubjectSelector(INSIGHTS_THREAT_INTELLIGENCE_TITLE_LINK_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_CONTENT =
getDataTestSubjectSelector(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VALUES =
getDataTestSubjectSelector(INSIGHTS_THREAT_INTELLIGENCE_VALUE_TEST_ID);
-export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON =
- getDataTestSubjectSelector(INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID);
+
+/* Insights Correlations */
+
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_HEADER =
- getDataTestSubjectSelector(INSIGHTS_CORRELATIONS_TITLE_TEST_ID);
+ getDataTestSubjectSelector(INSIGHTS_CORRELATIONS_TITLE_LINK_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_CONTENT =
getDataTestSubjectSelector(INSIGHTS_CORRELATIONS_CONTENT_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VALUES =
getDataTestSubjectSelector(INSIGHTS_CORRELATIONS_VALUE_TEST_ID);
-export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VIEW_ALL_BUTTON =
- getDataTestSubjectSelector(INSIGHTS_CORRELATIONS_VIEW_ALL_BUTTON_TEST_ID);
+
+/* Insights Prevalence */
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_HEADER =
- getDataTestSubjectSelector(INSIGHTS_PREVALENCE_TITLE_TEST_ID);
+ getDataTestSubjectSelector(INSIGHTS_PREVALENCE_TITLE_LINK_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_CONTENT =
getDataTestSubjectSelector(INSIGHTS_PREVALENCE_CONTENT_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_VALUES =
getDataTestSubjectSelector(INSIGHTS_PREVALENCE_VALUE_TEST_ID);
-export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_VIEW_ALL_BUTTON =
- getDataTestSubjectSelector(INSIGHTS_PREVALENCE_VIEW_ALL_BUTTON_TEST_ID);
/* Visualization section */
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_VISUALIZATIONS_SECTION_HEADER =
getDataTestSubjectSelector(VISUALIZATIONS_SECTION_HEADER_TEST_ID);
-export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_TREE =
- getDataTestSubjectSelector(ANALYZER_TREE_TEST_ID);
-export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW =
- getDataTestSubjectSelector(SESSION_PREVIEW_TEST_ID);
+export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_PREVIEW_CONTENT =
+ getDataTestSubjectSelector(ANALYZER_PREVIEW_CONTENT_TEST_ID);
+export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW_CONTENT =
+ getDataTestSubjectSelector(SESSION_PREVIEW_CONTENT_TEST_ID);
/* Response section */
diff --git a/x-pack/plugins/security_solution/cypress/tasks/expandable_flyout/alert_details_right_panel_overview_tab.ts b/x-pack/plugins/security_solution/cypress/tasks/expandable_flyout/alert_details_right_panel_overview_tab.ts
index f1f4f2a74d429..90b040845812b 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/expandable_flyout/alert_details_right_panel_overview_tab.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/expandable_flyout/alert_details_right_panel_overview_tab.ts
@@ -5,15 +5,17 @@
* 2.0.
*/
+import {
+ INSIGHTS_CORRELATIONS_TITLE_LINK_TEST_ID,
+ INSIGHTS_ENTITIES_TITLE_LINK_TEST_ID,
+ INSIGHTS_PREVALENCE_TITLE_LINK_TEST_ID,
+ INSIGHTS_THREAT_INTELLIGENCE_TITLE_LINK_TEST_ID,
+} from '../../../public/flyout/right/components/test_ids';
import {
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ABOUT_SECTION_HEADER,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_VISUALIZATIONS_SECTION_HEADER,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_SECTION_HEADER,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INVESTIGATION_SECTION_HEADER,
- DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_VIEW_ALL_ENTITIES_BUTTON,
- DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VIEW_ALL_BUTTON,
- DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON,
- DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_VIEW_ALL_BUTTON,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INVESTIGATION_GUIDE_BUTTON,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_TITLE,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_OPEN_RULE_PREVIEW_BUTTON,
@@ -53,47 +55,35 @@ export const toggleOverviewTabInsightsSection = () => {
};
/**
- * Click on the view all button under the right section, Insights, Entities
+ * Click on the header in the right section, Insights, Entities
*/
-export const clickEntitiesViewAllButton = () => {
- cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_VIEW_ALL_ENTITIES_BUTTON).scrollIntoView();
- cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_VIEW_ALL_ENTITIES_BUTTON)
- .should('be.visible')
- .click();
+export const navigateToEntitiesDetails = () => {
+ cy.get(INSIGHTS_ENTITIES_TITLE_LINK_TEST_ID).scrollIntoView();
+ cy.get(INSIGHTS_ENTITIES_TITLE_LINK_TEST_ID).should('be.visible').click();
};
/**
- * Click on the view all button under the right section, Insights, Threat Intelligence
+ * Click on the header in the right section, Insights, Threat Intelligence
*/
-export const clickThreatIntelligenceViewAllButton = () => {
- cy.get(
- DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON
- ).scrollIntoView();
- cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON)
- .should('be.visible')
- .click();
+export const navigateToThreatIntelligenceDetails = () => {
+ cy.get(INSIGHTS_THREAT_INTELLIGENCE_TITLE_LINK_TEST_ID).scrollIntoView();
+ cy.get(INSIGHTS_THREAT_INTELLIGENCE_TITLE_LINK_TEST_ID).should('be.visible').click();
};
/**
- * Click on the view all button under the right section, Insights, Correlations
+ * Click on the header in the right section, Insights, Correlations
*/
-export const clickCorrelationsViewAllButton = () => {
- cy.get(
- DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VIEW_ALL_BUTTON
- ).scrollIntoView();
- cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_VIEW_ALL_BUTTON)
- .should('be.visible')
- .click();
+export const navigateToCorrelationsDetails = () => {
+ cy.get(INSIGHTS_CORRELATIONS_TITLE_LINK_TEST_ID).scrollIntoView();
+ cy.get(INSIGHTS_CORRELATIONS_TITLE_LINK_TEST_ID).should('be.visible').click();
};
/**
* Click on the view all button under the right section, Insights, Prevalence
*/
-export const clickPrevalenceViewAllButton = () => {
- cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_VIEW_ALL_BUTTON).scrollIntoView();
- cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_PREVALENCE_VIEW_ALL_BUTTON)
- .should('be.visible')
- .click();
+export const navigateToPrevalenceDetails = () => {
+ cy.get(INSIGHTS_PREVALENCE_TITLE_LINK_TEST_ID).scrollIntoView();
+ cy.get(INSIGHTS_PREVALENCE_TITLE_LINK_TEST_ID).should('be.visible').click();
};
/* Visualizations section */
diff --git a/x-pack/plugins/security_solution/cypress/tasks/privileges.ts b/x-pack/plugins/security_solution/cypress/tasks/privileges.ts
index bd55745200b4f..b0489b14e8a8e 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/privileges.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/privileges.ts
@@ -63,6 +63,7 @@ export const secAll: Role = {
{
feature: {
siem: ['all'],
+ securitySolutionAssistant: ['all'],
securitySolutionCases: ['all'],
actions: ['all'],
actionsSimulators: ['all'],
@@ -94,6 +95,7 @@ export const secReadCasesAll: Role = {
{
feature: {
siem: ['read'],
+ securitySolutionAssistant: ['all'],
securitySolutionCases: ['all'],
actions: ['all'],
actionsSimulators: ['all'],
@@ -125,6 +127,7 @@ export const secAllCasesOnlyReadDelete: Role = {
{
feature: {
siem: ['all'],
+ securitySolutionAssistant: ['all'],
securitySolutionCases: ['cases_read', 'cases_delete'],
actions: ['all'],
actionsSimulators: ['all'],
@@ -156,6 +159,7 @@ export const secAllCasesNoDelete: Role = {
{
feature: {
siem: ['all'],
+ securitySolutionAssistant: ['all'],
securitySolutionCases: ['minimal_all'],
actions: ['all'],
actionsSimulators: ['all'],
diff --git a/x-pack/plugins/security_solution/public/assistant/use_assistant_availability/index.tsx b/x-pack/plugins/security_solution/public/assistant/use_assistant_availability/index.tsx
index fb58fb5509ba2..b2206157661b9 100644
--- a/x-pack/plugins/security_solution/public/assistant/use_assistant_availability/index.tsx
+++ b/x-pack/plugins/security_solution/public/assistant/use_assistant_availability/index.tsx
@@ -6,6 +6,8 @@
*/
import { useLicense } from '../../common/hooks/use_license';
+import { useKibana } from '../../common/lib/kibana';
+import { ASSISTANT_FEATURE_ID } from '../../../common/constants';
export interface UseAssistantAvailability {
// True when user is Enterprise. When false, the Assistant is disabled and unavailable
@@ -16,11 +18,11 @@ export interface UseAssistantAvailability {
export const useAssistantAvailability = (): UseAssistantAvailability => {
const isEnterprise = useLicense().isEnterprise();
+ const capabilities = useKibana().services.application.capabilities;
+ const isAssistantEnabled = capabilities[ASSISTANT_FEATURE_ID]?.['ai-assistant'] === true;
+
return {
isAssistantEnabled: isEnterprise,
- // TODO: RBAC check (https://github.com/elastic/security-team/issues/6932)
- // Leaving as a placeholder for RBAC as the same behavior will be required
- // When false, the Assistant is hidden and unavailable
- hasAssistantPrivilege: true,
+ hasAssistantPrivilege: isAssistantEnabled,
};
};
diff --git a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx
index ef8c68f9a25bb..44d9300a6d65e 100644
--- a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx
+++ b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx
@@ -35,7 +35,7 @@ import {
import type { FieldHook } from '../../shared_imports';
import { SUB_PLUGINS_REDUCER } from './utils';
import { createSecuritySolutionStorageMock, localStorageMock } from './mock_local_storage';
-import { CASES_FEATURE_ID } from '../../../common/constants';
+import { ASSISTANT_FEATURE_ID, CASES_FEATURE_ID } from '../../../common/constants';
import { UserPrivilegesProvider } from '../components/user_privileges/user_privileges_context';
const state: State = mockGlobalState;
@@ -125,6 +125,7 @@ const TestProvidersWithPrivilegesComponent: React.FC = ({
{
siem: { show: true, crud: true },
[CASES_FEATURE_ID]: { read_cases: true, crud_cases: false },
+ [ASSISTANT_FEATURE_ID]: { 'ai-assistant': true },
} as unknown as Capabilities
}
>
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.test.tsx
index 33e93c748f270..ed3f06c54e50e 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.test.tsx
@@ -113,6 +113,9 @@ jest.mock('../../../../common/lib/kibana', () => {
save: true,
show: true,
},
+ siem: {
+ 'ai-assistant': true,
+ },
},
},
data: {
diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details.tsx
index f0dfab8bb1cbd..85b497716b32d 100644
--- a/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details.tsx
@@ -21,7 +21,6 @@ import { useLeftPanelContext } from '../context';
import { useRouteSpy } from '../../../common/utils/route/use_route_spy';
import { SecurityPageName } from '../../../../common';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
-import { EntityPanel } from '../../right/components/entity_panel';
import { AlertsTable } from './correlations_details_alerts_table';
import { ERROR_MESSAGE, ERROR_TITLE } from '../../shared/translations';
import {
@@ -41,6 +40,7 @@ import {
SESSION_ALERTS_HEADING,
SOURCE_ALERTS_HEADING,
} from './translations';
+import { ExpandablePanel } from '../../shared/components/expandable_panel';
export const CORRELATIONS_TAB_ID = 'correlations-details';
@@ -105,56 +105,64 @@ export const CorrelationsDetails: React.FC = () => {
return (
<>
-
-
+
-
-
+
-
-
+
-
-
+
>
);
};
diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx
index 269aff686cf61..731bfeda95712 100644
--- a/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx
@@ -20,9 +20,9 @@ import {
EuiIcon,
} from '@elastic/eui';
import type { EuiBasicTableColumn } from '@elastic/eui';
+import { ExpandablePanel } from '../../shared/components/expandable_panel';
import type { RelatedUser } from '../../../../common/search_strategy/security_solution/related_entities/related_users';
import type { RiskSeverity } from '../../../../common/search_strategy';
-import { EntityPanel } from '../../right/components/entity_panel';
import { HostOverview } from '../../../overview/components/host_overview';
import { AnomalyTableProvider } from '../../../common/components/ml/anomaly/anomaly_table_provider';
import { InspectButton, InspectButtonContainer } from '../../../common/components/inspect';
@@ -210,12 +210,13 @@ export const HostDetails: React.FC = ({ hostName, timestamp })
{i18n.HOSTS_TITLE}
-
@@ -284,7 +285,7 @@ export const HostDetails: React.FC = ({ hostName, timestamp })
inspectIndex={0}
/>
-
+
>
);
};
diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/left/components/test_ids.ts
index f2ea803b53a9f..ea58b280a101e 100644
--- a/x-pack/plugins/security_solution/public/flyout/left/components/test_ids.ts
+++ b/x-pack/plugins/security_solution/public/flyout/left/components/test_ids.ts
@@ -55,16 +55,12 @@ export const HOST_DETAILS_INFO_TEST_ID = 'host-overview';
export const HOST_DETAILS_RELATED_USERS_TABLE_TEST_ID =
`${PREFIX}HostsDetailsRelatedUsersTable` as const;
-export const THREAT_INTELLIGENCE_DETAILS_TEST_ID = `${PREFIX}ThreatIntelligenceDetails` as const;
-export const PREVALENCE_DETAILS_TEST_ID = `${PREFIX}PrevalenceDetails` as const;
export const CORRELATIONS_DETAILS_TEST_ID = `${PREFIX}CorrelationsDetails` as const;
export const THREAT_INTELLIGENCE_DETAILS_ENRICHMENTS_TEST_ID = `threat-match-detected` as const;
export const THREAT_INTELLIGENCE_DETAILS_SPINNER_TEST_ID =
`${PREFIX}ThreatIntelligenceDetailsLoadingSpinner` as const;
-export const INVESTIGATION_TEST_ID = `${PREFIX}Investigation` as const;
-
export const CORRELATIONS_DETAILS_ERROR_TEST_ID = `${CORRELATIONS_DETAILS_TEST_ID}Error` as const;
export const CORRELATIONS_DETAILS_BY_ANCESTRY_TABLE_TEST_ID =
diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx
index bc0066f2488fd..ea55f811c341a 100644
--- a/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx
@@ -20,9 +20,9 @@ import {
EuiToolTip,
} from '@elastic/eui';
import type { EuiBasicTableColumn } from '@elastic/eui';
+import { ExpandablePanel } from '../../shared/components/expandable_panel';
import type { RelatedHost } from '../../../../common/search_strategy/security_solution/related_entities/related_hosts';
import type { RiskSeverity } from '../../../../common/search_strategy';
-import { EntityPanel } from '../../right/components/entity_panel';
import { UserOverview } from '../../../overview/components/user_overview';
import { AnomalyTableProvider } from '../../../common/components/ml/anomaly/anomaly_table_provider';
import { InspectButton, InspectButtonContainer } from '../../../common/components/inspect';
@@ -211,12 +211,16 @@ export const UserDetails: React.FC = ({ userName, timestamp })
{i18n.USERS_TITLE}
-
@@ -284,7 +288,7 @@ export const UserDetails: React.FC = ({ userName, timestamp })
inspectIndex={0}
/>
-
+
>
);
};
diff --git a/x-pack/plugins/security_solution/public/flyout/preview/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/preview/components/test_ids.ts
index 32a26d3f87db9..1c27fd7472fca 100644
--- a/x-pack/plugins/security_solution/public/flyout/preview/components/test_ids.ts
+++ b/x-pack/plugins/security_solution/public/flyout/preview/components/test_ids.ts
@@ -36,6 +36,5 @@ export const RULE_PREVIEW_ACTIONS_HEADER_TEST_ID = RULE_PREVIEW_ACTIONS_TEST_ID
export const RULE_PREVIEW_ACTIONS_CONTENT_TEST_ID = RULE_PREVIEW_ACTIONS_TEST_ID + CONTENT_TEST_ID;
export const RULE_PREVIEW_LOADING_TEST_ID =
'securitySolutionDocumentDetailsFlyoutRulePreviewLoadingSpinner';
-export const RULE_PREVIEW_HEADER_TEST_ID = 'securitySolutionDocumentDetailsFlyoutRulePreviewHeader';
export const RULE_PREVIEW_FOOTER_TEST_ID = 'securitySolutionDocumentDetailsFlyoutRulePreviewFooter';
export const RULE_PREVIEW_NAVIGATE_TO_RULE_TEST_ID = 'goToRuleDetails';
diff --git a/x-pack/plugins/security_solution/public/flyout/preview/components/translations.ts b/x-pack/plugins/security_solution/public/flyout/preview/components/translations.ts
index 8112b796c1d39..e3f3b1fd095fb 100644
--- a/x-pack/plugins/security_solution/public/flyout/preview/components/translations.ts
+++ b/x-pack/plugins/security_solution/public/flyout/preview/components/translations.ts
@@ -31,8 +31,3 @@ export const RULE_PREVIEW_ACTIONS_TEXT = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.rulePreviewActionsSectionText',
{ defaultMessage: 'Actions' }
);
-
-export const ENABLE_RULE_TEXT = i18n.translate(
- 'xpack.securitySolution.flyout.documentDetails.rulePreviewEnableRuleText',
- { defaultMessage: 'Enable' }
-);
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.test.tsx
index dc96b74bc52a6..8d691ad870892 100644
--- a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.test.tsx
@@ -13,7 +13,7 @@ import { mockContextValue } from '../mocks/mock_right_panel_context';
import { mockDataFormattedForFieldBrowser } from '../mocks/mock_context';
import { RightPanelContext } from '../context';
import { AnalyzerPreview } from './analyzer_preview';
-import { ANALYZER_PREVIEW_TEST_ID, ANALYZER_TREE_TEST_ID } from './test_ids';
+import { ANALYZER_PREVIEW_TEST_ID } from './test_ids';
import * as mock from '../mocks/mock_analyzer_data';
jest.mock('../../../common/containers/alerts/use_alert_prevalence_from_process_tree', () => ({
@@ -65,7 +65,6 @@ describe('', () => {
indices: ['rule-parameters-index'],
});
expect(wrapper.getByTestId(ANALYZER_PREVIEW_TEST_ID)).toBeInTheDocument();
- expect(wrapper.getByTestId(ANALYZER_TREE_TEST_ID)).toBeInTheDocument();
});
it('does not show analyzer preview when documentid and index are not present', () => {
@@ -88,6 +87,6 @@ describe('', () => {
documentId: '',
indices: [],
});
- expect(queryByTestId(ANALYZER_TREE_TEST_ID)).not.toBeInTheDocument();
+ expect(queryByTestId(ANALYZER_PREVIEW_TEST_ID)).not.toBeInTheDocument();
});
});
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.tsx
index 33b1c56e43e8a..e26ede68bc397 100644
--- a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_preview.tsx
@@ -6,7 +6,6 @@
*/
import React, { useEffect, useState } from 'react';
import { find } from 'lodash/fp';
-import { ANALYZER_PREVIEW_TEST_ID } from './test_ids';
import { useRightPanelContext } from '../context';
import { useAlertPrevalenceFromProcessTree } from '../../../common/containers/alerts/use_alert_prevalence_from_process_tree';
import type { StatsNode } from '../../../common/containers/alerts/use_alert_prevalence_from_process_tree';
@@ -50,19 +49,19 @@ export const AnalyzerPreview: React.FC = () => {
}
}, [statsNodes, setCache]);
+ if (!documentId || !index) {
+ return null;
+ }
+
return (
-
- {documentId && index && (
-
- )}
-
+
);
};
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_tree.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_tree.test.tsx
index cf05f61ace9eb..ce29b9959ff8c 100644
--- a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_tree.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_tree.test.tsx
@@ -8,10 +8,12 @@
import React from 'react';
import { render } from '@testing-library/react';
import {
- ANALYZER_TREE_TEST_ID,
- ANALYZER_TREE_LOADING_TEST_ID,
- ANALYZER_TREE_ERROR_TEST_ID,
- ANALYZER_TREE_VIEW_DETAILS_BUTTON_TEST_ID,
+ ANALYZER_PREVIEW_TOGGLE_ICON_TEST_ID,
+ ANALYZER_PREVIEW_TITLE_LINK_TEST_ID,
+ ANALYZER_PREVIEW_TITLE_ICON_TEST_ID,
+ ANALYZER_PREVIEW_CONTENT_TEST_ID,
+ ANALYZER_PREVIEW_TITLE_TEXT_TEST_ID,
+ ANALYZER_PREVIEW_LOADING_TEST_ID,
} from './test_ids';
import { ANALYZER_PREVIEW_TITLE } from './translations';
import * as mock from '../mocks/mock_analyzer_data';
@@ -51,10 +53,19 @@ const renderAnalyzerTree = (children: React.ReactNode) =>
);
describe('', () => {
+ it('should render wrapper component', () => {
+ const { getByTestId, queryByTestId } = renderAnalyzerTree();
+ expect(queryByTestId(ANALYZER_PREVIEW_TOGGLE_ICON_TEST_ID)).not.toBeInTheDocument();
+ expect(getByTestId(ANALYZER_PREVIEW_TITLE_LINK_TEST_ID)).toBeInTheDocument();
+ expect(getByTestId(ANALYZER_PREVIEW_TITLE_ICON_TEST_ID)).toBeInTheDocument();
+ expect(getByTestId(ANALYZER_PREVIEW_CONTENT_TEST_ID)).toBeInTheDocument();
+ expect(queryByTestId(ANALYZER_PREVIEW_TITLE_TEXT_TEST_ID)).not.toBeInTheDocument();
+ });
+
it('should render the component when data is passed', () => {
const { getByTestId, getByText } = renderAnalyzerTree();
expect(getByText(ANALYZER_PREVIEW_TITLE)).toBeInTheDocument();
- expect(getByTestId(ANALYZER_TREE_TEST_ID)).toBeInTheDocument();
+ expect(getByTestId(ANALYZER_PREVIEW_CONTENT_TEST_ID)).toBeInTheDocument();
});
it('should render blank when data is not passed', () => {
@@ -62,26 +73,23 @@ describe('', () => {
);
expect(queryByText(ANALYZER_PREVIEW_TITLE)).not.toBeInTheDocument();
- expect(queryByTestId(ANALYZER_TREE_TEST_ID)).not.toBeInTheDocument();
+ expect(queryByTestId(ANALYZER_PREVIEW_CONTENT_TEST_ID)).not.toBeInTheDocument();
});
it('should render loading spinner when loading is true', () => {
const { getByTestId } = renderAnalyzerTree();
- expect(getByTestId(ANALYZER_TREE_LOADING_TEST_ID)).toBeInTheDocument();
+ expect(getByTestId(ANALYZER_PREVIEW_LOADING_TEST_ID)).toBeInTheDocument();
});
- it('should display error message when error is true', () => {
- const { getByTestId, getByText } = renderAnalyzerTree(
-
- );
- expect(getByText('Unable to display analyzer preview.')).toBeInTheDocument();
- expect(getByTestId(ANALYZER_TREE_ERROR_TEST_ID)).toBeInTheDocument();
+ it('should not render when error is true', () => {
+ const { getByTestId } = renderAnalyzerTree();
+ expect(getByTestId(ANALYZER_PREVIEW_CONTENT_TEST_ID)).toBeEmptyDOMElement();
});
it('should navigate to left section Visualize tab when clicking on title', () => {
const { getByTestId } = renderAnalyzerTree();
- getByTestId(ANALYZER_TREE_VIEW_DETAILS_BUTTON_TEST_ID).click();
+ getByTestId(ANALYZER_PREVIEW_TITLE_LINK_TEST_ID).click();
expect(flyoutContextValue.openLeftPanel).toHaveBeenCalledWith({
id: LeftPanelKey,
path: LeftPanelVisualizeTabPath,
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_tree.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_tree.tsx
index 34b0274dd55af..87504dc05818f 100644
--- a/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_tree.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/right/components/analyzer_tree.tsx
@@ -5,26 +5,15 @@
* 2.0.
*/
import React, { useCallback, useMemo } from 'react';
-import {
- EuiPanel,
- EuiButtonEmpty,
- EuiTreeView,
- EuiLoadingSpinner,
- EuiEmptyPrompt,
-} from '@elastic/eui';
+import { EuiTreeView } from '@elastic/eui';
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
+import { ExpandablePanel } from '../../shared/components/expandable_panel';
import { useRightPanelContext } from '../context';
import { LeftPanelKey, LeftPanelVisualizeTabPath } from '../../left';
-import { ANALYZER_PREVIEW_TITLE, ANALYZER_PREVIEW_TEXT } from './translations';
-import {
- ANALYZER_TREE_TEST_ID,
- ANALYZER_TREE_LOADING_TEST_ID,
- ANALYZER_TREE_ERROR_TEST_ID,
- ANALYZER_TREE_VIEW_DETAILS_BUTTON_TEST_ID,
-} from './test_ids';
+import { ANALYZER_PREVIEW_TITLE } from './translations';
+import { ANALYZER_PREVIEW_TEST_ID } from './test_ids';
import type { StatsNode } from '../../../common/containers/alerts/use_alert_prevalence_from_process_tree';
import { getTreeNodes } from '../utils/analyzer_helpers';
-import { ERROR_TITLE, ERROR_MESSAGE } from '../../shared/translations';
export interface AnalyzerTreeProps {
/**
@@ -83,42 +72,24 @@ export const AnalyzerTree: React.FC = ({
});
}, [eventId, openLeftPanel, indexName, scopeId]);
- if (loading) {
- return ;
- }
-
- if (error) {
- return (
- {ERROR_TITLE(ANALYZER_PREVIEW_TEXT)}}
- body={{ERROR_MESSAGE(ANALYZER_PREVIEW_TEXT)}
}
- data-test-subj={ANALYZER_TREE_ERROR_TEST_ID}
- />
- );
- }
-
if (items && items.length !== 0) {
return (
-
-
-
- {ANALYZER_PREVIEW_TITLE}
-
-
-
-
+
+
+
);
}
return null;
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/correlations_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/correlations_overview.test.tsx
index dfd81606c10bd..b5be75716664a 100644
--- a/x-pack/plugins/security_solution/public/flyout/right/components/correlations_overview.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/right/components/correlations_overview.test.tsx
@@ -9,16 +9,18 @@ import React from 'react';
import { render } from '@testing-library/react';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { RightPanelContext } from '../context';
-import {
- INSIGHTS_CORRELATIONS_CONTENT_TEST_ID,
- INSIGHTS_CORRELATIONS_LOADING_TEST_ID,
- INSIGHTS_CORRELATIONS_TITLE_TEST_ID,
- INSIGHTS_CORRELATIONS_VIEW_ALL_BUTTON_TEST_ID,
-} from './test_ids';
import { TestProviders } from '../../../common/mock';
import { CorrelationsOverview } from './correlations_overview';
import { LeftPanelInsightsTabPath, LeftPanelKey } from '../../left';
import { useCorrelations } from '../../shared/hooks/use_correlations';
+import {
+ INSIGHTS_CORRELATIONS_CONTENT_TEST_ID,
+ INSIGHTS_CORRELATIONS_LOADING_TEST_ID,
+ INSIGHTS_CORRELATIONS_TITLE_ICON_TEST_ID,
+ INSIGHTS_CORRELATIONS_TITLE_LINK_TEST_ID,
+ INSIGHTS_CORRELATIONS_TITLE_TEXT_TEST_ID,
+ INSIGHTS_CORRELATIONS_TOGGLE_ICON_TEST_ID,
+} from './test_ids';
jest.mock('../../shared/hooks/use_correlations');
@@ -38,8 +40,22 @@ const renderCorrelationsOverview = (contextValue: RightPanelContext) => (
);
-describe('', () => {
- it('should show component with all rows in summary panel', () => {
+describe('', () => {
+ it('should render wrapper component', () => {
+ (useCorrelations as jest.Mock).mockReturnValue({
+ loading: false,
+ error: false,
+ data: [],
+ });
+
+ const { getByTestId, queryByTestId } = render(renderCorrelationsOverview(panelContextValue));
+ expect(queryByTestId(INSIGHTS_CORRELATIONS_TOGGLE_ICON_TEST_ID)).not.toBeInTheDocument();
+ expect(getByTestId(INSIGHTS_CORRELATIONS_TITLE_LINK_TEST_ID)).toBeInTheDocument();
+ expect(getByTestId(INSIGHTS_CORRELATIONS_TITLE_ICON_TEST_ID)).toBeInTheDocument();
+ expect(queryByTestId(INSIGHTS_CORRELATIONS_TITLE_TEXT_TEST_ID)).not.toBeInTheDocument();
+ });
+
+ it('should show component with all rows in expandable panel', () => {
(useCorrelations as jest.Mock).mockReturnValue({
loading: false,
error: false,
@@ -53,7 +69,7 @@ describe('', () => {
});
const { getByTestId } = render(renderCorrelationsOverview(panelContextValue));
- expect(getByTestId(INSIGHTS_CORRELATIONS_TITLE_TEST_ID)).toHaveTextContent('Correlations');
+ expect(getByTestId(INSIGHTS_CORRELATIONS_TITLE_LINK_TEST_ID)).toHaveTextContent('Correlations');
expect(getByTestId(INSIGHTS_CORRELATIONS_CONTENT_TEST_ID)).toHaveTextContent('1 related case');
expect(getByTestId(INSIGHTS_CORRELATIONS_CONTENT_TEST_ID)).toHaveTextContent(
'2 alerts related by ancestry'
@@ -64,7 +80,6 @@ describe('', () => {
expect(getByTestId(INSIGHTS_CORRELATIONS_CONTENT_TEST_ID)).toHaveTextContent(
'4 alerts related by session'
);
- expect(getByTestId(INSIGHTS_CORRELATIONS_VIEW_ALL_BUTTON_TEST_ID)).toBeInTheDocument();
});
it('should hide row if data is missing', () => {
@@ -93,8 +108,8 @@ describe('', () => {
dataCount: 0,
});
- const { container } = render(renderCorrelationsOverview(panelContextValue));
- expect(container).toBeEmptyDOMElement();
+ const { getByTestId } = render(renderCorrelationsOverview(panelContextValue));
+ expect(getByTestId(INSIGHTS_CORRELATIONS_CONTENT_TEST_ID)).toBeEmptyDOMElement();
});
it('should render loading if any rows are loading', () => {
@@ -136,7 +151,7 @@ describe('', () => {
);
- getByTestId(INSIGHTS_CORRELATIONS_VIEW_ALL_BUTTON_TEST_ID).click();
+ getByTestId(INSIGHTS_CORRELATIONS_TITLE_LINK_TEST_ID).click();
expect(flyoutContextValue.openLeftPanel).toHaveBeenCalledWith({
id: LeftPanelKey,
path: LeftPanelInsightsTabPath,
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/correlations_overview.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/correlations_overview.tsx
index 3936e80155a0d..92bf782f4988a 100644
--- a/x-pack/plugins/security_solution/public/flyout/right/components/correlations_overview.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/right/components/correlations_overview.tsx
@@ -6,14 +6,14 @@
*/
import React, { useCallback, useMemo } from 'react';
-import { EuiButtonEmpty, EuiFlexGroup, EuiPanel } from '@elastic/eui';
+import { EuiFlexGroup } from '@elastic/eui';
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
+import { ExpandablePanel } from '../../shared/components/expandable_panel';
import { InsightsSummaryRow } from './insights_summary_row';
import { useCorrelations } from '../../shared/hooks/use_correlations';
import { INSIGHTS_CORRELATIONS_TEST_ID } from './test_ids';
-import { InsightsSubSection } from './insights_subsection';
import { useRightPanelContext } from '../context';
-import { CORRELATIONS_TEXT, CORRELATIONS_TITLE, VIEW_ALL } from './translations';
+import { CORRELATIONS_TITLE } from './translations';
import { LeftPanelKey, LeftPanelInsightsTabPath } from '../../left';
/**
@@ -60,27 +60,19 @@ export const CorrelationsOverview: React.FC = () => {
);
return (
-
-
-
- {correlationRows}
-
-
-
- {VIEW_ALL(CORRELATIONS_TEXT)}
-
-
+
+ {correlationRows}
+
+
);
};
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/entities_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/entities_overview.test.tsx
index d059b5180abab..29bb8068281ae 100644
--- a/x-pack/plugins/security_solution/public/flyout/right/components/entities_overview.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/right/components/entities_overview.test.tsx
@@ -9,18 +9,40 @@ import React from 'react';
import { render } from '@testing-library/react';
import { RightPanelContext } from '../context';
import {
- ENTITIES_HEADER_TEST_ID,
- ENTITIES_USER_CONTENT_TEST_ID,
- ENTITIES_HOST_CONTENT_TEST_ID,
ENTITIES_HOST_OVERVIEW_TEST_ID,
ENTITIES_USER_OVERVIEW_TEST_ID,
+ INSIGHTS_ENTITIES_TOGGLE_ICON_TEST_ID,
+ INSIGHTS_ENTITIES_TITLE_LINK_TEST_ID,
+ INSIGHTS_ENTITIES_TITLE_ICON_TEST_ID,
+ INSIGHTS_ENTITIES_TITLE_TEXT_TEST_ID,
} from './test_ids';
import { EntitiesOverview } from './entities_overview';
import { TestProviders } from '../../../common/mock';
import { mockGetFieldsData } from '../mocks/mock_context';
describe('', () => {
- it('should render user and host by default', () => {
+ it('should render wrapper component', () => {
+ const contextValue = {
+ eventId: 'event id',
+ getFieldsData: mockGetFieldsData,
+ } as unknown as RightPanelContext;
+
+ const { getByTestId, queryByTestId } = render(
+
+
+
+
+
+ );
+
+ expect(queryByTestId(INSIGHTS_ENTITIES_TOGGLE_ICON_TEST_ID)).not.toBeInTheDocument();
+ expect(getByTestId(INSIGHTS_ENTITIES_TITLE_LINK_TEST_ID)).toBeInTheDocument();
+ expect(getByTestId(INSIGHTS_ENTITIES_TITLE_LINK_TEST_ID)).toHaveTextContent('Entities');
+ expect(getByTestId(INSIGHTS_ENTITIES_TITLE_ICON_TEST_ID)).toBeInTheDocument();
+ expect(queryByTestId(INSIGHTS_ENTITIES_TITLE_TEXT_TEST_ID)).not.toBeInTheDocument();
+ });
+
+ it('should render user and host', () => {
const contextValue = {
eventId: 'event id',
getFieldsData: mockGetFieldsData,
@@ -33,9 +55,8 @@ describe('', () => {
);
- expect(getByTestId(ENTITIES_HEADER_TEST_ID)).toHaveTextContent('Entities');
- expect(getByTestId(ENTITIES_USER_CONTENT_TEST_ID)).toBeInTheDocument();
- expect(getByTestId(ENTITIES_HOST_CONTENT_TEST_ID)).toBeInTheDocument();
+ expect(getByTestId(ENTITIES_USER_OVERVIEW_TEST_ID)).toBeInTheDocument();
+ expect(getByTestId(ENTITIES_HOST_OVERVIEW_TEST_ID)).toBeInTheDocument();
});
it('should only render user when host name is null', () => {
@@ -44,7 +65,7 @@ describe('', () => {
getFieldsData: (field: string) => (field === 'user.name' ? 'user1' : null),
} as unknown as RightPanelContext;
- const { queryByTestId, queryByText, getByTestId } = render(
+ const { queryByTestId, getByTestId } = render(
@@ -52,10 +73,8 @@ describe('', () => {
);
- expect(getByTestId(ENTITIES_USER_CONTENT_TEST_ID)).toBeInTheDocument();
- expect(queryByTestId(ENTITIES_HOST_CONTENT_TEST_ID)).not.toBeInTheDocument();
- expect(queryByText('user1')).toBeInTheDocument();
- expect(queryByTestId(ENTITIES_USER_OVERVIEW_TEST_ID)).toBeInTheDocument();
+ expect(getByTestId(ENTITIES_USER_OVERVIEW_TEST_ID)).toBeInTheDocument();
+ expect(queryByTestId(ENTITIES_HOST_OVERVIEW_TEST_ID)).not.toBeInTheDocument();
});
it('should only render host when user name is null', () => {
@@ -64,7 +83,7 @@ describe('', () => {
getFieldsData: (field: string) => (field === 'host.name' ? 'host1' : null),
} as unknown as RightPanelContext;
- const { queryByTestId, queryByText, getByTestId } = render(
+ const { queryByTestId, getByTestId } = render(
@@ -72,10 +91,8 @@ describe('', () => {
);
- expect(getByTestId(ENTITIES_HOST_CONTENT_TEST_ID)).toBeInTheDocument();
- expect(queryByTestId(ENTITIES_USER_CONTENT_TEST_ID)).not.toBeInTheDocument();
- expect(queryByText('host1')).toBeInTheDocument();
- expect(queryByTestId(ENTITIES_HOST_OVERVIEW_TEST_ID)).toBeInTheDocument();
+ expect(getByTestId(ENTITIES_HOST_OVERVIEW_TEST_ID)).toBeInTheDocument();
+ expect(queryByTestId(ENTITIES_USER_OVERVIEW_TEST_ID)).not.toBeInTheDocument();
});
it('should not render if both host name and user name are null/blank', () => {
@@ -84,7 +101,7 @@ describe('', () => {
getFieldsData: (field: string) => {},
} as unknown as RightPanelContext;
- const { queryByTestId } = render(
+ const { container } = render(
@@ -92,9 +109,7 @@ describe('', () => {
);
- expect(queryByTestId(ENTITIES_HEADER_TEST_ID)).not.toBeInTheDocument();
- expect(queryByTestId(ENTITIES_HOST_CONTENT_TEST_ID)).not.toBeInTheDocument();
- expect(queryByTestId(ENTITIES_USER_CONTENT_TEST_ID)).not.toBeInTheDocument();
+ expect(container).toBeEmptyDOMElement();
});
it('should not render if eventId is null', () => {
@@ -103,7 +118,7 @@ describe('', () => {
getFieldsData: (field: string) => {},
} as unknown as RightPanelContext;
- const { queryByTestId } = render(
+ const { container } = render(
@@ -111,6 +126,6 @@ describe('', () => {
);
- expect(queryByTestId(ENTITIES_HEADER_TEST_ID)).not.toBeInTheDocument();
+ expect(container).toBeEmptyDOMElement();
});
});
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/entities_overview.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/entities_overview.tsx
index b0a8d5c2faeb2..3a6c58caeb54f 100644
--- a/x-pack/plugins/security_solution/public/flyout/right/components/entities_overview.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/right/components/entities_overview.tsx
@@ -6,26 +6,17 @@
*/
import React, { useCallback } from 'react';
-import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle, EuiButtonEmpty } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
+import { ExpandablePanel } from '../../shared/components/expandable_panel';
import { useRightPanelContext } from '../context';
-import {
- ENTITIES_HEADER_TEST_ID,
- ENTITIES_CONTENT_TEST_ID,
- ENTITIES_HOST_CONTENT_TEST_ID,
- ENTITIES_USER_CONTENT_TEST_ID,
- ENTITIES_VIEW_ALL_BUTTON_TEST_ID,
-} from './test_ids';
-import { ENTITIES_TITLE, ENTITIES_TEXT, VIEW_ALL } from './translations';
-import { EntityPanel } from './entity_panel';
+import { INSIGHTS_ENTITIES_TEST_ID } from './test_ids';
+import { ENTITIES_TITLE } from './translations';
import { getField } from '../../shared/utils';
import { HostEntityOverview } from './host_entity_overview';
import { UserEntityOverview } from './user_entity_overview';
import { LeftPanelKey, LeftPanelInsightsTabPath } from '../../left';
-const USER_ICON = 'user';
-const HOST_ICON = 'storage';
-
/**
* Entities section under Insights section, overview tab. It contains a preview of host and user information.
*/
@@ -53,43 +44,28 @@ export const EntitiesOverview: React.FC = () => {
return (
<>
-
- {ENTITIES_TITLE}
-
-
-
- {userName && (
-
-
+
+
+ {userName && (
+
-
-
- )}
- {hostName && (
-
-
+
+ )}
+
+ {hostName && (
+
-
-
- )}
-
- {VIEW_ALL(ENTITIES_TEXT)}
-
-
+
+ )}
+
+
>
);
};
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/entity_panel.stories.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/entity_panel.stories.tsx
deleted file mode 100644
index 183d4c4b643b3..0000000000000
--- a/x-pack/plugins/security_solution/public/flyout/right/components/entity_panel.stories.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import type { Story } from '@storybook/react';
-import { EuiIcon } from '@elastic/eui';
-import { EntityPanel } from './entity_panel';
-
-export default {
- component: EntityPanel,
- title: 'Flyout/EntityPanel',
-};
-
-const defaultProps = {
- title: 'title',
- iconType: 'storage',
-};
-const headerContent = ;
-
-const children = {'test content'}
;
-
-export const Default: Story = () => {
- return {children};
-};
-
-export const DefaultWithHeaderContent: Story = () => {
- return (
-
- {children}
-
- );
-};
-
-export const Expandable: Story = () => {
- return (
-
- {children}
-
- );
-};
-
-export const ExpandableDefaultOpen: Story = () => {
- return (
-
- {children}
-
- );
-};
-
-export const EmptyDefault: Story = () => {
- return ;
-};
-
-export const EmptyDefaultExpanded: Story = () => {
- return ;
-};
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/entity_panel.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/entity_panel.test.tsx
deleted file mode 100644
index 5eedc99cf5e61..0000000000000
--- a/x-pack/plugins/security_solution/public/flyout/right/components/entity_panel.test.tsx
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import { render } from '@testing-library/react';
-import { EntityPanel } from './entity_panel';
-import {
- ENTITY_PANEL_TOGGLE_BUTTON_TEST_ID,
- ENTITY_PANEL_HEADER_TEST_ID,
- ENTITY_PANEL_HEADER_LEFT_SECTION_TEST_ID,
- ENTITY_PANEL_HEADER_RIGHT_SECTION_TEST_ID,
- ENTITY_PANEL_CONTENT_TEST_ID,
-} from './test_ids';
-import { ThemeProvider } from 'styled-components';
-import { getMockTheme } from '../../../common/lib/kibana/kibana_react.mock';
-
-const mockTheme = getMockTheme({ eui: { euiColorMediumShade: '#ece' } });
-const ENTITY_PANEL_TEST_ID = 'entityPanel';
-const defaultProps = {
- title: 'test',
- iconType: 'storage',
- 'data-test-subj': ENTITY_PANEL_TEST_ID,
-};
-const children = {'test content'}
;
-
-describe('', () => {
- describe('panel is not expandable by default', () => {
- it('should render non-expandable panel by default', () => {
- const { getByTestId, queryByTestId } = render(
-
- {children}
-
- );
- expect(getByTestId(ENTITY_PANEL_TEST_ID)).toBeInTheDocument();
- expect(getByTestId(ENTITY_PANEL_HEADER_TEST_ID)).toBeInTheDocument();
- expect(getByTestId(ENTITY_PANEL_CONTENT_TEST_ID)).toHaveTextContent('test content');
- expect(queryByTestId(ENTITY_PANEL_TOGGLE_BUTTON_TEST_ID)).not.toBeInTheDocument();
- });
-
- it('should only render left section of panel header when headerContent is not passed', () => {
- const { getByTestId, queryByTestId } = render(
-
- {children}
-
- );
- expect(getByTestId(ENTITY_PANEL_HEADER_LEFT_SECTION_TEST_ID)).toHaveTextContent('test');
- expect(queryByTestId(ENTITY_PANEL_HEADER_RIGHT_SECTION_TEST_ID)).not.toBeInTheDocument();
- });
-
- it('should render header properly when headerContent is available', () => {
- const { getByTestId } = render(
-
- {'test header content'}>}>
- {children}
-
-
- );
- expect(getByTestId(ENTITY_PANEL_HEADER_LEFT_SECTION_TEST_ID)).toBeInTheDocument();
- expect(getByTestId(ENTITY_PANEL_HEADER_RIGHT_SECTION_TEST_ID)).toBeInTheDocument();
- });
-
- it('should not render content when content is null', () => {
- const { queryByTestId } = render(
-
-
-
- );
-
- expect(queryByTestId(ENTITY_PANEL_CONTENT_TEST_ID)).not.toBeInTheDocument();
- expect(queryByTestId(ENTITY_PANEL_TOGGLE_BUTTON_TEST_ID)).not.toBeInTheDocument();
- });
- });
-
- describe('panel is expandable', () => {
- it('should render panel with toggle and collapsed by default', () => {
- const { getByTestId, queryByTestId } = render(
-
-
- {children}
-
-
- );
- expect(getByTestId(ENTITY_PANEL_TEST_ID)).toBeInTheDocument();
- expect(getByTestId(ENTITY_PANEL_HEADER_TEST_ID)).toHaveTextContent('test');
- expect(queryByTestId(ENTITY_PANEL_CONTENT_TEST_ID)).not.toBeInTheDocument();
- });
-
- it('click toggle button should expand the panel', () => {
- const { getByTestId } = render(
-
-
- {children}
-
-
- );
-
- const toggle = getByTestId(ENTITY_PANEL_TOGGLE_BUTTON_TEST_ID);
- expect(toggle.firstChild).toHaveAttribute('data-euiicon-type', 'arrowRight');
- toggle.click();
-
- expect(getByTestId(ENTITY_PANEL_CONTENT_TEST_ID)).toHaveTextContent('test content');
- expect(toggle.firstChild).toHaveAttribute('data-euiicon-type', 'arrowDown');
- });
-
- it('should not render toggle or content when content is null', () => {
- const { queryByTestId } = render(
-
-
-
- );
- expect(queryByTestId(ENTITY_PANEL_TOGGLE_BUTTON_TEST_ID)).not.toBeInTheDocument();
- expect(queryByTestId(ENTITY_PANEL_CONTENT_TEST_ID)).not.toBeInTheDocument();
- });
- });
-
- describe('panel is expandable and expanded by default', () => {
- it('should render header and content', () => {
- const { getByTestId } = render(
-
-
- {children}
-
-
- );
- expect(getByTestId(ENTITY_PANEL_TEST_ID)).toBeInTheDocument();
- expect(getByTestId(ENTITY_PANEL_HEADER_TEST_ID)).toHaveTextContent('test');
- expect(getByTestId(ENTITY_PANEL_CONTENT_TEST_ID)).toHaveTextContent('test content');
- expect(getByTestId(ENTITY_PANEL_TOGGLE_BUTTON_TEST_ID)).toBeInTheDocument();
- });
-
- it('click toggle button should collapse the panel', () => {
- const { getByTestId, queryByTestId } = render(
-
-
- {children}
-
-
- );
-
- const toggle = getByTestId(ENTITY_PANEL_TOGGLE_BUTTON_TEST_ID);
- expect(toggle.firstChild).toHaveAttribute('data-euiicon-type', 'arrowDown');
- expect(getByTestId(ENTITY_PANEL_CONTENT_TEST_ID)).toBeInTheDocument();
-
- toggle.click();
- expect(toggle.firstChild).toHaveAttribute('data-euiicon-type', 'arrowRight');
- expect(queryByTestId(ENTITY_PANEL_CONTENT_TEST_ID)).not.toBeInTheDocument();
- });
-
- it('should not render content when content is null', () => {
- const { queryByTestId } = render(
-
-
-
- );
- expect(queryByTestId(ENTITY_PANEL_TOGGLE_BUTTON_TEST_ID)).not.toBeInTheDocument();
- expect(queryByTestId(ENTITY_PANEL_CONTENT_TEST_ID)).not.toBeInTheDocument();
- });
- });
-});
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/entity_panel.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/entity_panel.tsx
deleted file mode 100644
index d095bf72e4c39..0000000000000
--- a/x-pack/plugins/security_solution/public/flyout/right/components/entity_panel.tsx
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { useMemo, useState, useCallback } from 'react';
-import {
- EuiButtonIcon,
- EuiSplitPanel,
- EuiText,
- EuiFlexGroup,
- EuiFlexItem,
- EuiTitle,
- EuiPanel,
- EuiIcon,
-} from '@elastic/eui';
-import styled from 'styled-components';
-import {
- ENTITY_PANEL_TOGGLE_BUTTON_TEST_ID,
- ENTITY_PANEL_HEADER_TEST_ID,
- ENTITY_PANEL_HEADER_LEFT_SECTION_TEST_ID,
- ENTITY_PANEL_HEADER_RIGHT_SECTION_TEST_ID,
- ENTITY_PANEL_CONTENT_TEST_ID,
-} from './test_ids';
-
-const PanelHeaderRightSectionWrapper = styled(EuiFlexItem)`
- margin-right: ${({ theme }) => theme.eui.euiSizeM};
-`;
-
-const IconWrapper = styled(EuiIcon)`
- margin: ${({ theme }) => theme.eui.euiSizeS} 0;
-`;
-
-export interface EntityPanelProps {
- /**
- * String value of the title to be displayed in the header of panel
- */
- title: string;
- /**
- * Icon string for displaying the specified icon in the header
- */
- iconType: string;
- /**
- * Boolean to determine the panel to be collapsable (with toggle)
- */
- expandable?: boolean;
- /**
- * Boolean to allow the component to be expanded or collapsed on first render
- */
- expanded?: boolean;
- /**
- Optional content and actions to be displayed on the right side of header
- */
- headerContent?: React.ReactNode;
- /**
- Data test subject string for testing
- */
- ['data-test-subj']?: string;
-}
-
-/**
- * Panel component to display user or host information.
- */
-export const EntityPanel: React.FC = ({
- title,
- iconType,
- children,
- expandable = false,
- expanded = false,
- headerContent,
- 'data-test-subj': dataTestSub,
-}) => {
- const [toggleStatus, setToggleStatus] = useState(expanded);
- const toggleQuery = useCallback(() => {
- setToggleStatus(!toggleStatus);
- }, [setToggleStatus, toggleStatus]);
-
- const toggleIcon = useMemo(
- () => (
-
- ),
- [toggleStatus, toggleQuery]
- );
-
- const headerLeftSection = useMemo(
- () => (
-
-
- {expandable && children && toggleIcon}
-
-
-
-
-
- {title}
-
-
-
-
- ),
- [title, children, toggleIcon, expandable, iconType]
- );
-
- const headerRightSection = useMemo(
- () =>
- headerContent && (
-
- {headerContent}
-
- ),
- [headerContent]
- );
-
- const showContent = useMemo(() => {
- if (!children) {
- return false;
- }
- return !expandable || (expandable && toggleStatus);
- }, [children, expandable, toggleStatus]);
-
- return (
-
-
-
- {headerLeftSection}
- {headerRightSection}
-
-
- {showContent && (
-
- {children}
-
- )}
-
- );
-};
-
-EntityPanel.displayName = 'EntityPanel';
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/host_entity_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/host_entity_overview.test.tsx
index 4515e011d3790..c7cd137808c18 100644
--- a/x-pack/plugins/security_solution/public/flyout/right/components/host_entity_overview.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/right/components/host_entity_overview.test.tsx
@@ -12,9 +12,15 @@ import { useRiskScore } from '../../../explore/containers/risk_score';
import { useHostDetails } from '../../../explore/hosts/containers/hosts/details';
import {
ENTITIES_HOST_OVERVIEW_IP_TEST_ID,
+ ENTITIES_HOST_OVERVIEW_LINK_TEST_ID,
ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID,
TECHNICAL_PREVIEW_ICON_TEST_ID,
} from './test_ids';
+import { RightPanelContext } from '../context';
+import { mockContextValue } from '../mocks/mock_right_panel_context';
+import { mockDataFormattedForFieldBrowser } from '../mocks/mock_context';
+import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
+import { LeftPanelInsightsTabPath, LeftPanelKey } from '../../left';
const hostName = 'host';
const ip = '10.200.000.000';
@@ -24,6 +30,15 @@ const selectedPatterns = 'alerts';
const hostData = { host: { ip: [ip] } };
const riskLevel = [{ host: { risk: { calculated_level: 'Medium' } } }];
+const panelContextValue = {
+ ...mockContextValue,
+ dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser,
+};
+
+const flyoutContextValue = {
+ openLeftPanel: jest.fn(),
+} as unknown as ExpandableFlyoutContext;
+
const mockUseGlobalTime = jest.fn().mockReturnValue({ from, to });
jest.mock('../../../common/containers/use_global_time', () => {
return {
@@ -52,7 +67,9 @@ describe('', () => {
const { getByTestId } = render(
-
+
+
+
);
@@ -67,7 +84,9 @@ describe('', () => {
const { getByTestId } = render(
-
+
+
+
);
expect(getByTestId(ENTITIES_HOST_OVERVIEW_IP_TEST_ID)).toHaveTextContent('—');
@@ -82,7 +101,9 @@ describe('', () => {
mockUseRiskScore.mockReturnValue({ data: riskLevel, isAuthorized: false });
const { getByTestId, queryByTestId } = render(
-
+
+
+
);
@@ -95,12 +116,40 @@ describe('', () => {
mockUseRiskScore.mockReturnValue({ data: null, isAuthorized: false });
const { getByTestId, queryByTestId } = render(
-
+
+
+
);
expect(getByTestId(ENTITIES_HOST_OVERVIEW_IP_TEST_ID)).toHaveTextContent('—');
expect(queryByTestId(TECHNICAL_PREVIEW_ICON_TEST_ID)).not.toBeInTheDocument();
});
+
+ it('should navigate to left panel entities tab when clicking on title', () => {
+ mockUseHostDetails.mockReturnValue([false, { hostDetails: hostData }]);
+ mockUseRiskScore.mockReturnValue({ data: riskLevel, isAuthorized: true });
+
+ const { getByTestId } = render(
+
+
+
+
+
+
+
+ );
+
+ getByTestId(ENTITIES_HOST_OVERVIEW_LINK_TEST_ID).click();
+ expect(flyoutContextValue.openLeftPanel).toHaveBeenCalledWith({
+ id: LeftPanelKey,
+ path: LeftPanelInsightsTabPath,
+ params: {
+ id: panelContextValue.eventId,
+ indexName: panelContextValue.indexName,
+ scopeId: panelContextValue.scopeId,
+ },
+ });
+ });
});
});
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/host_entity_overview.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/host_entity_overview.tsx
index 14a72804ead1f..d32df4f205088 100644
--- a/x-pack/plugins/security_solution/public/flyout/right/components/host_entity_overview.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/right/components/host_entity_overview.tsx
@@ -5,10 +5,20 @@
* 2.0.
*/
-import React, { useMemo } from 'react';
-import { EuiFlexGroup, EuiFlexItem, EuiBetaBadge } from '@elastic/eui';
+import React, { useCallback, useMemo } from 'react';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiBetaBadge,
+ EuiLink,
+ EuiIcon,
+ useEuiTheme,
+ useEuiFontSize,
+} from '@elastic/eui';
+import { css } from '@emotion/css';
import { getOr } from 'lodash/fp';
-import styled from 'styled-components';
+import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
+import { useRightPanelContext } from '../context';
import type { DescriptionList } from '../../../../common/utility_types';
import {
buildHostNamesFilter,
@@ -32,11 +42,11 @@ import {
ENTITIES_HOST_OVERVIEW_TEST_ID,
ENTITIES_HOST_OVERVIEW_IP_TEST_ID,
ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID,
+ ENTITIES_HOST_OVERVIEW_LINK_TEST_ID,
} from './test_ids';
+import { LeftPanelInsightsTabPath, LeftPanelKey } from '../../left';
-const StyledEuiBetaBadge = styled(EuiBetaBadge)`
- margin-left: ${({ theme }) => theme.eui.euiSizeXS};
-`;
+const HOST_ICON = 'storage';
const CONTEXT_ID = `flyout-host-entity-overview`;
export interface HostEntityOverviewProps {
@@ -50,6 +60,20 @@ export interface HostEntityOverviewProps {
* Host preview content for the entities preview in right flyout. It contains ip addresses and risk classification
*/
export const HostEntityOverview: React.FC = ({ hostName }) => {
+ const { eventId, indexName, scopeId } = useRightPanelContext();
+ const { openLeftPanel } = useExpandableFlyoutContext();
+ const goToEntitiesTab = useCallback(() => {
+ openLeftPanel({
+ id: LeftPanelKey,
+ path: LeftPanelInsightsTabPath,
+ params: {
+ id: eventId,
+ indexName,
+ scopeId,
+ },
+ });
+ }, [eventId, openLeftPanel, indexName, scopeId]);
+
const { from, to } = useGlobalTime();
const { selectedPatterns } = useSourcererDataView();
@@ -80,6 +104,9 @@ export const HostEntityOverview: React.FC = ({ hostName
endDate: to,
});
+ const { euiTheme } = useEuiTheme();
+ const xsFontSize = useEuiFontSize('xs').fontSize;
+
const [hostRiskLevel] = useMemo(() => {
const hostRiskData = hostRisk && hostRisk.length > 0 ? hostRisk[0] : undefined;
return [
@@ -87,7 +114,10 @@ export const HostEntityOverview: React.FC = ({ hostName
title: (
<>
{i18n.HOST_RISK_CLASSIFICATION}
- = ({ hostName
),
},
];
- }, [hostRisk]);
+ }, [euiTheme.size.xs, hostRisk]);
const descriptionList: DescriptionList[] = useMemo(
() => [
@@ -130,20 +160,43 @@ export const HostEntityOverview: React.FC = ({ hostName
);
return (
-
+
-
+
+
+
+
+
+
+ {hostName}
+
+
+
- {isAuthorized && (
-
- )}
+
+
+
+
+
+ {isAuthorized && (
+
+ )}
+
+
);
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_section.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_section.tsx
index 7e78e9f121a12..86d0447a5a590 100644
--- a/x-pack/plugins/security_solution/public/flyout/right/components/insights_section.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/right/components/insights_section.tsx
@@ -6,6 +6,7 @@
*/
import React from 'react';
+import { EuiSpacer } from '@elastic/eui';
import { CorrelationsOverview } from './correlations_overview';
import { PrevalenceOverview } from './prevalence_overview';
import { ThreatIntelligenceOverview } from './threat_intelligence_overview';
@@ -28,8 +29,11 @@ export const InsightsSection: React.FC = ({ expanded = fal
return (
+
+
+
);
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.stories.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.stories.tsx
deleted file mode 100644
index c1fc9dfe8a7f8..0000000000000
--- a/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.stories.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import type { Story } from '@storybook/react';
-import { InsightsSubSection } from './insights_subsection';
-
-export default {
- component: InsightsSubSection,
- title: 'Flyout/InsightsSubSection',
-};
-
-const title = 'Title';
-const children = {'hello'}
;
-
-export const Basic: Story = () => {
- return {children};
-};
-
-export const Loading: Story = () => {
- return (
-
- {null}
-
- );
-};
-
-export const NoTitle: Story = () => {
- return {children};
-};
-
-export const NoChildren: Story = () => {
- return {null};
-};
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.test.tsx
deleted file mode 100644
index 271953c8e8105..0000000000000
--- a/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.test.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import { render } from '@testing-library/react';
-import { InsightsSubSection } from './insights_subsection';
-
-const title = 'Title';
-const dataTestSubj = 'test';
-const children = {'hello'}
;
-
-describe('', () => {
- it('should render children component', () => {
- const { getByTestId } = render(
-
- {children}
-
- );
-
- const titleDataTestSubj = `${dataTestSubj}Title`;
- const contentDataTestSubj = `${dataTestSubj}Content`;
-
- expect(getByTestId(titleDataTestSubj)).toHaveTextContent(title);
- expect(getByTestId(contentDataTestSubj)).toBeInTheDocument();
- });
-
- it('should render loading component', () => {
- const { getByTestId } = render(
-
- {children}
-
- );
-
- const loadingDataTestSubj = `${dataTestSubj}Loading`;
- expect(getByTestId(loadingDataTestSubj)).toBeInTheDocument();
- });
-
- it('should render null if error', () => {
- const { container } = render(
-
- {children}
-
- );
-
- expect(container).toBeEmptyDOMElement();
- });
-
- it('should render null if no title', () => {
- const { container } = render({children});
-
- expect(container).toBeEmptyDOMElement();
- });
-
- it('should render null if no children', () => {
- const { container } = render(
-
- {null}
-
- );
-
- expect(container).toBeEmptyDOMElement();
- });
-});
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.tsx
deleted file mode 100644
index 5993d2d7555c3..0000000000000
--- a/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSpacer, EuiTitle } from '@elastic/eui';
-
-export interface InsightsSubSectionProps {
- /**
- * Renders a loading spinner if true
- */
- loading?: boolean;
- /**
- * Returns a null component if true
- */
- error?: boolean;
- /**
- * Title at the top of the component
- */
- title: string;
- /**
- * Content of the component
- */
- children: React.ReactNode;
- /**
- * Prefix data-test-subj to use for the elements
- */
- ['data-test-subj']?: string;
-}
-
-/**
- * Presentational component to handle loading and error in the subsections of the Insights section.
- * Should be used for Entities, Threat Intelligence, Prevalence, Correlations and Results
- */
-export const InsightsSubSection: React.FC = ({
- loading = false,
- error = false,
- title,
- 'data-test-subj': dataTestSubj,
- children,
-}) => {
- // showing the loading in this component as well as in SummaryPanel because we're hiding the entire section if no data
- const loadingDataTestSubj = `${dataTestSubj}Loading`;
- if (loading) {
- return (
-
-
-
-
-
- );
- }
-
- // hide everything
- if (error || !title || !children) {
- return null;
- }
-
- const titleDataTestSubj = `${dataTestSubj}Title`;
- const contentDataTestSubj = `${dataTestSubj}Content`;
-
- return (
- <>
-
- {title}
-
-
-
- {children}
-
- >
- );
-};
-
-InsightsSubSection.displayName = 'InsightsSubSection';
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/investigation_section.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/investigation_section.tsx
index 2c8de1e8aa71f..b0b021d1bd60c 100644
--- a/x-pack/plugins/security_solution/public/flyout/right/components/investigation_section.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/right/components/investigation_section.tsx
@@ -23,7 +23,7 @@ export interface DescriptionSectionProps {
/**
* Most top section of the overview tab. It contains the description, reason and mitre attack information (for a document of type alert).
*/
-export const InvestigationSection: VFC = ({ expanded = false }) => {
+export const InvestigationSection: VFC = ({ expanded = true }) => {
return (
+ ({
+ eventId,
+ indexName: 'indexName',
+ browserFields,
+ dataFormattedForFieldBrowser,
+ scopeId: 'scopeId',
+ } as unknown as RightPanelContext);
const renderPrevalenceOverview = (contextValue: RightPanelContext) => (
@@ -44,7 +54,7 @@ const renderPrevalenceOverview = (contextValue: RightPanelContext) => (
);
describe('', () => {
- it('should render PrevalenceOverviewRows', () => {
+ it('should render wrapper component', () => {
(useFetchFieldValuePairWithAggregation as jest.Mock).mockReturnValue({
loading: false,
error: false,
@@ -55,35 +65,62 @@ describe('', () => {
error: false,
count: 10,
});
- (usePrevalence as jest.Mock).mockReturnValue({
- empty: false,
- prevalenceRows: [
- ,
- ],
+ (usePrevalence as jest.Mock).mockReturnValue([]);
+
+ const { getByTestId, queryByTestId } = render(
+ renderPrevalenceOverview(panelContextValue('eventId', {}, []))
+ );
+ expect(queryByTestId(INSIGHTS_PREVALENCE_TOGGLE_ICON_TEST_ID)).not.toBeInTheDocument();
+ expect(getByTestId(INSIGHTS_PREVALENCE_TITLE_LINK_TEST_ID)).toBeInTheDocument();
+ expect(getByTestId(INSIGHTS_PREVALENCE_TITLE_ICON_TEST_ID)).toBeInTheDocument();
+ expect(queryByTestId(INSIGHTS_PREVALENCE_TITLE_TEXT_TEST_ID)).not.toBeInTheDocument();
+ });
+
+ it('should render component', () => {
+ (useFetchFieldValuePairWithAggregation as jest.Mock).mockReturnValue({
+ loading: false,
+ error: false,
+ count: 1,
});
+ (useFetchUniqueByField as jest.Mock).mockReturnValue({
+ loading: false,
+ error: false,
+ count: 10,
+ });
+ (usePrevalence as jest.Mock).mockReturnValue([
+ ,
+ ]);
- const titleDataTestSubj = `${INSIGHTS_PREVALENCE_TEST_ID}Title`;
- const iconDataTestSubj = 'testIcon';
- const valueDataTestSubj = 'testValue';
+ const { getByTestId } = render(renderPrevalenceOverview(panelContextValue('eventId', {}, [])));
- const { getByTestId } = render(renderPrevalenceOverview(panelContextValue));
+ expect(getByTestId(INSIGHTS_PREVALENCE_TITLE_LINK_TEST_ID)).toHaveTextContent('Prevalence');
- expect(getByTestId(titleDataTestSubj)).toBeInTheDocument();
+ const iconDataTestSubj = 'testIcon';
+ const valueDataTestSubj = 'testValue';
expect(getByTestId(iconDataTestSubj)).toBeInTheDocument();
expect(getByTestId(valueDataTestSubj)).toBeInTheDocument();
});
- it('should render null if no rows are rendered', () => {
- (usePrevalence as jest.Mock).mockReturnValue({
- empty: true,
- prevalenceRows: [],
- });
+ it('should render null if eventId is null', () => {
+ (usePrevalence as jest.Mock).mockReturnValue([]);
- const { container } = render(renderPrevalenceOverview(panelContextValue));
+ const { container } = render(renderPrevalenceOverview(panelContextValue(null, {}, [])));
+
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it('should render null if browserFields is null', () => {
+ (usePrevalence as jest.Mock).mockReturnValue([]);
+
+ const { container } = render(renderPrevalenceOverview(panelContextValue('eventId', null, [])));
+
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it('should render null if dataFormattedForFieldBrowser is null', () => {
+ (usePrevalence as jest.Mock).mockReturnValue([]);
+
+ const { container } = render(renderPrevalenceOverview(panelContextValue('eventId', {}, null)));
expect(container).toBeEmptyDOMElement();
});
@@ -99,16 +136,9 @@ describe('', () => {
error: false,
count: 10,
});
- (usePrevalence as jest.Mock).mockReturnValue({
- empty: false,
- prevalenceRows: [
- ,
- ],
- });
+ (usePrevalence as jest.Mock).mockReturnValue([
+ ,
+ ]);
const flyoutContextValue = {
openLeftPanel: jest.fn(),
} as unknown as ExpandableFlyoutContext;
@@ -116,21 +146,21 @@ describe('', () => {
const { getByTestId } = render(
-
+
);
- getByTestId(`${INSIGHTS_PREVALENCE_TEST_ID}ViewAllButton`).click();
+ getByTestId(INSIGHTS_PREVALENCE_TITLE_LINK_TEST_ID).click();
expect(flyoutContextValue.openLeftPanel).toHaveBeenCalledWith({
id: LeftPanelKey,
path: LeftPanelInsightsTabPath,
params: {
- id: panelContextValue.eventId,
- indexName: panelContextValue.indexName,
- scopeId: panelContextValue.scopeId,
+ id: 'eventId',
+ indexName: 'indexName',
+ scopeId: 'scopeId',
},
});
});
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview.tsx
index 77f5065bad450..c4a9dfd235949 100644
--- a/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview.tsx
@@ -7,13 +7,13 @@
import type { FC } from 'react';
import React, { useCallback } from 'react';
-import { EuiButtonEmpty, EuiFlexGroup, EuiPanel } from '@elastic/eui';
+import { EuiFlexGroup } from '@elastic/eui';
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
+import { ExpandablePanel } from '../../shared/components/expandable_panel';
import { usePrevalence } from '../hooks/use_prevalence';
import { INSIGHTS_PREVALENCE_TEST_ID } from './test_ids';
-import { InsightsSubSection } from './insights_subsection';
import { useRightPanelContext } from '../context';
-import { PREVALENCE_TEXT, PREVALENCE_TITLE, VIEW_ALL } from './translations';
+import { PREVALENCE_TITLE } from './translations';
import { LeftPanelKey, LeftPanelInsightsTabPath } from '../../left';
/**
@@ -38,34 +38,30 @@ export const PrevalenceOverview: FC = () => {
});
}, [eventId, openLeftPanel, indexName, scopeId]);
- const { empty, prevalenceRows } = usePrevalence({
+ const prevalenceRows = usePrevalence({
eventId,
browserFields,
dataFormattedForFieldBrowser,
scopeId,
});
- if (!eventId || !browserFields || !dataFormattedForFieldBrowser || empty) {
+ if (!eventId || !browserFields || !dataFormattedForFieldBrowser) {
return null;
}
return (
-
-
-
- {prevalenceRows}
-
-
-
- {VIEW_ALL(PREVALENCE_TEXT)}
-
-
+
+
+ {prevalenceRows}
+
+
);
};
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview_row.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview_row.test.tsx
index fc1a4ce2b1a3f..6398a55b50cdd 100644
--- a/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview_row.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview_row.test.tsx
@@ -38,11 +38,7 @@ describe('', () => {
});
const { getByTestId, getAllByText, queryByTestId } = render(
- {}}
- data-test-subj={dataTestSubj}
- />
+
);
const { name, values } = highlightedField;
@@ -64,18 +60,12 @@ describe('', () => {
error: false,
count: 2,
});
- const callbackIfNull = jest.fn();
const { queryAllByAltText } = render(
-
+
);
expect(queryAllByAltText('is uncommon')).toHaveLength(0);
- expect(callbackIfNull).toHaveBeenCalled();
});
it('should not display row if error retrieving data', () => {
@@ -89,18 +79,12 @@ describe('', () => {
error: true,
count: 0,
});
- const callbackIfNull = jest.fn();
const { queryAllByAltText } = render(
-
+
);
expect(queryAllByAltText('is uncommon')).toHaveLength(0);
- expect(callbackIfNull).toHaveBeenCalled();
});
it('should display loading', () => {
@@ -116,11 +100,7 @@ describe('', () => {
});
const { getByTestId } = render(
- {}}
- data-test-subj={dataTestSubj}
- />
+
);
expect(getByTestId(loadingDataTestSubj)).toBeInTheDocument();
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview_row.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview_row.tsx
index d7cef2fe99f8b..12e44a42220db 100644
--- a/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview_row.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview_row.tsx
@@ -20,10 +20,6 @@ export interface PrevalenceOverviewRowProps {
* The highlighted field name and values
* */
highlightedField: { name: string; values: string[] };
- /**
- * This is a solution to allow the parent component to NOT render if all its row children are null
- */
- callbackIfNull: () => void;
/**
* Prefix data-test-subj because this component will be used in multiple places
*/
@@ -32,12 +28,10 @@ export interface PrevalenceOverviewRowProps {
/**
* Retrieves the unique hosts for the field/value pair as well as the total number of unique hosts,
- * calculate the prevalence. If the prevalence is higher than 1, use the callback method to let the parent know
- * the row will render null.
+ * calculate the prevalence. If the prevalence is higher than 0.1, the row will render null.
*/
export const PrevalenceOverviewRow: VFC = ({
highlightedField,
- callbackIfNull,
'data-test-subj': dataTestSubj,
}) => {
const {
@@ -67,11 +61,6 @@ export const PrevalenceOverviewRow: VFC = ({
const shouldNotRender =
isFinite(prevalence) && (prevalence === 0 || prevalence > PERCENTAGE_THRESHOLD);
- // callback to let the parent component aware of which rows are null (so it can hide itself completely if all are null)
- if (!loading && (error || shouldNotRender)) {
- callbackIfNull();
- }
-
return (
{
jest.resetAllMocks();
});
+ it('should render wrapper component', () => {
+ jest.mocked(useProcessData).mockReturnValue({
+ processName: 'process1',
+ userName: 'user1',
+ startAt: '2022-01-01T00:00:00.000Z',
+ ruleName: 'rule1',
+ ruleId: 'id',
+ workdir: '/path/to/workdir',
+ command: 'command1',
+ });
+
+ renderSessionPreview();
+
+ expect(screen.queryByTestId(SESSION_PREVIEW_TOGGLE_ICON_TEST_ID)).not.toBeInTheDocument();
+ expect(screen.getByTestId(SESSION_PREVIEW_TITLE_LINK_TEST_ID)).toBeInTheDocument();
+ expect(screen.getByTestId(SESSION_PREVIEW_TITLE_ICON_TEST_ID)).toBeInTheDocument();
+ expect(screen.getByTestId(SESSION_PREVIEW_CONTENT_TEST_ID)).toBeInTheDocument();
+ expect(screen.queryByTestId(SESSION_PREVIEW_TITLE_TEXT_TEST_ID)).not.toBeInTheDocument();
+ });
+
it('renders session preview with all data', () => {
jest.mocked(useProcessData).mockReturnValue({
processName: 'process1',
@@ -61,7 +87,7 @@ describe('SessionPreview', () => {
expect(screen.getByText('started')).toBeInTheDocument();
expect(screen.getByText('process1')).toBeInTheDocument();
expect(screen.getByText('at')).toBeInTheDocument();
- expect(screen.getByText('Jan 1, 2022 @ 00:00:00.000')).toBeInTheDocument();
+ expect(screen.getByText('2022-01-01T00:00:00Z')).toBeInTheDocument();
expect(screen.getByText('with rule')).toBeInTheDocument();
expect(screen.getByText('rule1')).toBeInTheDocument();
expect(screen.getByText('by')).toBeInTheDocument();
@@ -102,7 +128,7 @@ describe('SessionPreview', () => {
const { getByTestId } = renderSessionPreview();
- getByTestId(SESSION_PREVIEW_VIEW_DETAILS_BUTTON_TEST_ID).click();
+ getByTestId(SESSION_PREVIEW_TITLE_LINK_TEST_ID).click();
expect(flyoutContextValue.openLeftPanel).toHaveBeenCalledWith({
id: LeftPanelKey,
path: LeftPanelVisualizeTabPath,
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/session_preview.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/session_preview.tsx
index 0d79d2b51f25b..b6ba4398f7e6b 100644
--- a/x-pack/plugins/security_solution/public/flyout/right/components/session_preview.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/right/components/session_preview.tsx
@@ -5,15 +5,16 @@
* 2.0.
*/
-import { EuiButtonEmpty, EuiCode, EuiIcon, EuiPanel, useEuiTheme } from '@elastic/eui';
+import { EuiCode, EuiIcon, useEuiTheme } from '@elastic/eui';
import React, { useMemo, type FC, useCallback } from 'react';
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
+import { ExpandablePanel } from '../../shared/components/expandable_panel';
import { SIGNAL_RULE_NAME_FIELD_NAME } from '../../../timelines/components/timeline/body/renderers/constants';
import { useRightPanelContext } from '../context';
import { PreferenceFormattedDate } from '../../../common/components/formatted_date';
import { useProcessData } from '../hooks/use_process_data';
-import { SESSION_PREVIEW_TEST_ID, SESSION_PREVIEW_VIEW_DETAILS_BUTTON_TEST_ID } from './test_ids';
+import { SESSION_PREVIEW_TEST_ID } from './test_ids';
import {
SESSION_PREVIEW_COMMAND_TEXT,
SESSION_PREVIEW_PROCESS_TEXT,
@@ -120,29 +121,25 @@ export const SessionPreview: FC = () => {
}, [command, workdir]);
return (
-
-
-
- {SESSION_PREVIEW_TITLE}
-
-
-
-
-
-
- {userName}
-
- {processNameFragment}
- {timeFragment}
- {ruleFragment}
- {commandFragment}
-
-
-
+
+
+
+
+
+ {userName}
+
+ {processNameFragment}
+ {timeFragment}
+ {ruleFragment}
+ {commandFragment}
+
+
);
};
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts
index 3224619575a07..43868275c0399 100644
--- a/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts
+++ b/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts
@@ -5,6 +5,14 @@
* 2.0.
*/
+import {
+ EXPANDABLE_PANEL_CONTENT_TEST_ID,
+ EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID,
+ EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID,
+ EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID,
+ EXPANDABLE_PANEL_LOADING_TEST_ID,
+ EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID,
+} from '../../shared/components/test_ids';
import { RESPONSE_BASE_TEST_ID } from '../../left/components/test_ids';
import { CONTENT_TEST_ID, HEADER_TEST_ID } from './expandable_section';
@@ -64,85 +72,145 @@ export const HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID =
export const INVESTIGATION_GUIDE_BUTTON_TEST_ID =
'securitySolutionDocumentDetailsFlyoutInvestigationGuideButton';
-/* Insights section*/
+/* Insights section */
export const INSIGHTS_TEST_ID = 'securitySolutionDocumentDetailsFlyoutInsights';
-export const INSIGHTS_HEADER_TEST_ID = 'securitySolutionDocumentDetailsFlyoutInsightsHeader';
-export const ENTITIES_HEADER_TEST_ID = 'securitySolutionDocumentDetailsFlyoutEntitiesHeader';
-export const ENTITIES_CONTENT_TEST_ID = 'securitySolutionDocumentDetailsFlyoutEntitiesContent';
-export const ENTITIES_USER_CONTENT_TEST_ID =
- 'securitySolutionDocumentDetailsFlyoutEntitiesUserContent';
-export const ENTITIES_HOST_CONTENT_TEST_ID =
- 'securitySolutionDocumentDetailsFlyoutEntitiesHostContent';
-export const ENTITIES_VIEW_ALL_BUTTON_TEST_ID =
- 'securitySolutionDocumentDetailsFlyoutEntitiesViewAllButton';
-export const ENTITY_PANEL_TOGGLE_BUTTON_TEST_ID =
- 'securitySolutionDocumentDetailsFlyoutEntityPanelToggleButton';
-export const ENTITY_PANEL_HEADER_TEST_ID = 'securitySolutionDocumentDetailsFlyoutEntityPanelHeader';
-export const ENTITY_PANEL_HEADER_LEFT_SECTION_TEST_ID =
- 'securitySolutionDocumentDetailsFlyoutEntityPanelHeaderLeftSection';
-export const ENTITY_PANEL_HEADER_RIGHT_SECTION_TEST_ID =
- 'securitySolutionDocumentDetailsFlyoutEntityPanelHeaderRightSection';
-export const ENTITY_PANEL_CONTENT_TEST_ID =
- 'securitySolutionDocumentDetailsFlyoutEntityPanelContent';
+export const INSIGHTS_HEADER_TEST_ID = `${INSIGHTS_TEST_ID}Header`;
+
+/* Insights Entities */
+
+export const INSIGHTS_ENTITIES_TEST_ID = 'securitySolutionDocumentDetailsFlyoutInsightsEntities';
+export const INSIGHTS_ENTITIES_TOGGLE_ICON_TEST_ID =
+ EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(INSIGHTS_ENTITIES_TEST_ID);
+export const INSIGHTS_ENTITIES_TITLE_LINK_TEST_ID =
+ EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(INSIGHTS_ENTITIES_TEST_ID);
+export const INSIGHTS_ENTITIES_TITLE_TEXT_TEST_ID =
+ EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(INSIGHTS_ENTITIES_TEST_ID);
+export const INSIGHTS_ENTITIES_TITLE_ICON_TEST_ID =
+ EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(INSIGHTS_ENTITIES_TEST_ID);
+export const INSIGHTS_ENTITIES_CONTENT_TEST_ID =
+ EXPANDABLE_PANEL_CONTENT_TEST_ID(INSIGHTS_ENTITIES_TEST_ID);
export const TECHNICAL_PREVIEW_ICON_TEST_ID =
'securitySolutionDocumentDetailsFlyoutTechnicalPreviewIcon';
export const ENTITIES_USER_OVERVIEW_TEST_ID =
'securitySolutionDocumentDetailsFlyoutEntitiesUserOverview';
-export const ENTITIES_USER_OVERVIEW_IP_TEST_ID =
- 'securitySolutionDocumentDetailsFlyoutEntitiesUserOverviewIP';
-export const ENTITIES_USER_OVERVIEW_RISK_LEVEL_TEST_ID =
- 'securitySolutionDocumentDetailsFlyoutEntitiesUserOverviewRiskLevel';
+export const ENTITIES_USER_OVERVIEW_LINK_TEST_ID = `${ENTITIES_USER_OVERVIEW_TEST_ID}Link`;
+export const ENTITIES_USER_OVERVIEW_IP_TEST_ID = `${ENTITIES_USER_OVERVIEW_TEST_ID}IP`;
+export const ENTITIES_USER_OVERVIEW_RISK_LEVEL_TEST_ID = `${ENTITIES_USER_OVERVIEW_TEST_ID}RiskLevel`;
export const ENTITIES_HOST_OVERVIEW_TEST_ID =
'securitySolutionDocumentDetailsFlyoutEntitiesHostOverview';
-export const ENTITIES_HOST_OVERVIEW_IP_TEST_ID =
- 'securitySolutionDocumentDetailsFlyoutEntitiesHostOverviewIP';
-export const ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID =
- 'securitySolutionDocumentDetailsFlyoutEntitiesHostOverviewRiskLevel';
+export const ENTITIES_HOST_OVERVIEW_LINK_TEST_ID = `${ENTITIES_HOST_OVERVIEW_TEST_ID}Link`;
+export const ENTITIES_HOST_OVERVIEW_IP_TEST_ID = `${ENTITIES_HOST_OVERVIEW_TEST_ID}IP`;
+export const ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID = `${ENTITIES_HOST_OVERVIEW_TEST_ID}RiskLevel`;
/* Insights Threat Intelligence */
export const INSIGHTS_THREAT_INTELLIGENCE_TEST_ID =
'securitySolutionDocumentDetailsFlyoutInsightsThreatIntelligence';
-export const INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Title`;
-export const INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Content`;
-export const INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}ViewAllButton`;
-export const INSIGHTS_THREAT_INTELLIGENCE_LOADING_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Loading`;
+export const INSIGHTS_THREAT_INTELLIGENCE_TOGGLE_ICON_TEST_ID =
+ EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(INSIGHTS_THREAT_INTELLIGENCE_TEST_ID);
+export const INSIGHTS_THREAT_INTELLIGENCE_TITLE_LINK_TEST_ID =
+ EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(INSIGHTS_THREAT_INTELLIGENCE_TEST_ID);
+export const INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEXT_TEST_ID =
+ EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(INSIGHTS_THREAT_INTELLIGENCE_TEST_ID);
+export const INSIGHTS_THREAT_INTELLIGENCE_TITLE_ICON_TEST_ID =
+ EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(INSIGHTS_THREAT_INTELLIGENCE_TEST_ID);
+export const INSIGHTS_THREAT_INTELLIGENCE_LOADING_TEST_ID = EXPANDABLE_PANEL_LOADING_TEST_ID(
+ INSIGHTS_THREAT_INTELLIGENCE_TEST_ID
+);
+export const INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID = EXPANDABLE_PANEL_CONTENT_TEST_ID(
+ INSIGHTS_THREAT_INTELLIGENCE_TEST_ID
+);
+export const INSIGHTS_THREAT_INTELLIGENCE_CONTAINER_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Container`;
export const INSIGHTS_THREAT_INTELLIGENCE_VALUE_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Value`;
/* Insights Correlations */
export const INSIGHTS_CORRELATIONS_TEST_ID =
'securitySolutionDocumentDetailsFlyoutInsightsCorrelations';
-export const INSIGHTS_CORRELATIONS_TITLE_TEST_ID = `${INSIGHTS_CORRELATIONS_TEST_ID}Title`;
-export const INSIGHTS_CORRELATIONS_CONTENT_TEST_ID = `${INSIGHTS_CORRELATIONS_TEST_ID}Content`;
-export const INSIGHTS_CORRELATIONS_VIEW_ALL_BUTTON_TEST_ID = `${INSIGHTS_CORRELATIONS_TEST_ID}ViewAllButton`;
-export const INSIGHTS_CORRELATIONS_LOADING_TEST_ID = `${INSIGHTS_CORRELATIONS_TEST_ID}Loading`;
+export const INSIGHTS_CORRELATIONS_TOGGLE_ICON_TEST_ID = EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(
+ INSIGHTS_CORRELATIONS_TEST_ID
+);
+export const INSIGHTS_CORRELATIONS_TITLE_LINK_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(
+ INSIGHTS_CORRELATIONS_TEST_ID
+);
+export const INSIGHTS_CORRELATIONS_TITLE_TEXT_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(
+ INSIGHTS_CORRELATIONS_TEST_ID
+);
+export const INSIGHTS_CORRELATIONS_TITLE_ICON_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(
+ INSIGHTS_CORRELATIONS_TEST_ID
+);
+export const INSIGHTS_CORRELATIONS_LOADING_TEST_ID = EXPANDABLE_PANEL_LOADING_TEST_ID(
+ INSIGHTS_CORRELATIONS_TEST_ID
+);
+export const INSIGHTS_CORRELATIONS_CONTENT_TEST_ID = EXPANDABLE_PANEL_CONTENT_TEST_ID(
+ INSIGHTS_CORRELATIONS_TEST_ID
+);
export const INSIGHTS_CORRELATIONS_VALUE_TEST_ID = `${INSIGHTS_CORRELATIONS_TEST_ID}Value`;
/* Insights Prevalence */
export const INSIGHTS_PREVALENCE_TEST_ID =
'securitySolutionDocumentDetailsFlyoutInsightsPrevalence';
-export const INSIGHTS_PREVALENCE_TITLE_TEST_ID = `${INSIGHTS_PREVALENCE_TEST_ID}Title`;
-export const INSIGHTS_PREVALENCE_CONTENT_TEST_ID = `${INSIGHTS_PREVALENCE_TEST_ID}Content`;
-export const INSIGHTS_PREVALENCE_VIEW_ALL_BUTTON_TEST_ID = `${INSIGHTS_PREVALENCE_TEST_ID}ViewAllButton`;
+export const INSIGHTS_PREVALENCE_TOGGLE_ICON_TEST_ID = EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(
+ INSIGHTS_PREVALENCE_TEST_ID
+);
+export const INSIGHTS_PREVALENCE_TITLE_LINK_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(
+ INSIGHTS_PREVALENCE_TEST_ID
+);
+export const INSIGHTS_PREVALENCE_TITLE_TEXT_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(
+ INSIGHTS_PREVALENCE_TEST_ID
+);
+export const INSIGHTS_PREVALENCE_TITLE_ICON_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(
+ INSIGHTS_PREVALENCE_TEST_ID
+);
+export const INSIGHTS_PREVALENCE_LOADING_TEST_ID = EXPANDABLE_PANEL_LOADING_TEST_ID(
+ INSIGHTS_PREVALENCE_TEST_ID
+);
+export const INSIGHTS_PREVALENCE_CONTENT_TEST_ID = EXPANDABLE_PANEL_CONTENT_TEST_ID(
+ INSIGHTS_PREVALENCE_TEST_ID
+);
export const INSIGHTS_PREVALENCE_VALUE_TEST_ID = `${INSIGHTS_PREVALENCE_TEST_ID}Value`;
+export const INSIGHTS_PREVALENCE_ROW_TEST_ID =
+ 'securitySolutionDocumentDetailsFlyoutInsightsPrevalenceRow';
/* Visualizations section */
export const VISUALIZATIONS_SECTION_TEST_ID = 'securitySolutionDocumentDetailsVisualizationsTitle';
export const VISUALIZATIONS_SECTION_HEADER_TEST_ID =
'securitySolutionDocumentDetailsVisualizationsTitleHeader';
+
+/* Visualizations analyzer preview */
+
export const ANALYZER_PREVIEW_TEST_ID = 'securitySolutionDocumentDetailsAnalayzerPreview';
-export const ANALYZER_TREE_TEST_ID = 'securitySolutionDocumentDetailsAnalayzerTree';
-export const ANALYZER_TREE_VIEW_DETAILS_BUTTON_TEST_ID =
- 'securitySolutionDocumentDetailsAnalayzerTreeViewDetailsButton';
-export const ANALYZER_TREE_LOADING_TEST_ID = 'securitySolutionDocumentDetailsAnalayzerTreeLoading';
-export const ANALYZER_TREE_ERROR_TEST_ID = 'securitySolutionDocumentDetailsAnalayzerTreeError';
+export const ANALYZER_PREVIEW_TOGGLE_ICON_TEST_ID =
+ EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(ANALYZER_PREVIEW_TEST_ID);
+export const ANALYZER_PREVIEW_TITLE_LINK_TEST_ID =
+ EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(ANALYZER_PREVIEW_TEST_ID);
+export const ANALYZER_PREVIEW_TITLE_TEXT_TEST_ID =
+ EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(ANALYZER_PREVIEW_TEST_ID);
+export const ANALYZER_PREVIEW_TITLE_ICON_TEST_ID =
+ EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(ANALYZER_PREVIEW_TEST_ID);
+export const ANALYZER_PREVIEW_LOADING_TEST_ID =
+ EXPANDABLE_PANEL_LOADING_TEST_ID(ANALYZER_PREVIEW_TEST_ID);
+export const ANALYZER_PREVIEW_CONTENT_TEST_ID =
+ EXPANDABLE_PANEL_CONTENT_TEST_ID(ANALYZER_PREVIEW_TEST_ID);
+
+/* Visualizations session preview */
+
export const SESSION_PREVIEW_TEST_ID = 'securitySolutionDocumentDetailsSessionPreview';
-export const SESSION_PREVIEW_VIEW_DETAILS_BUTTON_TEST_ID =
- 'securitySolutionDocumentDetailsSessionPreviewViewDetailsButton';
+export const SESSION_PREVIEW_TOGGLE_ICON_TEST_ID =
+ EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(SESSION_PREVIEW_TEST_ID);
+export const SESSION_PREVIEW_TITLE_LINK_TEST_ID =
+ EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(SESSION_PREVIEW_TEST_ID);
+export const SESSION_PREVIEW_TITLE_TEXT_TEST_ID =
+ EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(SESSION_PREVIEW_TEST_ID);
+export const SESSION_PREVIEW_TITLE_ICON_TEST_ID =
+ EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(SESSION_PREVIEW_TEST_ID);
+export const SESSION_PREVIEW_LOADING_TEST_ID =
+ EXPANDABLE_PANEL_LOADING_TEST_ID(SESSION_PREVIEW_TEST_ID);
+export const SESSION_PREVIEW_CONTENT_TEST_ID =
+ EXPANDABLE_PANEL_CONTENT_TEST_ID(SESSION_PREVIEW_TEST_ID);
/* Response section */
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.test.tsx
index ba954d0960913..09628a71fbbba 100644
--- a/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.test.tsx
@@ -9,16 +9,19 @@ import React from 'react';
import { render } from '@testing-library/react';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { RightPanelContext } from '../context';
-import {
- INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID,
- INSIGHTS_THREAT_INTELLIGENCE_LOADING_TEST_ID,
- INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID,
- INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID,
-} from './test_ids';
import { TestProviders } from '../../../common/mock';
import { ThreatIntelligenceOverview } from './threat_intelligence_overview';
import { LeftPanelInsightsTabPath, LeftPanelKey } from '../../left';
import { useFetchThreatIntelligence } from '../hooks/use_fetch_threat_intelligence';
+import {
+ INSIGHTS_THREAT_INTELLIGENCE_CONTAINER_TEST_ID,
+ INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID,
+ INSIGHTS_THREAT_INTELLIGENCE_LOADING_TEST_ID,
+ INSIGHTS_THREAT_INTELLIGENCE_TITLE_ICON_TEST_ID,
+ INSIGHTS_THREAT_INTELLIGENCE_TITLE_LINK_TEST_ID,
+ INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEXT_TEST_ID,
+ INSIGHTS_THREAT_INTELLIGENCE_TOGGLE_ICON_TEST_ID,
+} from './test_ids';
jest.mock('../hooks/use_fetch_threat_intelligence');
@@ -37,6 +40,21 @@ const renderThreatIntelligenceOverview = (contextValue: RightPanelContext) => (
);
describe('', () => {
+ it('should render wrapper component', () => {
+ (useFetchThreatIntelligence as jest.Mock).mockReturnValue({
+ loading: false,
+ });
+
+ const { getByTestId, queryByTestId } = render(
+ renderThreatIntelligenceOverview(panelContextValue)
+ );
+
+ expect(queryByTestId(INSIGHTS_THREAT_INTELLIGENCE_TOGGLE_ICON_TEST_ID)).not.toBeInTheDocument();
+ expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_TITLE_ICON_TEST_ID)).toBeInTheDocument();
+ expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_TITLE_LINK_TEST_ID)).toBeInTheDocument();
+ expect(queryByTestId(INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEXT_TEST_ID)).not.toBeInTheDocument();
+ });
+
it('should render 1 match detected and 1 field enriched', () => {
(useFetchThreatIntelligence as jest.Mock).mockReturnValue({
loading: false,
@@ -46,7 +64,7 @@ describe('', () => {
const { getByTestId } = render(renderThreatIntelligenceOverview(panelContextValue));
- expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID)).toHaveTextContent(
+ expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_TITLE_LINK_TEST_ID)).toHaveTextContent(
'Threat Intelligence'
);
expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID)).toHaveTextContent(
@@ -55,7 +73,6 @@ describe('', () => {
expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID)).toHaveTextContent(
'1 field enriched with threat intelligence'
);
- expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID)).toBeInTheDocument();
});
it('should render 2 matches detected and 2 fields enriched', () => {
@@ -67,7 +84,7 @@ describe('', () => {
const { getByTestId } = render(renderThreatIntelligenceOverview(panelContextValue));
- expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID)).toHaveTextContent(
+ expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_TITLE_LINK_TEST_ID)).toHaveTextContent(
'Threat Intelligence'
);
expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID)).toHaveTextContent(
@@ -76,7 +93,6 @@ describe('', () => {
expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID)).toHaveTextContent(
'2 fields enriched with threat intelligence'
);
- expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID)).toBeInTheDocument();
});
it('should render 0 field enriched', () => {
@@ -126,9 +142,9 @@ describe('', () => {
eventId: null,
} as unknown as RightPanelContext;
- const { container } = render(renderThreatIntelligenceOverview(contextValue));
+ const { getByTestId } = render(renderThreatIntelligenceOverview(contextValue));
- expect(container).toBeEmptyDOMElement();
+ expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTAINER_TEST_ID)).toBeEmptyDOMElement();
});
it('should render null when dataFormattedForFieldBrowser is null', () => {
@@ -141,9 +157,9 @@ describe('', () => {
dataFormattedForFieldBrowser: null,
} as unknown as RightPanelContext;
- const { container } = render(renderThreatIntelligenceOverview(contextValue));
+ const { getByTestId } = render(renderThreatIntelligenceOverview(contextValue));
- expect(container).toBeEmptyDOMElement();
+ expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTAINER_TEST_ID)).toBeEmptyDOMElement();
});
it('should navigate to left section Insights tab when clicking on button', () => {
@@ -166,7 +182,7 @@ describe('', () => {
);
- getByTestId(INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID).click();
+ getByTestId(INSIGHTS_THREAT_INTELLIGENCE_TITLE_LINK_TEST_ID).click();
expect(flyoutContextValue.openLeftPanel).toHaveBeenCalledWith({
id: LeftPanelKey,
path: LeftPanelInsightsTabPath,
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.tsx
index 3c9bbc1e356df..aa6ced53979e8 100644
--- a/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.tsx
@@ -7,17 +7,15 @@
import type { FC } from 'react';
import React, { useCallback } from 'react';
-import { EuiButtonEmpty, EuiFlexGroup, EuiPanel } from '@elastic/eui';
+import { EuiFlexGroup } from '@elastic/eui';
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
+import { ExpandablePanel } from '../../shared/components/expandable_panel';
import { useFetchThreatIntelligence } from '../hooks/use_fetch_threat_intelligence';
-import { InsightsSubSection } from './insights_subsection';
import { InsightsSummaryRow } from './insights_summary_row';
import { useRightPanelContext } from '../context';
import { INSIGHTS_THREAT_INTELLIGENCE_TEST_ID } from './test_ids';
import {
- VIEW_ALL,
THREAT_INTELLIGENCE_TITLE,
- THREAT_INTELLIGENCE_TEXT,
THREAT_MATCH_DETECTED,
THREAT_ENRICHMENT,
THREAT_MATCHES_DETECTED,
@@ -58,41 +56,37 @@ export const ThreatIntelligenceOverview: FC = () => {
const error: boolean = !eventId || !dataFormattedForFieldBrowser || threatIntelError;
return (
-
-
-
-
-
-
-
-
- {VIEW_ALL(THREAT_INTELLIGENCE_TEXT)}
-
-
+
+
+
+
);
};
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts b/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts
index 03b3c8d4b9d80..65dcb49f36f21 100644
--- a/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts
+++ b/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts
@@ -31,13 +31,6 @@ export const SEVERITY_TITLE = i18n.translate(
}
);
-export const STATUS_TITLE = i18n.translate(
- 'xpack.securitySolution.flyout.documentDetails.statusTitle',
- {
- defaultMessage: 'Status',
- }
-);
-
export const RISK_SCORE_TITLE = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.riskScoreTitle',
{
@@ -158,20 +151,6 @@ export const TECHNICAL_PREVIEW_MESSAGE = i18n.translate(
}
);
-export const ENTITIES_TEXT = i18n.translate(
- 'xpack.securitySolution.flyout.documentDetails.overviewTab.entitiesText',
- {
- defaultMessage: 'entities',
- }
-);
-
-export const THREAT_INTELLIGENCE_TEXT = i18n.translate(
- 'xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligenceText',
- {
- defaultMessage: 'fields of threat intelligence',
- }
-);
-
export const THREAT_MATCH_DETECTED = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatMatch',
{
@@ -200,73 +179,6 @@ export const THREAT_ENRICHMENTS = i18n.translate(
}
);
-export const CORRELATIONS_TEXT = i18n.translate(
- 'xpack.securitySolution.flyout.documentDetails.overviewTab.correlationsText',
- {
- defaultMessage: 'fields of correlation',
- }
-);
-
-export const CORRELATIONS_ANCESTRY_ALERT = i18n.translate(
- 'xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.ancestryAlert',
- {
- defaultMessage: 'alert related by ancestry',
- }
-);
-
-export const CORRELATIONS_ANCESTRY_ALERTS = i18n.translate(
- 'xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.ancestryAlerts',
- {
- defaultMessage: 'alerts related by ancestry',
- }
-);
-export const CORRELATIONS_SAME_SOURCE_EVENT_ALERT = i18n.translate(
- 'xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.sameSourceEventAlert',
- {
- defaultMessage: 'alert related by the same source event',
- }
-);
-
-export const CORRELATIONS_SAME_SOURCE_EVENT_ALERTS = i18n.translate(
- 'xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.sameSourceEventAlerts',
- {
- defaultMessage: 'alerts related by the same source event',
- }
-);
-export const CORRELATIONS_SAME_SESSION_ALERT = i18n.translate(
- 'xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.sameSessionAlert',
- {
- defaultMessage: 'alert related by session',
- }
-);
-
-export const CORRELATIONS_SAME_SESSION_ALERTS = i18n.translate(
- 'xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.sameSessionAlerts',
- {
- defaultMessage: 'alerts related by session',
- }
-);
-export const CORRELATIONS_RELATED_CASE = i18n.translate(
- 'xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.relatedCase',
- {
- defaultMessage: 'related case',
- }
-);
-
-export const CORRELATIONS_RELATED_CASES = i18n.translate(
- 'xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.relatedCases',
- {
- defaultMessage: 'related cases',
- }
-);
-
-export const PREVALENCE_TEXT = i18n.translate(
- 'xpack.securitySolution.flyout.documentDetails.overviewTab.prevalenceText',
- {
- defaultMessage: 'fields of prevalence',
- }
-);
-
export const PREVALENCE_ROW_UNCOMMON = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.overviewTab.prevalenceRowText',
{
@@ -274,11 +186,6 @@ export const PREVALENCE_ROW_UNCOMMON = i18n.translate(
}
);
-export const VIEW_ALL = (text: string) =>
- i18n.translate('xpack.securitySolution.flyout.documentDetails.overviewTab.viewAllButton', {
- values: { text },
- defaultMessage: 'View all {text}',
- });
export const VISUALIZATIONS_TITLE = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.visualizationsTitle',
{ defaultMessage: 'Visualizations' }
@@ -289,13 +196,6 @@ export const ANALYZER_PREVIEW_TITLE = i18n.translate(
{ defaultMessage: 'Analyzer preview' }
);
-export const ANALYZER_PREVIEW_TEXT = i18n.translate(
- 'xpack.securitySolution.flyout.documentDetails.analyzerPreviewText',
- {
- defaultMessage: 'analyzer preview.',
- }
-);
-
export const SHARE = i18n.translate('xpack.securitySolution.flyout.documentDetails.share', {
defaultMessage: 'Share Alert',
});
@@ -349,13 +249,6 @@ export const RESPONSE_TITLE = i18n.translate(
}
);
-export const RESPONSE_BUTTON = i18n.translate(
- 'xpack.securitySolution.flyout.documentDetails.responseSectionButton',
- {
- defaultMessage: 'Response details',
- }
-);
-
export const RESPONSE_EMPTY = i18n.translate('xpack.securitySolution.flyout.response.empty', {
defaultMessage: 'There are no response actions defined for this event.',
});
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/user_entity_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/user_entity_overview.test.tsx
index 1d97eb642967d..c2c26a8d288b8 100644
--- a/x-pack/plugins/security_solution/public/flyout/right/components/user_entity_overview.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/right/components/user_entity_overview.test.tsx
@@ -12,10 +12,16 @@ import { useRiskScore } from '../../../explore/containers/risk_score';
import {
ENTITIES_USER_OVERVIEW_IP_TEST_ID,
+ ENTITIES_USER_OVERVIEW_LINK_TEST_ID,
ENTITIES_USER_OVERVIEW_RISK_LEVEL_TEST_ID,
TECHNICAL_PREVIEW_ICON_TEST_ID,
} from './test_ids';
import { useObservedUserDetails } from '../../../explore/users/containers/users/observed_details';
+import { mockContextValue } from '../mocks/mock_right_panel_context';
+import { mockDataFormattedForFieldBrowser } from '../mocks/mock_context';
+import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
+import { RightPanelContext } from '../context';
+import { LeftPanelInsightsTabPath, LeftPanelKey } from '../../left';
const userName = 'user';
const ip = '10.200.000.000';
@@ -25,6 +31,15 @@ const selectedPatterns = 'alerts';
const userData = { host: { ip: [ip] } };
const riskLevel = [{ user: { risk: { calculated_level: 'Medium' } } }];
+const panelContextValue = {
+ ...mockContextValue,
+ dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser,
+};
+
+const flyoutContextValue = {
+ openLeftPanel: jest.fn(),
+} as unknown as ExpandableFlyoutContext;
+
const mockUseGlobalTime = jest.fn().mockReturnValue({ from, to });
jest.mock('../../../common/containers/use_global_time', () => {
return {
@@ -53,7 +68,9 @@ describe('', () => {
const { getByTestId } = render(
-
+
+
+
);
@@ -68,7 +85,9 @@ describe('', () => {
const { getByTestId } = render(
-
+
+
+
);
expect(getByTestId(ENTITIES_USER_OVERVIEW_IP_TEST_ID)).toHaveTextContent('—');
@@ -83,7 +102,9 @@ describe('', () => {
mockUseRiskScore.mockReturnValue({ data: riskLevel, isAuthorized: false });
const { getByTestId, queryByTestId } = render(
-
+
+
+
);
@@ -96,12 +117,40 @@ describe('', () => {
mockUseRiskScore.mockReturnValue({ data: null, isAuthorized: false });
const { getByTestId, queryByTestId } = render(
-
+
+
+
);
expect(getByTestId(ENTITIES_USER_OVERVIEW_IP_TEST_ID)).toHaveTextContent('—');
expect(queryByTestId(TECHNICAL_PREVIEW_ICON_TEST_ID)).not.toBeInTheDocument();
});
+
+ it('should navigate to left panel entities tab when clicking on title', () => {
+ mockUseUserDetails.mockReturnValue([false, { userDetails: userData }]);
+ mockUseRiskScore.mockReturnValue({ data: riskLevel, isAuthorized: true });
+
+ const { getByTestId } = render(
+
+
+
+
+
+
+
+ );
+
+ getByTestId(ENTITIES_USER_OVERVIEW_LINK_TEST_ID).click();
+ expect(flyoutContextValue.openLeftPanel).toHaveBeenCalledWith({
+ id: LeftPanelKey,
+ path: LeftPanelInsightsTabPath,
+ params: {
+ id: panelContextValue.eventId,
+ indexName: panelContextValue.indexName,
+ scopeId: panelContextValue.scopeId,
+ },
+ });
+ });
});
});
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/user_entity_overview.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/user_entity_overview.tsx
index da821902ff190..61a598ddef771 100644
--- a/x-pack/plugins/security_solution/public/flyout/right/components/user_entity_overview.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/right/components/user_entity_overview.tsx
@@ -5,10 +5,21 @@
* 2.0.
*/
-import React, { useMemo } from 'react';
-import { EuiFlexGroup, EuiFlexItem, EuiBetaBadge } from '@elastic/eui';
+import React, { useCallback, useMemo } from 'react';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiBetaBadge,
+ EuiIcon,
+ EuiLink,
+ useEuiTheme,
+ useEuiFontSize,
+} from '@elastic/eui';
+import { css } from '@emotion/css';
import { getOr } from 'lodash/fp';
-import styled from 'styled-components';
+import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
+import { LeftPanelInsightsTabPath, LeftPanelKey } from '../../left';
+import { useRightPanelContext } from '../context';
import type { DescriptionList } from '../../../../common/utility_types';
import {
buildUserNamesFilter,
@@ -31,13 +42,11 @@ import {
ENTITIES_USER_OVERVIEW_TEST_ID,
ENTITIES_USER_OVERVIEW_IP_TEST_ID,
ENTITIES_USER_OVERVIEW_RISK_LEVEL_TEST_ID,
+ ENTITIES_USER_OVERVIEW_LINK_TEST_ID,
} from './test_ids';
import { useObservedUserDetails } from '../../../explore/users/containers/users/observed_details';
-const StyledEuiBetaBadge = styled(EuiBetaBadge)`
- margin-left: ${({ theme }) => theme.eui.euiSizeXS};
-`;
-
+const USER_ICON = 'user';
const CONTEXT_ID = `flyout-user-entity-overview`;
export interface UserEntityOverviewProps {
@@ -51,6 +60,20 @@ export interface UserEntityOverviewProps {
* User preview content for the entities preview in right flyout. It contains ip addresses and risk classification
*/
export const UserEntityOverview: React.FC = ({ userName }) => {
+ const { eventId, indexName, scopeId } = useRightPanelContext();
+ const { openLeftPanel } = useExpandableFlyoutContext();
+ const goToEntitiesTab = useCallback(() => {
+ openLeftPanel({
+ id: LeftPanelKey,
+ path: LeftPanelInsightsTabPath,
+ params: {
+ id: eventId,
+ indexName,
+ scopeId,
+ },
+ });
+ }, [eventId, openLeftPanel, indexName, scopeId]);
+
const { from, to } = useGlobalTime();
const { selectedPatterns } = useSourcererDataView();
@@ -97,6 +120,9 @@ export const UserEntityOverview: React.FC = ({ userName
[data]
);
+ const { euiTheme } = useEuiTheme();
+ const xsFontSize = useEuiFontSize('xs').fontSize;
+
const [userRiskLevel] = useMemo(() => {
const userRiskData = userRisk && userRisk.length > 0 ? userRisk[0] : undefined;
@@ -105,7 +131,10 @@ export const UserEntityOverview: React.FC = ({ userName
title: (
<>
{i18n.USER_RISK_CLASSIFICATION}
- = ({ userName
),
},
];
- }, [userRisk]);
+ }, [euiTheme.size.xs, userRisk]);
return (
-
+
-
+
+
+
+
+
+
+ {userName}
+
+
+
- {isAuthorized && (
-
- )}
+
+
+
+
+
+ {isAuthorized && (
+
+ )}
+
+
);
diff --git a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.test.tsx
index dfbdf3f278ef5..aff691037d435 100644
--- a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.test.tsx
@@ -5,8 +5,8 @@
* 2.0.
*/
+import type { ReactElement } from 'react';
import { getSummaryRows } from '../../../common/components/event_details/get_alert_summary_rows';
-import type { UsePrevalenceResult } from './use_prevalence';
import { usePrevalence } from './use_prevalence';
import type { RenderHookResult } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react-hooks';
@@ -20,7 +20,7 @@ const dataFormattedForFieldBrowser = mockDataFormattedForFieldBrowser;
const scopeId = 'scopeId';
describe('usePrevalence', () => {
- let hookResult: RenderHookResult;
+ let hookResult: RenderHookResult;
it('should return 1 row to render', () => {
const mockSummaryRow = {
@@ -38,8 +38,7 @@ describe('usePrevalence', () => {
usePrevalence({ browserFields, dataFormattedForFieldBrowser, eventId, scopeId })
);
- expect(hookResult.result.current.prevalenceRows.length).toEqual(1);
- expect(hookResult.result.current.empty).toEqual(false);
+ expect(hookResult.result.current.length).toEqual(1);
});
it('should return empty true', () => {
@@ -49,7 +48,6 @@ describe('usePrevalence', () => {
usePrevalence({ browserFields, dataFormattedForFieldBrowser, eventId, scopeId })
);
- expect(hookResult.result.current.prevalenceRows.length).toEqual(0);
- expect(hookResult.result.current.empty).toEqual(true);
+ expect(hookResult.result.current.length).toEqual(0);
});
});
diff --git a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.tsx b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.tsx
index 82a359d0e70f1..162bc8bc851aa 100644
--- a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.tsx
@@ -7,10 +7,10 @@
import type { BrowserFields, TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
import type { ReactElement } from 'react';
-import React, { useMemo, useState } from 'react';
+import React, { useMemo } from 'react';
import { getSummaryRows } from '../../../common/components/event_details/get_alert_summary_rows';
import { PrevalenceOverviewRow } from '../components/prevalence_overview_row';
-import { INSIGHTS_PREVALENCE_TEST_ID } from '../components/test_ids';
+import { INSIGHTS_PREVALENCE_ROW_TEST_ID } from '../components/test_ids';
export interface UsePrevalenceParams {
/**
@@ -30,16 +30,6 @@ export interface UsePrevalenceParams {
*/
scopeId: string;
}
-export interface UsePrevalenceResult {
- /**
- * Returns all row children to render
- */
- prevalenceRows: ReactElement[];
- /**
- * Returns true if all row children render null
- */
- empty: boolean;
-}
/**
* This hook retrieves the highlighted fields from the {@link getSummaryRows} method, then iterates through them
@@ -52,9 +42,7 @@ export const usePrevalence = ({
browserFields,
dataFormattedForFieldBrowser,
scopeId,
-}: UsePrevalenceParams): UsePrevalenceResult => {
- const [count, setCount] = useState(0); // TODO this needs to be changed at it causes a re-render when the count is updated
-
+}: UsePrevalenceParams): ReactElement[] => {
// retrieves the highlighted fields
const summaryRows = useMemo(
() =>
@@ -68,7 +56,7 @@ export const usePrevalence = ({
[browserFields, dataFormattedForFieldBrowser, eventId, scopeId]
);
- const prevalenceRows = useMemo(
+ return useMemo(
() =>
summaryRows.map((row) => {
const highlightedField = {
@@ -79,17 +67,11 @@ export const usePrevalence = ({
return (
setCount((prevCount) => prevCount + 1)}
- data-test-subj={INSIGHTS_PREVALENCE_TEST_ID}
+ data-test-subj={INSIGHTS_PREVALENCE_ROW_TEST_ID}
key={row.description.data.field}
/>
);
}),
[summaryRows]
);
-
- return {
- prevalenceRows,
- empty: count >= summaryRows.length,
- };
};
diff --git a/x-pack/plugins/security_solution/public/flyout/right/tabs/overview_tab.tsx b/x-pack/plugins/security_solution/public/flyout/right/tabs/overview_tab.tsx
index 5a59c56a2394d..9c689e4d3fc7b 100644
--- a/x-pack/plugins/security_solution/public/flyout/right/tabs/overview_tab.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/right/tabs/overview_tab.tsx
@@ -22,10 +22,10 @@ export const OverviewTab: FC = memo(() => {
<>
-
-
+
+
diff --git a/x-pack/plugins/security_solution/public/flyout/shared/components/expandable_panel.stories.tsx b/x-pack/plugins/security_solution/public/flyout/shared/components/expandable_panel.stories.tsx
new file mode 100644
index 0000000000000..25b5243e82d3a
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/flyout/shared/components/expandable_panel.stories.tsx
@@ -0,0 +1,66 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import type { Story } from '@storybook/react';
+import { EuiIcon } from '@elastic/eui';
+import { ExpandablePanel } from './expandable_panel';
+
+export default {
+ component: ExpandablePanel,
+ title: 'Flyout/ExpandablePanel',
+};
+
+const defaultProps = {
+ header: {
+ title: 'title',
+ iconType: 'storage',
+ },
+};
+const headerContent = ;
+
+const children = {'test content'}
;
+
+export const Default: Story = () => {
+ return {children};
+};
+
+export const DefaultWithHeaderContent: Story = () => {
+ const props = {
+ ...defaultProps,
+ header: { ...defaultProps.header, headerContent },
+ };
+ return {children};
+};
+
+export const Expandable: Story = () => {
+ const props = {
+ ...defaultProps,
+ expand: { expandable: true },
+ };
+ return {children};
+};
+
+export const ExpandableDefaultOpen: Story = () => {
+ const props = {
+ ...defaultProps,
+ expand: { expandable: true, expandedOnFirstRender: true },
+ };
+ return {children};
+};
+
+export const EmptyDefault: Story = () => {
+ return ;
+};
+
+export const EmptyDefaultExpanded: Story = () => {
+ const props = {
+ ...defaultProps,
+ expand: { expandable: true },
+ };
+ return ;
+};
diff --git a/x-pack/plugins/security_solution/public/flyout/shared/components/expandable_panel.test.tsx b/x-pack/plugins/security_solution/public/flyout/shared/components/expandable_panel.test.tsx
new file mode 100644
index 0000000000000..7c7f46a11c308
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/flyout/shared/components/expandable_panel.test.tsx
@@ -0,0 +1,190 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { render } from '@testing-library/react';
+import {
+ EXPANDABLE_PANEL_HEADER_LEFT_SECTION_TEST_ID,
+ EXPANDABLE_PANEL_HEADER_RIGHT_SECTION_TEST_ID,
+ EXPANDABLE_PANEL_CONTENT_TEST_ID,
+ EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID,
+ EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID,
+} from './test_ids';
+import { ThemeProvider } from 'styled-components';
+import { getMockTheme } from '../../../common/lib/kibana/kibana_react.mock';
+import { ExpandablePanel } from './expandable_panel';
+
+const mockTheme = getMockTheme({ eui: { euiColorMediumShade: '#ece' } });
+const TEST_ID = 'test-id';
+const defaultProps = {
+ header: {
+ title: 'test title',
+ iconType: 'storage',
+ },
+ 'data-test-subj': TEST_ID,
+};
+const children = {'test content'}
;
+
+describe('', () => {
+ describe('panel is not expandable by default', () => {
+ it('should render non-expandable panel by default', () => {
+ const { getByTestId, queryByTestId } = render(
+
+ {children}
+
+ );
+ expect(getByTestId(TEST_ID)).toBeInTheDocument();
+ expect(getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(TEST_ID))).toBeInTheDocument();
+ expect(getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(TEST_ID))).toHaveTextContent(
+ 'test content'
+ );
+ expect(queryByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(TEST_ID))).not.toBeInTheDocument();
+ });
+
+ it('should only render left section of panel header when headerContent is not passed', () => {
+ const { getByTestId, queryByTestId } = render(
+
+ {children}
+
+ );
+ expect(getByTestId(EXPANDABLE_PANEL_HEADER_LEFT_SECTION_TEST_ID(TEST_ID))).toHaveTextContent(
+ 'test title'
+ );
+ expect(
+ queryByTestId(EXPANDABLE_PANEL_HEADER_RIGHT_SECTION_TEST_ID(TEST_ID))
+ ).not.toBeInTheDocument();
+ });
+
+ it('should render header properly when headerContent is available', () => {
+ const props = {
+ ...defaultProps,
+ header: { ...defaultProps.header, headerContent: <>{'test header content'}> },
+ };
+ const { getByTestId } = render(
+
+ {children}
+
+ );
+ expect(
+ getByTestId(EXPANDABLE_PANEL_HEADER_LEFT_SECTION_TEST_ID(TEST_ID))
+ ).toBeInTheDocument();
+ expect(
+ getByTestId(EXPANDABLE_PANEL_HEADER_RIGHT_SECTION_TEST_ID(TEST_ID))
+ ).toBeInTheDocument();
+ expect(getByTestId(EXPANDABLE_PANEL_HEADER_RIGHT_SECTION_TEST_ID(TEST_ID))).toHaveTextContent(
+ 'test header content'
+ );
+ });
+
+ it('should not render content when content is null', () => {
+ const { queryByTestId } = render(
+
+
+
+ );
+
+ expect(queryByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(TEST_ID))).not.toBeInTheDocument();
+ expect(queryByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(TEST_ID))).not.toBeInTheDocument();
+ });
+ });
+
+ describe('panel is expandable', () => {
+ const expandableDefaultProps = {
+ ...defaultProps,
+ expand: { expandable: true },
+ };
+
+ it('should render panel with toggle and collapsed by default', () => {
+ const { getByTestId, queryByTestId } = render(
+
+ {children}
+
+ );
+ expect(getByTestId(TEST_ID)).toBeInTheDocument();
+ expect(getByTestId(EXPANDABLE_PANEL_HEADER_LEFT_SECTION_TEST_ID(TEST_ID))).toHaveTextContent(
+ 'test title'
+ );
+ expect(queryByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(TEST_ID))).not.toBeInTheDocument();
+ });
+
+ it('click toggle button should expand the panel', () => {
+ const { getByTestId } = render(
+
+ {children}
+
+ );
+
+ const toggle = getByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(TEST_ID));
+ expect(toggle.firstChild).toHaveAttribute('data-euiicon-type', 'arrowRight');
+ toggle.click();
+
+ expect(getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(TEST_ID))).toHaveTextContent(
+ 'test content'
+ );
+ expect(toggle.firstChild).toHaveAttribute('data-euiicon-type', 'arrowDown');
+ });
+
+ it('should not render toggle or content when content is null', () => {
+ const { queryByTestId } = render(
+
+
+
+ );
+ expect(queryByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(TEST_ID))).not.toBeInTheDocument();
+ expect(queryByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(TEST_ID))).not.toBeInTheDocument();
+ });
+ });
+
+ describe('panel is expandable and expanded', () => {
+ const expandedDefaultProps = {
+ ...defaultProps,
+ expand: { expandable: true, expandedOnFirstRender: true },
+ };
+
+ it('should render header and content', () => {
+ const { getByTestId } = render(
+
+ {children}
+
+ );
+ expect(getByTestId(TEST_ID)).toBeInTheDocument();
+ expect(getByTestId(EXPANDABLE_PANEL_HEADER_LEFT_SECTION_TEST_ID(TEST_ID))).toHaveTextContent(
+ 'test title'
+ );
+ expect(getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(TEST_ID))).toHaveTextContent(
+ 'test content'
+ );
+ expect(getByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(TEST_ID))).toBeInTheDocument();
+ });
+
+ it('click toggle button should collapse the panel', () => {
+ const { getByTestId, queryByTestId } = render(
+
+ {children}
+
+ );
+
+ const toggle = getByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(TEST_ID));
+ expect(toggle.firstChild).toHaveAttribute('data-euiicon-type', 'arrowDown');
+ expect(getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(TEST_ID))).toBeInTheDocument();
+
+ toggle.click();
+ expect(toggle.firstChild).toHaveAttribute('data-euiicon-type', 'arrowRight');
+ expect(queryByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(TEST_ID))).not.toBeInTheDocument();
+ });
+
+ it('should not render content when content is null', () => {
+ const { queryByTestId } = render(
+
+
+
+ );
+ expect(queryByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(TEST_ID))).not.toBeInTheDocument();
+ expect(queryByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(TEST_ID))).not.toBeInTheDocument();
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/flyout/shared/components/expandable_panel.tsx b/x-pack/plugins/security_solution/public/flyout/shared/components/expandable_panel.tsx
new file mode 100644
index 0000000000000..f9bf5994dff35
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/flyout/shared/components/expandable_panel.tsx
@@ -0,0 +1,194 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useMemo, useState, useCallback } from 'react';
+import {
+ EuiButtonIcon,
+ EuiSplitPanel,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiPanel,
+ EuiIcon,
+ EuiLink,
+ EuiTitle,
+ EuiText,
+ EuiLoadingSpinner,
+} from '@elastic/eui';
+import styled from 'styled-components';
+
+const StyledEuiFlexItem = styled(EuiFlexItem)`
+ margin-right: ${({ theme }) => theme.eui.euiSizeM};
+`;
+
+const StyledEuiIcon = styled(EuiIcon)`
+ margin: ${({ theme }) => theme.eui.euiSizeS} 0;
+`;
+
+const StyledEuiLink = styled(EuiLink)`
+ font-size: 12px;
+ font-weight: 700;
+`;
+
+export interface ExpandablePanelPanelProps {
+ header: {
+ /**
+ * String value of the title to be displayed in the header of panel
+ */
+ title: string;
+ /**
+ * Callback function to be called when the title is clicked
+ */
+ callback?: () => void;
+ /**
+ * Icon string for displaying the specified icon in the header
+ */
+ iconType: string;
+ /**
+ * Optional content and actions to be displayed on the right side of header
+ */
+ headerContent?: React.ReactNode;
+ };
+ content?: {
+ /**
+ * Renders a loading spinner if true
+ */
+ loading?: boolean;
+ /**
+ * Returns a null component if true
+ */
+ error?: boolean;
+ };
+ expand?: {
+ /**
+ * Boolean to determine the panel to be collapsable (with toggle)
+ */
+ expandable?: boolean;
+ /**
+ * Boolean to allow the component to be expanded or collapsed on first render
+ */
+ expandedOnFirstRender?: boolean;
+ };
+ /**
+ Data test subject string for testing
+ */
+ ['data-test-subj']?: string;
+}
+
+/**
+ * Wrapper component that is composed of a header section and a content section.
+ * The header can display an icon, a title (that can be a link), and an optional content section on the right.
+ * The content section can display a loading spinner, an error message, or any other content.
+ * The component can be expanded or collapsed by clicking on the chevron icon on the left of the title.
+ */
+export const ExpandablePanel: React.FC = ({
+ header: { title, callback, iconType, headerContent },
+ content: { loading, error } = { loading: false, error: false },
+ expand: { expandable, expandedOnFirstRender } = {
+ expandable: false,
+ expandedOnFirstRender: false,
+ },
+ 'data-test-subj': dataTestSubj,
+ children,
+}) => {
+ const [toggleStatus, setToggleStatus] = useState(expandedOnFirstRender);
+ const toggleQuery = useCallback(() => {
+ setToggleStatus(!toggleStatus);
+ }, [setToggleStatus, toggleStatus]);
+
+ const toggleIcon = useMemo(
+ () => (
+
+ ),
+ [dataTestSubj, toggleStatus, toggleQuery]
+ );
+
+ const headerLeftSection = useMemo(
+ () => (
+
+
+ {expandable && children && toggleIcon}
+
+
+
+
+ {callback ? (
+
+ {title}
+
+ ) : (
+
+ {title}
+
+ )}
+
+
+
+ ),
+ [dataTestSubj, expandable, children, toggleIcon, callback, iconType, title]
+ );
+
+ const headerRightSection = useMemo(
+ () =>
+ headerContent && (
+
+ {headerContent}
+
+ ),
+ [dataTestSubj, headerContent]
+ );
+
+ const showContent = useMemo(() => {
+ if (!children) {
+ return false;
+ }
+ return !expandable || (expandable && toggleStatus);
+ }, [children, expandable, toggleStatus]);
+
+ const content = loading ? (
+
+
+
+
+
+ ) : error ? null : (
+ children
+ );
+
+ return (
+
+
+
+ {headerLeftSection}
+ {headerRightSection}
+
+
+ {showContent && (
+
+ {content}
+
+ )}
+
+ );
+};
+
+ExpandablePanel.displayName = 'ExpandablePanel';
diff --git a/x-pack/plugins/security_solution/public/flyout/shared/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/shared/components/test_ids.ts
new file mode 100644
index 0000000000000..1e5ed99958b04
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/flyout/shared/components/test_ids.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+/* Insights section*/
+
+export const EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID = (dataTestSubj: string) =>
+ `${dataTestSubj}ToggleIcon`;
+export const EXPANDABLE_PANEL_HEADER_LEFT_SECTION_TEST_ID = (dataTestSubj: string) =>
+ `${dataTestSubj}LeftSection`;
+export const EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID = (dataTestSubj: string) =>
+ `${dataTestSubj}TitleIcon`;
+export const EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID = (dataTestSubj: string) =>
+ `${dataTestSubj}TitleLink`;
+export const EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID = (dataTestSubj: string) =>
+ `${dataTestSubj}TitleText`;
+export const EXPANDABLE_PANEL_HEADER_RIGHT_SECTION_TEST_ID = (dataTestSubj: string) =>
+ `${dataTestSubj}RightSection`;
+export const EXPANDABLE_PANEL_LOADING_TEST_ID = (dataTestSubj: string) => `${dataTestSubj}Loading`;
+export const EXPANDABLE_PANEL_CONTENT_TEST_ID = (dataTestSubj: string) => `${dataTestSubj}Content`;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.test.tsx
index ac425b18bbb7c..10a536f69c8d0 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.test.tsx
@@ -21,7 +21,11 @@ import { coreMock } from '@kbn/core/public/mocks';
import { mockCasesContext } from '@kbn/cases-plugin/public/mocks/mock_cases_context';
import { useTimelineEventsDetails } from '../../../containers/details';
import { allCasesPermissions } from '../../../../cases_test_utils';
-import { DEFAULT_ALERTS_INDEX, DEFAULT_PREVIEW_INDEX } from '../../../../../common/constants';
+import {
+ DEFAULT_ALERTS_INDEX,
+ DEFAULT_PREVIEW_INDEX,
+ ASSISTANT_FEATURE_ID,
+} from '../../../../../common/constants';
const ecsData: Ecs = {
_id: '1',
@@ -138,6 +142,13 @@ describe('event details panel component', () => {
(KibanaServices.get as jest.Mock).mockReturnValue(coreStartMock);
(useKibana as jest.Mock).mockReturnValue({
services: {
+ application: {
+ capabilities: {
+ [ASSISTANT_FEATURE_ID]: {
+ 'ai-assistant': true,
+ },
+ },
+ },
uiSettings: {
get: jest.fn().mockReturnValue([]),
},
diff --git a/x-pack/plugins/security_solution/server/lib/app_features/app_features.test.ts b/x-pack/plugins/security_solution/server/lib/app_features/app_features.test.ts
index 5de9c57e2938f..1951f6d8b00fa 100644
--- a/x-pack/plugins/security_solution/server/lib/app_features/app_features.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/app_features/app_features.test.ts
@@ -48,6 +48,25 @@ const CASES_APP_FEATURE_CONFIG = {
},
};
+const ASSISTANT_BASE_CONFIG = {
+ bar: 'bar',
+};
+
+const ASSISTANT_APP_FEATURE_CONFIG = {
+ 'test-assistant-feature': {
+ privileges: {
+ all: {
+ ui: ['test-assistant-capability'],
+ api: ['test-assistant-capability'],
+ },
+ read: {
+ ui: ['test-assistant-capability'],
+ api: ['test-assistant-capability'],
+ },
+ },
+ },
+};
+
jest.mock('./security_kibana_features', () => {
return {
getSecurityBaseKibanaFeature: jest.fn(() => SECURITY_BASE_CONFIG),
@@ -75,6 +94,20 @@ jest.mock('./security_cases_kibana_sub_features', () => {
};
});
+jest.mock('./security_assistant_kibana_features', () => {
+ return {
+ getAssistantBaseKibanaFeature: jest.fn(() => ASSISTANT_BASE_CONFIG),
+ getAssistantBaseKibanaSubFeatureIds: jest.fn(() => ['subFeature1']),
+ getAssistantAppFeaturesConfig: jest.fn(() => ASSISTANT_APP_FEATURE_CONFIG),
+ };
+});
+
+jest.mock('./security_assistant_kibana_sub_features', () => {
+ return {
+ assistantSubFeaturesMap: new Map([['subFeature1', { baz: 'baz' }]]),
+ };
+});
+
describe('AppFeatures', () => {
it('should register enabled kibana features', () => {
const featuresSetup = {
@@ -118,4 +151,25 @@ describe('AppFeatures', () => {
subFeatures: [{ baz: 'baz' }],
});
});
+
+ it('should register enabled assistant features', () => {
+ const featuresSetup = {
+ registerKibanaFeature: jest.fn(),
+ } as unknown as PluginSetupContract;
+
+ const appFeatureKeys = ['test-assistant-feature'] as unknown as AppFeatureKeys;
+
+ const appFeatures = new AppFeatures(
+ loggingSystemMock.create().get('mock'),
+ [] as unknown as ExperimentalFeatures
+ );
+ appFeatures.init(featuresSetup);
+ appFeatures.set(appFeatureKeys);
+
+ expect(featuresSetup.registerKibanaFeature).toHaveBeenCalledWith({
+ ...ASSISTANT_BASE_CONFIG,
+ ...ASSISTANT_APP_FEATURE_CONFIG['test-assistant-feature'],
+ subFeatures: [{ baz: 'baz' }],
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/server/lib/app_features/app_features.ts b/x-pack/plugins/security_solution/server/lib/app_features/app_features.ts
index edeec0d533a40..0b17f6d71d00d 100644
--- a/x-pack/plugins/security_solution/server/lib/app_features/app_features.ts
+++ b/x-pack/plugins/security_solution/server/lib/app_features/app_features.ts
@@ -22,9 +22,16 @@ import {
import { AppFeaturesConfigMerger } from './app_features_config_merger';
import { casesSubFeaturesMap } from './security_cases_kibana_sub_features';
import { securitySubFeaturesMap } from './security_kibana_sub_features';
+import { assistantSubFeaturesMap } from './security_assistant_kibana_sub_features';
+import {
+ getAssistantAppFeaturesConfig,
+ getAssistantBaseKibanaFeature,
+ getAssistantBaseKibanaSubFeatureIds,
+} from './security_assistant_kibana_features';
export class AppFeatures {
private securityFeatureConfigMerger: AppFeaturesConfigMerger;
+ private assistantFeatureConfigMerger: AppFeaturesConfigMerger;
private casesFeatureConfigMerger: AppFeaturesConfigMerger;
private appFeatures?: Set;
private featuresSetup?: FeaturesPluginSetup;
@@ -38,6 +45,10 @@ export class AppFeatures {
securitySubFeaturesMap
);
this.casesFeatureConfigMerger = new AppFeaturesConfigMerger(this.logger, casesSubFeaturesMap);
+ this.assistantFeatureConfigMerger = new AppFeaturesConfigMerger(
+ this.logger,
+ assistantSubFeaturesMap
+ );
}
public init(featuresSetup: FeaturesPluginSetup) {
@@ -98,6 +109,23 @@ export class AppFeatures {
this.logger.info(JSON.stringify(completeCasesAppFeatureConfig));
this.featuresSetup.registerKibanaFeature(completeCasesAppFeatureConfig);
+
+ // register security assistant Kibana features
+ const securityAssistantBaseKibanaFeature = getAssistantBaseKibanaFeature();
+ const securityAssistantBaseKibanaSubFeatureIds = getAssistantBaseKibanaSubFeatureIds();
+ const enabledAssistantAppFeaturesConfigs = this.getEnabledAppFeaturesConfigs(
+ getAssistantAppFeaturesConfig()
+ );
+ const completeAssistantAppFeatureConfig =
+ this.assistantFeatureConfigMerger.mergeAppFeatureConfigs(
+ securityAssistantBaseKibanaFeature,
+ securityAssistantBaseKibanaSubFeatureIds,
+ enabledAssistantAppFeaturesConfigs
+ );
+
+ this.logger.info(JSON.stringify(completeAssistantAppFeatureConfig));
+
+ this.featuresSetup.registerKibanaFeature(completeAssistantAppFeatureConfig);
}
private getEnabledAppFeaturesConfigs(
diff --git a/x-pack/plugins/security_solution/server/lib/app_features/security_assistant_kibana_features.ts b/x-pack/plugins/security_solution/server/lib/app_features/security_assistant_kibana_features.ts
new file mode 100644
index 0000000000000..1927591da202f
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/app_features/security_assistant_kibana_features.ts
@@ -0,0 +1,74 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server';
+import type { AppFeaturesAssistantConfig, BaseKibanaFeatureConfig } from './types';
+import { APP_ID, ASSISTANT_FEATURE_ID } from '../../../common/constants';
+import { AppFeatureAssistantKey } from '../../../common/types/app_features';
+import type { AssistantSubFeatureId } from './security_assistant_kibana_sub_features';
+
+export const getAssistantBaseKibanaFeature = (): BaseKibanaFeatureConfig => ({
+ id: ASSISTANT_FEATURE_ID,
+ name: i18n.translate(
+ 'xpack.securitySolution.featureRegistry.linkSecuritySolutionAssistantTitle',
+ {
+ defaultMessage: 'Elastic AI Assistant',
+ }
+ ),
+ order: 1100,
+ category: DEFAULT_APP_CATEGORIES.security,
+ app: [ASSISTANT_FEATURE_ID, 'kibana'],
+ catalogue: [APP_ID],
+ minimumLicense: 'enterprise',
+ privileges: {
+ all: {
+ api: [],
+ app: [ASSISTANT_FEATURE_ID, 'kibana'],
+ catalogue: [APP_ID],
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: [],
+ },
+ read: {
+ // No read-only mode currently supported
+ disabled: true,
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: [],
+ },
+ },
+});
+
+export const getAssistantBaseKibanaSubFeatureIds = (): AssistantSubFeatureId[] => [
+ // This is a sample sub-feature that can be used for future implementations
+ // AssistantSubFeatureId.createConversation,
+];
+
+/**
+ * Maps the AppFeatures keys to Kibana privileges that will be merged
+ * into the base privileges config for the Security app.
+ *
+ * Privileges can be added in different ways:
+ * - `privileges`: the privileges that will be added directly into the main Security Assistant feature.
+ * - `subFeatureIds`: the ids of the sub-features that will be added into the Assistant subFeatures entry.
+ * - `subFeaturesPrivileges`: the privileges that will be added into the existing Assistant subFeature with the privilege `id` specified.
+ */
+export const getAssistantAppFeaturesConfig = (): AppFeaturesAssistantConfig => ({
+ [AppFeatureAssistantKey.assistant]: {
+ privileges: {
+ all: {
+ ui: ['ai-assistant'],
+ },
+ },
+ },
+});
diff --git a/x-pack/plugins/security_solution/server/lib/app_features/security_assistant_kibana_sub_features.ts b/x-pack/plugins/security_solution/server/lib/app_features/security_assistant_kibana_sub_features.ts
new file mode 100644
index 0000000000000..bc495e8c24d60
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/app_features/security_assistant_kibana_sub_features.ts
@@ -0,0 +1,59 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+import type { SubFeatureConfig } from '@kbn/features-plugin/common';
+
+// This is a sample sub-feature that can be used for future implementations
+// @ts-expect-error unused variable
+const createConversationSubFeature: SubFeatureConfig = {
+ name: i18n.translate(
+ 'xpack.securitySolution.featureRegistry.assistant.createConversationSubFeatureName',
+ {
+ defaultMessage: 'Create Conversations',
+ }
+ ),
+ description: i18n.translate(
+ 'xpack.securitySolution.featureRegistry.subFeatures.assistant.description',
+ { defaultMessage: 'Create custom conversations.' }
+ ),
+ privilegeGroups: [
+ {
+ groupType: 'independent',
+ privileges: [
+ {
+ api: [],
+ id: 'create_conversation',
+ name: i18n.translate(
+ 'xpack.securitySolution.featureRegistry.assistant.createConversationSubFeatureDetails',
+ {
+ defaultMessage: 'Create conversations',
+ }
+ ),
+ includeIn: 'all',
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: ['createConversation'],
+ },
+ ],
+ },
+ ],
+};
+
+export enum AssistantSubFeatureId {
+ createConversation = 'createConversationSubFeature',
+}
+
+// Defines all the ordered Security Assistant subFeatures available
+export const assistantSubFeaturesMap = Object.freeze(
+ new Map([
+ // This is a sample sub-feature that can be used for future implementations
+ // [AssistantSubFeatureId.createConversation, createConversationSubFeature],
+ ])
+);
diff --git a/x-pack/plugins/security_solution/server/lib/app_features/types.ts b/x-pack/plugins/security_solution/server/lib/app_features/types.ts
index 67480b33a2089..e6a4fd8db0304 100644
--- a/x-pack/plugins/security_solution/server/lib/app_features/types.ts
+++ b/x-pack/plugins/security_solution/server/lib/app_features/types.ts
@@ -7,7 +7,11 @@
import type { KibanaFeatureConfig, SubFeaturePrivilegeConfig } from '@kbn/features-plugin/common';
import type { AppFeatureKey } from '../../../common';
-import type { AppFeatureSecurityKey, AppFeatureCasesKey } from '../../../common/types/app_features';
+import type {
+ AppFeatureSecurityKey,
+ AppFeatureCasesKey,
+ AppFeatureAssistantKey,
+} from '../../../common/types/app_features';
import type { RecursivePartial } from '../../../common/utility_types';
export type BaseKibanaFeatureConfig = Omit;
@@ -29,3 +33,7 @@ export type AppFeaturesCasesConfig = Record<
AppFeatureCasesKey,
AppFeatureKibanaConfig
>;
+export type AppFeaturesAssistantConfig = Record<
+ AppFeatureAssistantKey,
+ AppFeatureKibanaConfig
+>;
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/detections_role.json
index c1a62bf7ca31f..133083cec2601 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/detections_role.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/detections_admin/detections_role.json
@@ -32,6 +32,7 @@
"feature": {
"ml": ["all"],
"siem": ["all", "read_alerts", "crud_alerts"],
+ "securitySolutionAssistant": ["all"],
"securitySolutionCases": ["all"],
"actions": ["read"],
"builtInAlerts": ["all"],
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/detections_role.json
index 42ef9ba1122c7..23a1256dac4aa 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/detections_role.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter/detections_role.json
@@ -34,6 +34,7 @@
"feature": {
"ml": ["read"],
"siem": ["all", "read_alerts", "crud_alerts"],
+ "securitySolutionAssistant": ["all"],
"securitySolutionCases": ["all"],
"actions": ["read"],
"builtInAlerts": ["all"]
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter_no_actions/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter_no_actions/detections_role.json
index e8000d6bb50e7..6b392c18f8caa 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter_no_actions/detections_role.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/hunter_no_actions/detections_role.json
@@ -34,6 +34,7 @@
"feature": {
"ml": ["read"],
"siem": ["all", "read_alerts", "crud_alerts"],
+ "securitySolutionAssistant": ["all"],
"securitySolutionCases": ["all"],
"builtInAlerts": ["all"]
},
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/detections_role.json
index 88d863631a90b..17b6e45f8c72d 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/detections_role.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/platform_engineer/detections_role.json
@@ -38,6 +38,7 @@
"feature": {
"ml": ["all"],
"siem": ["all", "read_alerts", "crud_alerts"],
+ "securitySolutionAssistant": ["all"],
"securitySolutionCases": ["all"],
"actions": ["all"],
"builtInAlerts": ["all"]
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/reader/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/reader/detections_role.json
index 95be607cf7181..137091bc7f795 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/reader/detections_role.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/reader/detections_role.json
@@ -27,6 +27,7 @@
"feature": {
"ml": ["read"],
"siem": ["read", "read_alerts"],
+ "securitySolutionAssistant": ["none"],
"securitySolutionCases": ["read"],
"actions": ["read"],
"builtInAlerts": ["read"]
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_role.json
index ea1fb2bf1433f..dafe85548d4d0 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_role.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_role.json
@@ -37,6 +37,7 @@
"feature": {
"ml": ["read"],
"siem": ["all", "read_alerts", "crud_alerts"],
+ "securitySolutionAssistant": ["all"],
"securitySolutionCases": ["all"],
"actions": ["read"],
"builtInAlerts": ["all"]
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/detections_role.json
index 4ad6488d0b5ab..5e3aa868f6147 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/detections_role.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/soc_manager/detections_role.json
@@ -37,6 +37,7 @@
"feature": {
"ml": ["read"],
"siem": ["all", "read_alerts", "crud_alerts"],
+ "securitySolutionAssistant": ["all"],
"securitySolutionCases": ["all"],
"actions": ["all"],
"builtInAlerts": ["all"]
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_role.json
index 2f555bebbff90..d670fd9555f59 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_role.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_role.json
@@ -26,6 +26,7 @@
"feature": {
"ml": ["read"],
"siem": ["read", "read_alerts"],
+ "securitySolutionAssistant": ["all"],
"securitySolutionCases": ["read"],
"actions": ["read"],
"builtInAlerts": ["read"]
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_role.json
index f8216a613cb5a..4db91de93709a 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_role.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_role.json
@@ -31,6 +31,7 @@
"feature": {
"ml": ["read"],
"siem": ["read", "read_alerts"],
+ "securitySolutionAssistant": ["all"],
"securitySolutionCases": ["read"],
"actions": ["read"],
"builtInAlerts": ["read"]
diff --git a/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts b/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts
index 82f180d4a541e..779f874b266da 100644
--- a/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts
+++ b/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts
@@ -17,6 +17,7 @@ export const PLI_APP_FEATURES: PliAppFeatures = {
essentials: [AppFeatureKey.endpointHostManagement, AppFeatureKey.endpointPolicyManagement],
complete: [
AppFeatureKey.advancedInsights,
+ AppFeatureKey.assistant,
AppFeatureKey.investigationGuide,
AppFeatureKey.threatIntelligence,
AppFeatureKey.casesConnectors,
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index e1d6766eab2d1..10039c28b2c3c 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -399,7 +399,6 @@
"controls.controlGroup.manageControl.controlTypeSettings.formGroupDescription": "Paramètres personnalisés pour votre contrôle {controlType}.",
"controls.controlGroup.manageControl.controlTypeSettings.formGroupTitle": "Paramètres de {controlType}",
"controls.optionsList.controlAndPopover.exists": "{negate, plural, one {Existe} many {Existent} other {Existent}}",
- "controls.optionsList.errors.dataViewNotFound": "Impossible de localiser la vue de données : {dataViewId}",
"controls.optionsList.errors.fieldNotFound": "Impossible de localiser le champ : {fieldName}",
"controls.optionsList.popover.ariaLabel": "Fenêtre contextuelle pour le contrôle {fieldName}",
"controls.optionsList.popover.cardinalityLabel": "{totalOptions, number} {totalOptions, plural, one {option} many {options disponibles} other {options}}",
@@ -409,7 +408,6 @@
"controls.optionsList.popover.invalidSelectionsLabel": "{selectedOptions} {selectedOptions, plural, one {sélection ignorée} many {Sélections ignorées} other {sélections ignorées}}",
"controls.optionsList.popover.invalidSelectionsSectionTitle": "{invalidSelectionCount, plural, one {Sélection ignorée} many {Sélections ignorées} other {Sélections ignorées}}",
"controls.optionsList.popover.suggestionsAriaLabel": "{optionCount, plural, one {option disponible} many {options disponibles} other {options disponibles}} pour {fieldName}",
- "controls.rangeSlider.errors.dataViewNotFound": "Impossible de localiser la vue de données : {dataViewId}",
"controls.rangeSlider.errors.fieldNotFound": "Impossible de localiser le champ : {fieldName}",
"controls.controlGroup.emptyState.addControlButtonTitle": "Ajouter un contrôle",
"controls.controlGroup.emptyState.badgeText": "Nouveauté",
@@ -30066,7 +30064,6 @@
"xpack.securitySolution.flyout.correlations.relatedCasesHeading": "{count} associé {count, plural, one {cas} many {aux cas suivants} other {aux cas suivants}}",
"xpack.securitySolution.flyout.correlations.sessionAlertsHeading": "{count, plural, one {# alerte} many {# alertes} other {Alertes #}} associé(es) par session",
"xpack.securitySolution.flyout.correlations.sourceAlertsHeading": "{count, plural, one {# alerte} many {# alertes} other {Alertes #}} associé(es) par événement source",
- "xpack.securitySolution.flyout.documentDetails.overviewTab.viewAllButton": "Afficher tous les {text}",
"xpack.securitySolution.flyout.errorMessage": "Une erreur est survenue lors de l'affichage de {message}",
"xpack.securitySolution.flyout.errorTitle": "Impossible d'afficher {title}",
"xpack.securitySolution.footer.autoRefreshActiveTooltip": "Lorsque le rafraîchissement automatique est activé, la chronologie vous montre les {numberOfItems} derniers événements qui correspondent à votre requête.",
@@ -33336,7 +33333,6 @@
"xpack.securitySolution.flyout.correlations.timestampColumnTitle": "Horodatage",
"xpack.securitySolution.flyout.documentDetails.alertReasonTitle": "Raison d'alerte",
"xpack.securitySolution.flyout.documentDetails.analyzerGraphButton": "Graph Analyseur",
- "xpack.securitySolution.flyout.documentDetails.analyzerPreviewText": "aperçu de l'analyseur.",
"xpack.securitySolution.flyout.documentDetails.analyzerPreviewTitle": "Aperçu de l'analyseur",
"xpack.securitySolution.flyout.documentDetails.collapseDetailButton": "Réduire les détails de l'alerte",
"xpack.securitySolution.flyout.documentDetails.correlationsButton": "Corrélations",
@@ -33365,14 +33361,11 @@
"xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.sameSourceEventAlert": "alerte associée par le même événement source",
"xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.sameSourceEventAlerts": "alertes associées par le même événement source",
"xpack.securitySolution.flyout.documentDetails.overviewTab.correlationsText": "champs de corrélation",
- "xpack.securitySolution.flyout.documentDetails.overviewTab.entitiesText": "les entités sélectionnées",
"xpack.securitySolution.flyout.documentDetails.overviewTab.prevalenceRowText": "est inhabituel",
- "xpack.securitySolution.flyout.documentDetails.overviewTab.prevalenceText": "champs de prévalence",
"xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatEnrichment": "champ enrichi avec la Threat Intelligence",
"xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatEnrichments": "champs enrichis avec la Threat Intelligence",
"xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatMatch": "correspondance de menace détectée",
"xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatMatches": "correspondances de menaces détectées",
- "xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligenceText": "champs de Threat Intelligence",
"xpack.securitySolution.flyout.documentDetails.prevalenceButton": "Prévalence",
"xpack.securitySolution.flyout.documentDetails.prevalenceTitle": "Prévalence",
"xpack.securitySolution.flyout.documentDetails.riskScoreTitle": "Score de risque",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 86ed70f542dfc..07046aa334aa3 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -399,7 +399,6 @@
"controls.controlGroup.manageControl.controlTypeSettings.formGroupDescription": "{controlType}コントロールのカスタム設定",
"controls.controlGroup.manageControl.controlTypeSettings.formGroupTitle": "{controlType}設定",
"controls.optionsList.controlAndPopover.exists": "{negate, plural, other {存在します}}",
- "controls.optionsList.errors.dataViewNotFound": "データビューが見つかりませんでした:{dataViewId}",
"controls.optionsList.errors.fieldNotFound": "フィールドが見つかりませんでした:{fieldName}",
"controls.optionsList.popover.ariaLabel": "{fieldName}コントロールのポップオーバー",
"controls.optionsList.popover.cardinalityLabel": "{totalOptions, number}{totalOptions, plural, other {オプション}}",
@@ -409,7 +408,6 @@
"controls.optionsList.popover.invalidSelectionsLabel": "{selectedOptions}{selectedOptions, plural, other {選択項目}}が無視されました",
"controls.optionsList.popover.invalidSelectionsSectionTitle": "{invalidSelectionCount, plural, other {選択項目}}が無視されました",
"controls.optionsList.popover.suggestionsAriaLabel": "{fieldName}の{optionCount, plural, other {オプション}}があります",
- "controls.rangeSlider.errors.dataViewNotFound": "データビューが見つかりませんでした:{dataViewId}",
"controls.rangeSlider.errors.fieldNotFound": "フィールドが見つかりませんでした:{fieldName}",
"controls.controlGroup.emptyState.addControlButtonTitle": "コントロールを追加",
"controls.controlGroup.emptyState.badgeText": "新規",
@@ -30065,7 +30063,6 @@
"xpack.securitySolution.flyout.correlations.relatedCasesHeading": "{count}件の関連する{count, plural, other {ケース}}",
"xpack.securitySolution.flyout.correlations.sessionAlertsHeading": "セッションに関連する{count, plural, other {#件のアラート}}",
"xpack.securitySolution.flyout.correlations.sourceAlertsHeading": "ソースイベントに関連する{count, plural, other {#件のアラート}}",
- "xpack.securitySolution.flyout.documentDetails.overviewTab.viewAllButton": "すべての{text}を表示",
"xpack.securitySolution.flyout.errorMessage": "{message}の表示中にエラーが発生しました",
"xpack.securitySolution.flyout.errorTitle": "{title}を表示できません",
"xpack.securitySolution.footer.autoRefreshActiveTooltip": "自動更新が有効な間、タイムラインはクエリに一致する直近{numberOfItems}件のイベントを表示します。",
@@ -33335,7 +33332,6 @@
"xpack.securitySolution.flyout.correlations.timestampColumnTitle": "タイムスタンプ",
"xpack.securitySolution.flyout.documentDetails.alertReasonTitle": "アラートの理由",
"xpack.securitySolution.flyout.documentDetails.analyzerGraphButton": "アナライザーグラフ",
- "xpack.securitySolution.flyout.documentDetails.analyzerPreviewText": "アナライザープレビュー。",
"xpack.securitySolution.flyout.documentDetails.analyzerPreviewTitle": "アナライザープレビュー",
"xpack.securitySolution.flyout.documentDetails.collapseDetailButton": "アラート詳細を折りたたむ",
"xpack.securitySolution.flyout.documentDetails.correlationsButton": "相関関係",
@@ -33364,14 +33360,11 @@
"xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.sameSourceEventAlert": "同じソースイベントに関連するアラート",
"xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.sameSourceEventAlerts": "同じソースイベントに関連するアラート",
"xpack.securitySolution.flyout.documentDetails.overviewTab.correlationsText": "相関のフィールド",
- "xpack.securitySolution.flyout.documentDetails.overviewTab.entitiesText": "エンティティ",
"xpack.securitySolution.flyout.documentDetails.overviewTab.prevalenceRowText": "共通しない",
- "xpack.securitySolution.flyout.documentDetails.overviewTab.prevalenceText": "発生率のフィールド",
"xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatEnrichment": "脅威インテリジェンスで拡張されたフィールド",
"xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatEnrichments": "脅威インテリジェンスで拡張されたフィールド",
"xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatMatch": "脅威一致が検出されました",
"xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatMatches": "脅威一致が検出されました",
- "xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligenceText": "脅威インテリジェンスのフィールド",
"xpack.securitySolution.flyout.documentDetails.prevalenceButton": "発生率",
"xpack.securitySolution.flyout.documentDetails.prevalenceTitle": "発生率",
"xpack.securitySolution.flyout.documentDetails.riskScoreTitle": "リスクスコア",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 0ee3c9fbd860e..2a13938f0e939 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -399,7 +399,6 @@
"controls.controlGroup.manageControl.controlTypeSettings.formGroupDescription": "{controlType} 控件的定制设置。",
"controls.controlGroup.manageControl.controlTypeSettings.formGroupTitle": "{controlType} 设置",
"controls.optionsList.controlAndPopover.exists": "{negate, plural, other {存在}}",
- "controls.optionsList.errors.dataViewNotFound": "找不到数据视图:{dataViewId}",
"controls.optionsList.errors.fieldNotFound": "找不到字段:{fieldName}",
"controls.optionsList.popover.ariaLabel": "{fieldName} 控件的弹出框",
"controls.optionsList.popover.cardinalityLabel": "{totalOptions, number} 个{totalOptions, plural, other {选项}}",
@@ -409,7 +408,6 @@
"controls.optionsList.popover.invalidSelectionsLabel": "已忽略 {selectedOptions} 个{selectedOptions, plural, other {选择的内容}}",
"controls.optionsList.popover.invalidSelectionsSectionTitle": "已忽略{invalidSelectionCount, plural, other {选择的内容}}",
"controls.optionsList.popover.suggestionsAriaLabel": "{fieldName} 的可用{optionCount, plural, other {选项}}",
- "controls.rangeSlider.errors.dataViewNotFound": "找不到数据视图:{dataViewId}",
"controls.rangeSlider.errors.fieldNotFound": "找不到字段:{fieldName}",
"controls.controlGroup.emptyState.addControlButtonTitle": "添加控件",
"controls.controlGroup.emptyState.badgeText": "新建",
@@ -30061,7 +30059,6 @@
"xpack.securitySolution.flyout.correlations.relatedCasesHeading": "{count} 个相关{count, plural, other {案例}}",
"xpack.securitySolution.flyout.correlations.sessionAlertsHeading": "{count, plural, other {# 个告警}}与会话相关",
"xpack.securitySolution.flyout.correlations.sourceAlertsHeading": "{count, plural, other {# 个告警}}与源事件相关",
- "xpack.securitySolution.flyout.documentDetails.overviewTab.viewAllButton": "查看所有 {text}",
"xpack.securitySolution.flyout.errorMessage": "显示 {message} 时出现错误",
"xpack.securitySolution.flyout.errorTitle": "无法显示 {title}",
"xpack.securitySolution.footer.autoRefreshActiveTooltip": "自动刷新已启用时,时间线将显示匹配查询的最近 {numberOfItems} 个事件。",
@@ -33331,7 +33328,6 @@
"xpack.securitySolution.flyout.correlations.timestampColumnTitle": "时间戳",
"xpack.securitySolution.flyout.documentDetails.alertReasonTitle": "告警原因",
"xpack.securitySolution.flyout.documentDetails.analyzerGraphButton": "分析器图表",
- "xpack.securitySolution.flyout.documentDetails.analyzerPreviewText": "分析器预览。",
"xpack.securitySolution.flyout.documentDetails.analyzerPreviewTitle": "分析器预览",
"xpack.securitySolution.flyout.documentDetails.collapseDetailButton": "折叠告警详情",
"xpack.securitySolution.flyout.documentDetails.correlationsButton": "相关性",
@@ -33360,14 +33356,11 @@
"xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.sameSourceEventAlert": "告警与同一源事件相关",
"xpack.securitySolution.flyout.documentDetails.overviewTab.correlations.sameSourceEventAlerts": "告警与同一源事件相关",
"xpack.securitySolution.flyout.documentDetails.overviewTab.correlationsText": "相关性字段",
- "xpack.securitySolution.flyout.documentDetails.overviewTab.entitiesText": "实体",
"xpack.securitySolution.flyout.documentDetails.overviewTab.prevalenceRowText": "不常见",
- "xpack.securitySolution.flyout.documentDetails.overviewTab.prevalenceText": "普及率字段",
"xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatEnrichment": "已使用威胁情报扩充字段",
"xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatEnrichments": "已使用威胁情报扩充字段",
"xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatMatch": "检测到威胁匹配",
"xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatMatches": "检测到威胁匹配",
- "xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligenceText": "威胁情报字段",
"xpack.securitySolution.flyout.documentDetails.prevalenceButton": "普及率",
"xpack.securitySolution.flyout.documentDetails.prevalenceTitle": "普及率",
"xpack.securitySolution.flyout.documentDetails.riskScoreTitle": "风险分数",
diff --git a/x-pack/test/api_integration/apis/features/features/features.ts b/x-pack/test/api_integration/apis/features/features/features.ts
index fd91262d57a61..a80a39e4af5dc 100644
--- a/x-pack/test/api_integration/apis/features/features/features.ts
+++ b/x-pack/test/api_integration/apis/features/features/features.ts
@@ -125,6 +125,7 @@ export default function ({ getService }: FtrProviderContext) {
'uptime',
'siem',
'slo',
+ 'securitySolutionAssistant',
'securitySolutionCases',
'fleet',
'fleetv2',
diff --git a/x-pack/test/api_integration/apis/management/advanced_settings/feature_controls.ts b/x-pack/test/api_integration/apis/management/advanced_settings/feature_controls.ts
index ccc9b92ac8298..4af49a3991611 100644
--- a/x-pack/test/api_integration/apis/management/advanced_settings/feature_controls.ts
+++ b/x-pack/test/api_integration/apis/management/advanced_settings/feature_controls.ts
@@ -8,6 +8,10 @@
import expect from '@kbn/expect';
import { SuperTest } from 'supertest';
import { CSV_QUOTE_VALUES_SETTING } from '@kbn/share-plugin/common/constants';
+import {
+ ELASTIC_HTTP_VERSION_HEADER,
+ X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
+} from '@kbn/core-http-common';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function featureControlsTests({ getService }: FtrProviderContext) {
@@ -64,9 +68,11 @@ export default function featureControlsTests({ getService }: FtrProviderContext)
const basePath = spaceId ? `/s/${spaceId}` : '';
return await supertest
- .post(`${basePath}/api/telemetry/v2/optIn`)
+ .post(`${basePath}/internal/telemetry/optIn`)
.auth(username, password)
.set('kbn-xsrf', 'foo')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ enabled: true })
.then((response: any) => ({ error: undefined, response }))
.catch((error: any) => ({ error, response: undefined }));
diff --git a/x-pack/test/api_integration/apis/maps/maps_telemetry.ts b/x-pack/test/api_integration/apis/maps/maps_telemetry.ts
index 8dbfca9a40a77..4883f01b5ac6e 100644
--- a/x-pack/test/api_integration/apis/maps/maps_telemetry.ts
+++ b/x-pack/test/api_integration/apis/maps/maps_telemetry.ts
@@ -7,6 +7,10 @@
import expect from '@kbn/expect';
import { estypes } from '@elastic/elasticsearch';
+import {
+ ELASTIC_HTTP_VERSION_HEADER,
+ X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
+} from '@kbn/core-http-common';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
@@ -17,7 +21,9 @@ export default function ({ getService }: FtrProviderContext) {
const {
body: [{ stats: apiResponse }],
} = await supertest
- .post(`/api/telemetry/v2/clusters/_stats`)
+ .post(`/internal/telemetry/clusters/_stats`)
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.set('kbn-xsrf', 'xxxx')
.send({
unencrypted: true,
diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts
index ad9eb9b3bd6eb..d49df52bfcd1c 100644
--- a/x-pack/test/api_integration/apis/security/privileges.ts
+++ b/x-pack/test/api_integration/apis/security/privileges.ts
@@ -56,6 +56,7 @@ export default function ({ getService }: FtrProviderContext) {
'execute_operations_all',
],
uptime: ['all', 'read', 'minimal_all', 'minimal_read'],
+ securitySolutionAssistant: ['all', 'read', 'minimal_all', 'minimal_read'],
securitySolutionCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'],
infrastructure: ['all', 'read', 'minimal_all', 'minimal_read'],
logs: ['all', 'read', 'minimal_all', 'minimal_read'],
diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts
index 680bd9fd13298..c6982b3c6d53e 100644
--- a/x-pack/test/api_integration/apis/security/privileges_basic.ts
+++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts
@@ -42,6 +42,7 @@ export default function ({ getService }: FtrProviderContext) {
osquery: ['all', 'read', 'minimal_all', 'minimal_read'],
ml: ['all', 'read', 'minimal_all', 'minimal_read'],
siem: ['all', 'read', 'minimal_all', 'minimal_read'],
+ securitySolutionAssistant: ['all', 'read', 'minimal_all', 'minimal_read'],
securitySolutionCases: ['all', 'read', 'minimal_all', 'minimal_read'],
fleetv2: ['all', 'read', 'minimal_all', 'minimal_read'],
fleet: ['all', 'read', 'minimal_all', 'minimal_read'],
@@ -130,6 +131,7 @@ export default function ({ getService }: FtrProviderContext) {
'execute_operations_all',
],
uptime: ['all', 'read', 'minimal_all', 'minimal_read'],
+ securitySolutionAssistant: ['all', 'read', 'minimal_all', 'minimal_read'],
securitySolutionCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'],
infrastructure: ['all', 'read', 'minimal_all', 'minimal_read'],
logs: ['all', 'read', 'minimal_all', 'minimal_read'],
diff --git a/x-pack/test/api_integration/apis/telemetry/telemetry.ts b/x-pack/test/api_integration/apis/telemetry/telemetry.ts
index 2d02f0a976421..601e2fddbd83e 100644
--- a/x-pack/test/api_integration/apis/telemetry/telemetry.ts
+++ b/x-pack/test/api_integration/apis/telemetry/telemetry.ts
@@ -21,6 +21,10 @@ import type {
CacheDetails,
} from '@kbn/telemetry-collection-manager-plugin/server/types';
import { assertTelemetryPayload } from '@kbn/telemetry-tools';
+import {
+ ELASTIC_HTTP_VERSION_HEADER,
+ X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
+} from '@kbn/core-http-common';
import basicClusterFixture from './fixtures/basiccluster.json';
import multiClusterFixture from './fixtures/multicluster.json';
import type { SecurityService } from '../../../../../test/common/services/security/security';
@@ -97,7 +101,7 @@ export default function ({ getService }: FtrProviderContext) {
const esSupertest = getService('esSupertest');
const security = getService('security');
- describe('/api/telemetry/v2/clusters/_stats', () => {
+ describe('/internal/telemetry/clusters/_stats', () => {
const timestamp = new Date().toISOString();
describe('monitoring/multicluster', () => {
let localXPack: Record;
@@ -112,8 +116,10 @@ export default function ({ getService }: FtrProviderContext) {
await updateMonitoringDates(esSupertest, fromTimestamp, toTimestamp, timestamp);
const { body }: { body: UnencryptedTelemetryPayload } = await supertest
- .post('/api/telemetry/v2/clusters/_stats')
+ .post('/internal/telemetry/clusters/_stats')
.set('kbn-xsrf', 'xxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: true, refreshCache: true })
.expect(200);
@@ -167,8 +173,10 @@ export default function ({ getService }: FtrProviderContext) {
after(() => esArchiver.unload(archive));
it('should load non-expiring basic cluster', async () => {
const { body }: { body: UnencryptedTelemetryPayload } = await supertest
- .post('/api/telemetry/v2/clusters/_stats')
+ .post('/internal/telemetry/clusters/_stats')
.set('kbn-xsrf', 'xxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: true, refreshCache: true })
.expect(200);
@@ -193,8 +201,10 @@ export default function ({ getService }: FtrProviderContext) {
await updateMonitoringDates(esSupertest, fromTimestamp, toTimestamp, timestamp);
// hit the endpoint to cache results
await supertest
- .post('/api/telemetry/v2/clusters/_stats')
+ .post('/internal/telemetry/clusters/_stats')
.set('kbn-xsrf', 'xxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: true, refreshCache: true })
.expect(200);
});
@@ -204,8 +214,10 @@ export default function ({ getService }: FtrProviderContext) {
it('returns non-cached results when unencrypted', async () => {
const now = Date.now();
const { body }: { body: UnencryptedTelemetryPayload } = await supertest
- .post('/api/telemetry/v2/clusters/_stats')
+ .post('/internal/telemetry/clusters/_stats')
.set('kbn-xsrf', 'xxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: true })
.expect(200);
@@ -224,8 +236,10 @@ export default function ({ getService }: FtrProviderContext) {
it('grabs a fresh copy on refresh', async () => {
const now = Date.now();
const { body }: { body: UnencryptedTelemetryPayload } = await supertest
- .post('/api/telemetry/v2/clusters/_stats')
+ .post('/internal/telemetry/clusters/_stats')
.set('kbn-xsrf', 'xxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: true, refreshCache: true })
.expect(200);
@@ -243,16 +257,20 @@ export default function ({ getService }: FtrProviderContext) {
describe('superadmin user', () => {
it('should return unencrypted telemetry for the admin user', async () => {
await supertest
- .post('/api/telemetry/v2/clusters/_stats')
+ .post('/internal/telemetry/clusters/_stats')
.set('kbn-xsrf', 'xxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: true })
.expect(200);
});
it('should return encrypted telemetry for the admin user', async () => {
await supertest
- .post('/api/telemetry/v2/clusters/_stats')
+ .post('/internal/telemetry/clusters/_stats')
.set('kbn-xsrf', 'xxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: false })
.expect(200);
});
@@ -281,18 +299,22 @@ export default function ({ getService }: FtrProviderContext) {
it('should return encrypted telemetry for the global-read user', async () => {
await supertestWithoutAuth
- .post('/api/telemetry/v2/clusters/_stats')
+ .post('/internal/telemetry/clusters/_stats')
.auth(globalReadOnlyUser, password(globalReadOnlyUser))
.set('kbn-xsrf', 'xxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: false })
.expect(200);
});
it('should return unencrypted telemetry for the global-read user', async () => {
await supertestWithoutAuth
- .post('/api/telemetry/v2/clusters/_stats')
+ .post('/internal/telemetry/clusters/_stats')
.auth(globalReadOnlyUser, password(globalReadOnlyUser))
.set('kbn-xsrf', 'xxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: true })
.expect(200);
});
@@ -330,18 +352,22 @@ export default function ({ getService }: FtrProviderContext) {
it('should return encrypted telemetry for the read-only user', async () => {
await supertestWithoutAuth
- .post('/api/telemetry/v2/clusters/_stats')
+ .post('/internal/telemetry/clusters/_stats')
.auth(noGlobalUser, password(noGlobalUser))
.set('kbn-xsrf', 'xxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: false })
.expect(200);
});
it('should return 403 when the read-only user requests unencrypted telemetry', async () => {
await supertestWithoutAuth
- .post('/api/telemetry/v2/clusters/_stats')
+ .post('/internal/telemetry/clusters/_stats')
.auth(noGlobalUser, password(noGlobalUser))
.set('kbn-xsrf', 'xxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: true })
.expect(403);
});
diff --git a/x-pack/test/api_integration/apis/telemetry/telemetry_local.ts b/x-pack/test/api_integration/apis/telemetry/telemetry_local.ts
index f0bb15d29b87b..51e60c2e22bd1 100644
--- a/x-pack/test/api_integration/apis/telemetry/telemetry_local.ts
+++ b/x-pack/test/api_integration/apis/telemetry/telemetry_local.ts
@@ -12,6 +12,10 @@ import ossPluginsTelemetrySchema from '@kbn/telemetry-plugin/schema/oss_plugins.
import xpackRootTelemetrySchema from '@kbn/telemetry-collection-xpack-plugin/schema/xpack_root.json';
import xpackPluginsTelemetrySchema from '@kbn/telemetry-collection-xpack-plugin/schema/xpack_plugins.json';
import { assertTelemetryPayload } from '@kbn/telemetry-tools';
+import {
+ ELASTIC_HTTP_VERSION_HEADER,
+ X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
+} from '@kbn/core-http-common';
import { flatKeys } from '../../../../../test/api_integration/apis/telemetry/utils';
import type { FtrProviderContext } from '../../ftr_provider_context';
@@ -31,7 +35,7 @@ export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const es = getService('es');
- describe('/api/telemetry/v2/clusters/_stats with monitoring disabled', () => {
+ describe('/internal/telemetry/clusters/_stats with monitoring disabled', () => {
let stats: Record;
before('disable monitoring and pull local stats', async () => {
@@ -39,8 +43,10 @@ export default function ({ getService }: FtrProviderContext) {
await new Promise((r) => setTimeout(r, 1000));
const { body } = await supertest
- .post('/api/telemetry/v2/clusters/_stats')
+ .post('/internal/telemetry/clusters/_stats')
.set('kbn-xsrf', 'xxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: true, refreshCache: true })
.expect(200);
diff --git a/x-pack/test/api_integration/services/usage_api.ts b/x-pack/test/api_integration/services/usage_api.ts
index fbcddfb3dc512..500212d96ddfc 100644
--- a/x-pack/test/api_integration/services/usage_api.ts
+++ b/x-pack/test/api_integration/services/usage_api.ts
@@ -6,6 +6,10 @@
*/
import { UsageStatsPayload } from '@kbn/telemetry-collection-manager-plugin/server';
+import {
+ ELASTIC_HTTP_VERSION_HEADER,
+ X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
+} from '@kbn/core-http-common';
import { FtrProviderContext } from '../ftr_provider_context';
export interface UsageStatsPayloadTestFriendly extends UsageStatsPayload {
@@ -29,9 +33,10 @@ export function UsageAPIProvider({ getService }: FtrProviderContext) {
refreshCache?: boolean;
}): Promise> {
const { body } = await supertest
- .post('/api/telemetry/v2/clusters/_stats')
+ .post('/internal/telemetry/clusters/_stats')
.set('kbn-xsrf', 'xxx')
- .set('x-elastic-internal-origin', 'xxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ refreshCache: true, ...payload })
.expect(200);
return body;
diff --git a/x-pack/test/cloud_security_posture_api/telemetry/telemetry.ts b/x-pack/test/cloud_security_posture_api/telemetry/telemetry.ts
index 797a88881a87b..e5867ccd74897 100644
--- a/x-pack/test/cloud_security_posture_api/telemetry/telemetry.ts
+++ b/x-pack/test/cloud_security_posture_api/telemetry/telemetry.ts
@@ -6,7 +6,10 @@
*/
import expect from '@kbn/expect';
-import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
+import {
+ ELASTIC_HTTP_VERSION_HEADER,
+ X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
+} from '@kbn/core-http-common';
import { data, MockTelemetryFindings } from './data';
import type { FtrProviderContext } from '../ftr_provider_context';
@@ -67,7 +70,9 @@ export default function ({ getService }: FtrProviderContext) {
const {
body: [{ stats: apiResponse }],
} = await supertest
- .post(`/api/telemetry/v2/clusters/_stats`)
+ .post(`/internal/telemetry/clusters/_stats`)
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.set('kbn-xsrf', 'xxxx')
.send({
unencrypted: true,
@@ -119,8 +124,10 @@ export default function ({ getService }: FtrProviderContext) {
const {
body: [{ stats: apiResponse }],
} = await supertest
- .post(`/api/telemetry/v2/clusters/_stats`)
+ .post(`/internal/telemetry/clusters/_stats`)
.set('kbn-xsrf', 'xxxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({
unencrypted: true,
refreshCache: true,
@@ -164,8 +171,10 @@ export default function ({ getService }: FtrProviderContext) {
const {
body: [{ stats: apiResponse }],
} = await supertest
- .post(`/api/telemetry/v2/clusters/_stats`)
+ .post(`/internal/telemetry/clusters/_stats`)
.set('kbn-xsrf', 'xxxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({
unencrypted: true,
refreshCache: true,
@@ -240,8 +249,10 @@ export default function ({ getService }: FtrProviderContext) {
const {
body: [{ stats: apiResponse }],
} = await supertest
- .post(`/api/telemetry/v2/clusters/_stats`)
+ .post(`/internal/telemetry/clusters/_stats`)
.set('kbn-xsrf', 'xxxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({
unencrypted: true,
refreshCache: true,
@@ -294,8 +305,10 @@ export default function ({ getService }: FtrProviderContext) {
const {
body: [{ stats: apiResponse }],
} = await supertest
- .post(`/api/telemetry/v2/clusters/_stats`)
+ .post(`/internal/telemetry/clusters/_stats`)
.set('kbn-xsrf', 'xxxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({
unencrypted: true,
refreshCache: true,
diff --git a/x-pack/test/detection_engine_api_integration/utils/get_stats.ts b/x-pack/test/detection_engine_api_integration/utils/get_stats.ts
index 0871012f8749f..7f4a2bddbd833 100644
--- a/x-pack/test/detection_engine_api_integration/utils/get_stats.ts
+++ b/x-pack/test/detection_engine_api_integration/utils/get_stats.ts
@@ -8,6 +8,10 @@
import type { ToolingLog } from '@kbn/tooling-log';
import type SuperTest from 'supertest';
import type { DetectionMetrics } from '@kbn/security-solution-plugin/server/usage/detections/types';
+import {
+ ELASTIC_HTTP_VERSION_HEADER,
+ X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
+} from '@kbn/core-http-common';
import { getStatsUrl } from './get_stats_url';
import { getDetectionMetricsFromBody } from './get_detection_metrics_from_body';
@@ -24,6 +28,8 @@ export const getStats = async (
const response = await supertest
.post(getStatsUrl())
.set('kbn-xsrf', 'true')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: true, refreshCache: true });
if (response.status !== 200) {
log.error(
diff --git a/x-pack/test/detection_engine_api_integration/utils/get_stats_url.ts b/x-pack/test/detection_engine_api_integration/utils/get_stats_url.ts
index ac6537f670f77..1cd397df92267 100644
--- a/x-pack/test/detection_engine_api_integration/utils/get_stats_url.ts
+++ b/x-pack/test/detection_engine_api_integration/utils/get_stats_url.ts
@@ -8,4 +8,4 @@
/**
* Cluster stats URL. Replace this with any from kibana core if there is ever a constant there for this.
*/
-export const getStatsUrl = (): string => '/api/telemetry/v2/clusters/_stats';
+export const getStatsUrl = (): string => '/internal/telemetry/clusters/_stats';
diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts
index 47d3f93fd8ddf..90426d9bdfa3e 100644
--- a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts
+++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts
@@ -119,7 +119,6 @@ export default function (providerContext: FtrProviderContext) {
expect(body.item.is_managed).to.equal(false);
expect(body.item.inactivity_timeout).to.equal(1209600);
expect(body.item.status).to.be('active');
- expect(body.item.is_protected).to.equal(false);
});
it('sets given is_managed value', async () => {
@@ -445,13 +444,13 @@ export default function (providerContext: FtrProviderContext) {
status: 'active',
description: 'Test',
is_managed: false,
- is_protected: false,
namespace: 'default',
monitoring_enabled: ['logs', 'metrics'],
revision: 1,
schema_version: FLEET_AGENT_POLICIES_SCHEMA_VERSION,
updated_by: 'elastic',
package_policies: [],
+ is_protected: false,
});
});
@@ -732,7 +731,7 @@ export default function (providerContext: FtrProviderContext) {
name: 'Updated name',
description: 'Updated description',
namespace: 'default',
- is_protected: true,
+ is_protected: false,
})
.expect(200);
createdPolicyIds.push(updatedPolicy.id);
@@ -750,7 +749,7 @@ export default function (providerContext: FtrProviderContext) {
updated_by: 'elastic',
inactivity_timeout: 1209600,
package_policies: [],
- is_protected: true,
+ is_protected: false,
});
});
diff --git a/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts b/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts
index 04b8d80fdbee1..3efb072907fac 100644
--- a/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts
+++ b/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts
@@ -5,6 +5,10 @@
* 2.0.
*/
+import {
+ ELASTIC_HTTP_VERSION_HEADER,
+ X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
+} from '@kbn/core-http-common';
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../api_integration/ftr_provider_context';
import { skipIfNoDockerRegistry, generateAgent } from '../helpers';
@@ -124,8 +128,10 @@ export default function (providerContext: FtrProviderContext) {
const {
body: [{ stats: apiResponse }],
} = await supertest
- .post(`/api/telemetry/v2/clusters/_stats`)
+ .post(`/internal/telemetry/clusters/_stats`)
.set('kbn-xsrf', 'xxxx')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({
unencrypted: true,
refreshCache: true,
diff --git a/x-pack/test/fleet_api_integration/apis/policy_secrets.ts b/x-pack/test/fleet_api_integration/apis/policy_secrets.ts
index 52b614f389ba9..63878420084a2 100644
--- a/x-pack/test/fleet_api_integration/apis/policy_secrets.ts
+++ b/x-pack/test/fleet_api_integration/apis/policy_secrets.ts
@@ -12,6 +12,7 @@
import type { Client } from '@elastic/elasticsearch';
import expect from '@kbn/expect';
import { FullAgentPolicy } from '@kbn/fleet-plugin/common';
+import { GLOBAL_SETTINGS_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common/constants';
import { v4 as uuidv4 } from 'uuid';
import { FtrProviderContext } from '../../api_integration/ftr_provider_context';
import { skipIfNoDockerRegistry } from '../helpers';
@@ -50,6 +51,126 @@ export default function (providerContext: FtrProviderContext) {
const supertest = getService('supertest');
const kibanaServer = getService('kibanaServer');
+ const createFleetServerAgentPolicy = async () => {
+ const agentPolicyResponse = await supertest
+ .post(`/api/fleet/agent_policies`)
+ .set('kbn-xsrf', 'xxx')
+ .send({
+ name: `Fleet server policy ${uuidv4()}`,
+ namespace: 'default',
+ })
+ .expect(200);
+
+ const agentPolicyId = agentPolicyResponse.body.item.id;
+
+ // create fleet_server package policy
+ await supertest
+ .post(`/api/fleet/package_policies`)
+ .set('kbn-xsrf', 'xxx')
+ .send({
+ force: true,
+ package: {
+ name: 'fleet_server',
+ version: '1.3.1',
+ },
+ name: `Fleet Server ${uuidv4()}`,
+ namespace: 'default',
+ policy_id: agentPolicyId,
+ vars: {},
+ inputs: {
+ 'fleet_server-fleet-server': {
+ enabled: true,
+ vars: {
+ custom: '',
+ },
+ streams: {},
+ },
+ },
+ })
+ .expect(200);
+
+ return agentPolicyId;
+ };
+
+ const createPolicyWithSecrets = async () => {
+ return supertest
+ .post(`/api/fleet/package_policies`)
+ .set('kbn-xsrf', 'xxxx')
+ .send({
+ name: `secrets-${Date.now()}`,
+ description: '',
+ namespace: 'default',
+ policy_id: agentPolicyId,
+ inputs: {
+ 'secrets-test_input': {
+ enabled: true,
+ vars: {
+ input_var_secret: 'input_secret_val',
+ },
+ streams: {
+ 'secrets.log': {
+ enabled: true,
+ vars: {
+ stream_var_secret: 'stream_secret_val',
+ },
+ },
+ },
+ },
+ },
+ vars: {
+ package_var_secret: 'package_secret_val',
+ },
+ package: {
+ name: 'secrets',
+ version: '1.0.0',
+ },
+ })
+ .expect(200);
+ };
+
+ const createFleetServerAgent = async (
+ agentPolicyId: string,
+ hostname: string,
+ agentVersion: string
+ ) => {
+ const agentResponse = await es.index({
+ index: '.fleet-agents',
+ refresh: true,
+ body: {
+ access_api_key_id: 'api-key-3',
+ active: true,
+ policy_id: agentPolicyId,
+ type: 'PERMANENT',
+ local_metadata: {
+ host: { hostname },
+ elastic: { agent: { version: agentVersion } },
+ },
+ user_provided_metadata: {},
+ enrolled_at: '2022-06-21T12:14:25Z',
+ last_checkin: '2022-06-27T12:28:29Z',
+ tags: ['tag1'],
+ },
+ });
+
+ return agentResponse._id;
+ };
+
+ const clearAgents = async () => {
+ try {
+ await es.deleteByQuery({
+ index: '.fleet-agents',
+ refresh: true,
+ body: {
+ query: {
+ match_all: {},
+ },
+ },
+ });
+ } catch (err) {
+ // index doesn't exist
+ }
+ };
+
const getSecrets = async (ids?: string[]) => {
const query = ids ? { terms: { _id: ids } } : { match_all: {} };
return es.search({
@@ -71,7 +192,7 @@ export default function (providerContext: FtrProviderContext) {
},
});
} catch (err) {
- // index doesnt exis
+ // index doesn't exist
}
};
@@ -80,6 +201,36 @@ export default function (providerContext: FtrProviderContext) {
return body.item;
};
+ const enableSecrets = async () => {
+ try {
+ await kibanaServer.savedObjects.update({
+ type: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
+ id: 'fleet-default-settings',
+ attributes: {
+ secret_storage_requirements_met: true,
+ },
+ overwrite: false,
+ });
+ } catch (e) {
+ throw e;
+ }
+ };
+
+ const disableSecrets = async () => {
+ try {
+ await kibanaServer.savedObjects.update({
+ type: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
+ id: 'fleet-default-settings',
+ attributes: {
+ secret_storage_requirements_met: false,
+ },
+ overwrite: false,
+ });
+ } catch (e) {
+ throw e;
+ }
+ };
+
const getFullAgentPolicyById = async (id: string) => {
const { body } = await supertest.get(`/api/fleet/agent_policies/${id}/full`).expect(200);
return body.item;
@@ -141,10 +292,13 @@ export default function (providerContext: FtrProviderContext) {
skipIfNoDockerRegistry(providerContext);
let agentPolicyId: string;
+ let fleetServerAgentPolicyId: string;
before(async () => {
await kibanaServer.savedObjects.cleanStandardList();
await deleteAllSecrets();
+ await clearAgents();
+ await enableSecrets();
});
setupFleetAndAgents(providerContext);
@@ -160,6 +314,8 @@ export default function (providerContext: FtrProviderContext) {
.expect(200);
agentPolicyId = agentPolicyResponse.item.id;
+
+ fleetServerAgentPolicyId = await createFleetServerAgentPolicy();
});
after(async () => {
@@ -427,5 +583,67 @@ export default function (providerContext: FtrProviderContext) {
expect(searchRes.hits.hits.length).to.eql(0);
});
+
+ it('should not store secrets if fleet server does not meet minimum version', async () => {
+ await createFleetServerAgent(fleetServerAgentPolicyId, 'server_1', '7.0.0');
+ await disableSecrets();
+
+ const createdPolicy = await createPolicyWSecretVar();
+
+ // secret should be in plain text i.e not a secret refrerence
+ expect(createdPolicy.vars.package_var_secret.value).eql('package_secret_val');
+ });
+
+ async function createPolicyWSecretVar() {
+ const { body: createResBody } = await createPolicyWithSecrets();
+ const createdPolicy = createResBody.item;
+ return createdPolicy;
+ }
+
+ it('should not store secrets if there are no fleet servers', async () => {
+ await clearAgents();
+
+ const { body: createResBody } = await createPolicyWithSecrets();
+
+ const createdPolicy = createResBody.item;
+
+ // secret should be in plain text i.e not a secret refrerence
+ expect(createdPolicy.vars.package_var_secret.value).eql('package_secret_val');
+ });
+
+ it('should convert plain text values to secrets once fleet server requirements are met', async () => {
+ await clearAgents();
+
+ const createdPolicy = await createPolicyWSecretVar();
+
+ await createFleetServerAgent(fleetServerAgentPolicyId, 'server_2', '9.0.0');
+
+ const updatedPolicy = createdPolicyToUpdatePolicy(createdPolicy);
+ delete updatedPolicy.name;
+
+ updatedPolicy.vars.package_var_secret.value = 'package_secret_val_2';
+
+ const updateRes = await supertest
+ .put(`/api/fleet/package_policies/${createdPolicy.id}`)
+ .set('kbn-xsrf', 'xxxx')
+ .send(updatedPolicy)
+ .expect(200);
+
+ const updatedPolicyRes = updateRes.body.item;
+
+ expect(updatedPolicyRes.vars.package_var_secret.value.isSecretRef).eql(true);
+ expect(updatedPolicyRes.inputs[0].vars.input_var_secret.value.isSecretRef).eql(true);
+ expect(updatedPolicyRes.inputs[0].streams[0].vars.stream_var_secret.value.isSecretRef).eql(
+ true
+ );
+ });
+
+ it('should not revert to plaintext values if the user adds an out of date fleet server', async () => {
+ await createFleetServerAgent(fleetServerAgentPolicyId, 'server_3', '7.0.0');
+
+ const createdPolicy = await createPolicyWSecretVar();
+
+ expect(createdPolicy.vars.package_var_secret.value.isSecretRef).eql(true);
+ });
});
}
diff --git a/x-pack/test/functional/apps/infra/logs_source_configuration.ts b/x-pack/test/functional/apps/infra/logs_source_configuration.ts
index 8166af7848275..daf6296ed2c2c 100644
--- a/x-pack/test/functional/apps/infra/logs_source_configuration.ts
+++ b/x-pack/test/functional/apps/infra/logs_source_configuration.ts
@@ -6,6 +6,10 @@
*/
import expect from '@kbn/expect';
+import {
+ ELASTIC_HTTP_VERSION_HEADER,
+ X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
+} from '@kbn/core-http-common';
import { DATES } from './constants';
import { FtrProviderContext } from '../../ftr_provider_context';
@@ -133,8 +137,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await logsUi.logStreamPage.getStreamEntries();
const [{ stats }] = await supertest
- .post(`/api/telemetry/v2/clusters/_stats`)
+ .post(`/internal/telemetry/clusters/_stats`)
.set(COMMON_REQUEST_HEADERS)
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.set('Accept', 'application/json')
.send({
unencrypted: true,
diff --git a/x-pack/test/functional_execution_context/tests/server.ts b/x-pack/test/functional_execution_context/tests/server.ts
index 1d854fed2b94d..64035a0077966 100644
--- a/x-pack/test/functional_execution_context/tests/server.ts
+++ b/x-pack/test/functional_execution_context/tests/server.ts
@@ -5,6 +5,10 @@
* 2.0.
*/
+import {
+ ELASTIC_HTTP_VERSION_HEADER,
+ X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
+} from '@kbn/core-http-common';
import expect from '@kbn/expect';
import type { FtrProviderContext } from '../ftr_provider_context';
import { assertLogContains, isExecutionContextLog, ANY } from '../test_utils';
@@ -111,8 +115,10 @@ export default function ({ getService }: FtrProviderContext) {
it('propagates context for Telemetry collection', async () => {
await supertest
- .post('/api/telemetry/v2/clusters/_stats')
+ .post('/internal/telemetry/clusters/_stats')
.set('kbn-xsrf', 'true')
+ .set(ELASTIC_HTTP_VERSION_HEADER, '2')
+ .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: false })
.expect(200);
diff --git a/x-pack/test/scalability/apis/api.telemetry.cluster_stats.1600_dataviews.json b/x-pack/test/scalability/apis/api.telemetry.cluster_stats.1600_dataviews.json
index 621bd21556d66..485208916d48e 100644
--- a/x-pack/test/scalability/apis/api.telemetry.cluster_stats.1600_dataviews.json
+++ b/x-pack/test/scalability/apis/api.telemetry.cluster_stats.1600_dataviews.json
@@ -1,5 +1,5 @@
{
- "journeyName": "POST /api/telemetry/v2/clusters/_stats - 1600 dataviews",
+ "journeyName": "POST /internal/telemetry/clusters/_stats - 1600 dataviews",
"scalabilitySetup": {
"warmup": [
{
@@ -30,13 +30,15 @@
{
"http": {
"method": "POST",
- "path": "/api/telemetry/v2/clusters/_stats",
+ "path": "/internal/telemetry/clusters/_stats",
"body": "{}",
"headers": {
"Cookie": "",
"Kbn-Version": "",
"Accept-Encoding": "gzip, deflate, br",
- "Content-Type": "application/json"
+ "Content-Type": "application/json",
+ "elastic-api-version": "2",
+ "x-elastic-internal-origin": "kibana"
},
"statusCode": 200
}
diff --git a/x-pack/test/scalability/apis/api.telemetry.cluster_stats.json b/x-pack/test/scalability/apis/api.telemetry.cluster_stats.json
index eb5bf0808d3ea..041fb1fae31ea 100644
--- a/x-pack/test/scalability/apis/api.telemetry.cluster_stats.json
+++ b/x-pack/test/scalability/apis/api.telemetry.cluster_stats.json
@@ -1,5 +1,5 @@
{
- "journeyName": "POST /api/telemetry/v2/clusters/_stats",
+ "journeyName": "POST /internal/telemetry/clusters/_stats",
"scalabilitySetup": {
"warmup": [
{
@@ -28,13 +28,15 @@
{
"http": {
"method": "POST",
- "path": "/api/telemetry/v2/clusters/_stats",
+ "path": "/internal/telemetry/clusters/_stats",
"body": "{}",
"headers": {
"Cookie": "",
"Kbn-Version": "",
"Accept-Encoding": "gzip, deflate, br",
- "Content-Type": "application/json"
+ "Content-Type": "application/json",
+ "elastic-api-version": "2",
+ "x-elastic-internal-origin": "kibana"
},
"statusCode": 200
}
diff --git a/x-pack/test/scalability/apis/api.telemetry.cluster_stats.no_cache.1600_dataviews.json b/x-pack/test/scalability/apis/api.telemetry.cluster_stats.no_cache.1600_dataviews.json
index 191d01c6a7424..2a3095447e8b4 100644
--- a/x-pack/test/scalability/apis/api.telemetry.cluster_stats.no_cache.1600_dataviews.json
+++ b/x-pack/test/scalability/apis/api.telemetry.cluster_stats.no_cache.1600_dataviews.json
@@ -1,5 +1,5 @@
{
- "journeyName": "POST /api/telemetry/v2/clusters/_stats - no cache - 1600 dataviews",
+ "journeyName": "POST /internal/telemetry/clusters/_stats - no cache - 1600 dataviews",
"scalabilitySetup": {
"responseTimeThreshold": {
"threshold1": 1000,
@@ -35,13 +35,15 @@
{
"http": {
"method": "POST",
- "path": "/api/telemetry/v2/clusters/_stats",
+ "path": "/internal/telemetry/clusters/_stats",
"body": "{ \"refreshCache\": true }",
"headers": {
"Cookie": "",
"Kbn-Version": "",
"Accept-Encoding": "gzip, deflate, br",
- "Content-Type": "application/json"
+ "Content-Type": "application/json",
+ "elastic-api-version": "2",
+ "x-elastic-internal-origin": "kibana"
},
"timeout": 240000,
"statusCode": 200
diff --git a/x-pack/test/scalability/apis/api.telemetry.cluster_stats.no_cache.json b/x-pack/test/scalability/apis/api.telemetry.cluster_stats.no_cache.json
index b3099941180a3..c0521ffb2607b 100644
--- a/x-pack/test/scalability/apis/api.telemetry.cluster_stats.no_cache.json
+++ b/x-pack/test/scalability/apis/api.telemetry.cluster_stats.no_cache.json
@@ -1,5 +1,5 @@
{
- "journeyName": "POST /api/telemetry/v2/clusters/_stats - no cache",
+ "journeyName": "POST /internal/telemetry/clusters/_stats - no cache",
"scalabilitySetup": {
"responseTimeThreshold": {
"threshold1": 1000,
@@ -33,13 +33,15 @@
{
"http": {
"method": "POST",
- "path": "/api/telemetry/v2/clusters/_stats",
+ "path": "/internal/telemetry/clusters/_stats",
"body": "{ \"refreshCache\": true }",
"headers": {
"Cookie": "",
"Kbn-Version": "",
"Accept-Encoding": "gzip, deflate, br",
- "Content-Type": "application/json"
+ "Content-Type": "application/json",
+ "elastic-api-version": "2",
+ "x-elastic-internal-origin": "kibana"
},
"statusCode": 200
}
diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts
index 6159b765f4a77..8a292d4d2dede 100644
--- a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts
+++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts
@@ -77,7 +77,8 @@ export default function navLinksTests({ getService }: FtrProviderContext) {
'enterpriseSearchVectorSearch',
'appSearch',
'workplaceSearch',
- 'guidedOnboardingFeature'
+ 'guidedOnboardingFeature',
+ 'securitySolutionAssistant'
)
);
break;