diff --git a/package.json b/package.json index dd72003d7bd6e..35e8a6451b1e5 100644 --- a/package.json +++ b/package.json @@ -226,7 +226,7 @@ "chokidar": "^3.4.3", "chroma-js": "^1.4.1", "classnames": "2.2.6", - "color": "1.0.3", + "color": "^4.2.3", "commander": "^4.1.1", "compare-versions": "3.5.1", "constate": "^1.3.2", @@ -539,7 +539,7 @@ "@types/chromedriver": "^81.0.1", "@types/classnames": "^2.2.9", "@types/cmd-shim": "^2.0.0", - "@types/color": "^3.0.0", + "@types/color": "^3.0.3", "@types/compression-webpack-plugin": "^2.0.2", "@types/cypress-cucumber-preprocessor": "^1.14.1", "@types/cytoscape": "^3.14.0", diff --git a/src/plugins/charts/public/services/active_cursor/use_active_cursor.test.ts b/src/plugins/charts/public/services/active_cursor/use_active_cursor.test.ts index b42e3f5d01a5d..4cc5e747dcef6 100644 --- a/src/plugins/charts/public/services/active_cursor/use_active_cursor.test.ts +++ b/src/plugins/charts/public/services/active_cursor/use_active_cursor.test.ts @@ -5,8 +5,6 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - -import { from, of, delay, concatMap } from 'rxjs'; import { renderHook } from '@testing-library/react-hooks'; import type { RefObject } from 'react'; @@ -17,59 +15,69 @@ import type { ActiveCursorSyncOption, ActiveCursorPayload } from './types'; import type { Chart, PointerEvent } from '@elastic/charts'; import type { Datatable } from '@kbn/expressions-plugin/public'; -/** @internal **/ -type DispatchExternalPointerEventFn = (pointerEvent: PointerEvent) => void; +// FLAKY: https://github.com/elastic/kibana/issues/130177 +describe.skip('useActiveCursor', () => { + let cursor: ActiveCursorPayload['cursor']; + let dispatchExternalPointerEvent: jest.Mock; -describe('useActiveCursor', () => { - const act = async ( + const act = ( syncOption: ActiveCursorSyncOption, events: Array>, - eventsTimeout = 5 - ): Promise<{ dispatchExternalPointerEvent: DispatchExternalPointerEventFn }> => { - const activeCursor = new ActiveCursor(); - const cursor = {} as ActiveCursorPayload['cursor']; - const dispatchExternalPointerEvent: DispatchExternalPointerEventFn = jest.fn(); - const debounce = syncOption.debounce ?? 5; - - activeCursor.setup(); - - renderHook(() => - useActiveCursor( - activeCursor, - { - current: { - dispatchExternalPointerEvent, - }, - } as RefObject, - { ...syncOption, debounce } - ) - ); - - return new Promise((res, rej) => - from(events) - .pipe(concatMap((x) => of(x).pipe(delay(eventsTimeout)))) - .subscribe({ - next: (item) => { - activeCursor.activeCursor$!.next({ - cursor, - ...item, - }); - }, - complete: () => { - /** We have to wait before resolving the promise to make sure the debouncedEvent gets fired. **/ - setTimeout(() => res({ dispatchExternalPointerEvent }), eventsTimeout + debounce + 30); - }, - error: (error) => { - rej(error); - }, - }) - ); - }; + eventsTimeout = 1 + ) => + new Promise(async (resolve, reject) => { + try { + const activeCursor = new ActiveCursor(); + let allEventsExecuted = false; + activeCursor.setup(); + dispatchExternalPointerEvent.mockImplementation((pointerEvent) => { + if (allEventsExecuted) { + resolve(pointerEvent); + } + }); + renderHook(() => + useActiveCursor( + activeCursor, + { + current: { + dispatchExternalPointerEvent: dispatchExternalPointerEvent as ( + pointerEvent: PointerEvent + ) => void, + }, + } as RefObject, + { ...syncOption, debounce: syncOption.debounce ?? 1 } + ) + ); + + for (const e of events) { + await new Promise((eventResolve) => + setTimeout(() => { + if (e === events[events.length - 1]) { + allEventsExecuted = true; + } + + activeCursor.activeCursor$!.next({ + cursor, + ...e, + }); + eventResolve(null); + }, eventsTimeout) + ); + } + } catch (error) { + reject(error); + } + }); + + beforeEach(() => { + cursor = {} as ActiveCursorPayload['cursor']; + dispatchExternalPointerEvent = jest.fn(); + }); test('should debounce events', async () => { - const { dispatchExternalPointerEvent } = await act( + await act( { - debounce: 10, + debounce: 50, datatables: [ { columns: [ @@ -95,15 +103,13 @@ describe('useActiveCursor', () => { }); test('should trigger cursor pointer update (chart type: time, event type: time)', async () => { - const { dispatchExternalPointerEvent } = await act({ isDateHistogram: true }, [ - { isDateHistogram: true }, - ]); + await act({ isDateHistogram: true }, [{ isDateHistogram: true }]); expect(dispatchExternalPointerEvent).toHaveBeenCalledTimes(1); }); test('should trigger cursor pointer update (chart type: datatable - time based, event type: time)', async () => { - const { dispatchExternalPointerEvent } = await act( + await act( { datatables: [ { @@ -128,7 +134,7 @@ describe('useActiveCursor', () => { }); test('should not trigger cursor pointer update (chart type: datatable, event type: time)', async () => { - const { dispatchExternalPointerEvent } = await act( + await act( { datatables: [ { @@ -150,7 +156,7 @@ describe('useActiveCursor', () => { }); test('should works with multi datatables (intersection)', async () => { - const { dispatchExternalPointerEvent } = await act( + await act( { datatables: [ { diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index 3b51a6d45c9bc..a45222ebbcec9 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -13,11 +13,15 @@ export const UPDATE_RULES_CONFIG_ROUTE_PATH = export const CSP_FINDINGS_INDEX_NAME = 'findings'; export const CIS_KUBERNETES_PACKAGE_NAME = 'cis_kubernetes_benchmark'; +export const FINDINGS_DATA_STREAM_NAME = + // Currently 'cis_kubernetes_benchmark.findings', To be refactored to 'cloud_security_posture.findings' + CIS_KUBERNETES_PACKAGE_NAME + '.' + CSP_FINDINGS_INDEX_NAME; export const LATEST_FINDINGS_INDEX_NAME = 'cloud_security_posture.findings_latest'; export const BENCHMARK_SCORE_INDEX_NAME = 'cloud_security_posture.scores'; export const AGENT_LOGS_INDEX_PATTERN = '.logs-cis_kubernetes_benchmark.metadata*'; -export const CSP_KUBEBEAT_INDEX_PATTERN = 'logs-cis_kubernetes_benchmark.findings*'; +export const CSP_KUBEBEAT_INDEX_PATTERN = 'logs-cis_kubernetes_benchmark.findings-*'; +export const FINDINGS_INDEX_PATTERN = 'logs-' + FINDINGS_DATA_STREAM_NAME + '-default'; export const LATEST_FINDINGS_INDEX_PATTERN = 'logs-' + LATEST_FINDINGS_INDEX_NAME + '-default'; export const BENCHMARK_SCORE_INDEX_PATTERN = 'logs-' + BENCHMARK_SCORE_INDEX_NAME + '-default'; diff --git a/x-pack/plugins/cloud_security_posture/server/create_indices/create_transforms_indices.ts b/x-pack/plugins/cloud_security_posture/server/create_indices/create_transforms_indices.ts index 048c122103a9e..c30bf09a60e0f 100644 --- a/x-pack/plugins/cloud_security_posture/server/create_indices/create_transforms_indices.ts +++ b/x-pack/plugins/cloud_security_posture/server/create_indices/create_transforms_indices.ts @@ -21,20 +21,22 @@ export const initializeCspTransformsIndices = async ( esClient: ElasticsearchClient, logger: Logger ) => { - createIndexIfNotExists( - esClient, - LATEST_FINDINGS_INDEX_NAME, - LATEST_FINDINGS_INDEX_PATTERN, - latestFindingsMapping, - logger - ); - createIndexIfNotExists( - esClient, - BENCHMARK_SCORE_INDEX_NAME, - BENCHMARK_SCORE_INDEX_PATTERN, - benchmarkScoreMapping, - logger - ); + return Promise.all([ + createIndexIfNotExists( + esClient, + LATEST_FINDINGS_INDEX_NAME, + LATEST_FINDINGS_INDEX_PATTERN, + latestFindingsMapping, + logger + ), + createIndexIfNotExists( + esClient, + BENCHMARK_SCORE_INDEX_NAME, + BENCHMARK_SCORE_INDEX_PATTERN, + benchmarkScoreMapping, + logger + ), + ]); }; export const createIndexIfNotExists = async ( diff --git a/x-pack/plugins/cloud_security_posture/server/create_transforms/benchmark_score_transform.ts b/x-pack/plugins/cloud_security_posture/server/create_transforms/benchmark_score_transform.ts new file mode 100644 index 0000000000000..8837fea0fa183 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/server/create_transforms/benchmark_score_transform.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; +import { + LATEST_FINDINGS_INDEX_PATTERN, + BENCHMARK_SCORE_INDEX_PATTERN, +} from '../../common/constants'; + +export const benchmarkScoreTransform: TransformPutTransformRequest = { + transform_id: 'cloud_security_posture.score-default-0.0.1', + description: 'Calculate latest findings score', + source: { + index: LATEST_FINDINGS_INDEX_PATTERN, + }, + dest: { + index: BENCHMARK_SCORE_INDEX_PATTERN, + }, + frequency: '30m', + sync: { + time: { + field: 'event.ingested', + delay: '60s', + }, + }, + retention_policy: { + time: { + field: '@timestamp', + max_age: '30d', + }, + }, + pivot: { + group_by: { + '@timestamp': { + date_histogram: { + field: '@timestamp', + calendar_interval: '1m', + }, + }, + }, + aggregations: { + total_findings: { + value_count: { + field: 'result.evaluation.keyword', + }, + }, + passed_findings: { + filter: { + term: { + 'result.evaluation.keyword': 'passed', + }, + }, + }, + failed_findings: { + filter: { + term: { + 'result.evaluation.keyword': 'failed', + }, + }, + }, + score_by_cluster_id: { + terms: { + field: 'cluster_id.keyword', + }, + aggregations: { + total_findings: { + value_count: { + field: 'result.evaluation.keyword', + }, + }, + passed_findings: { + filter: { + term: { + 'result.evaluation.keyword': 'passed', + }, + }, + }, + failed_findings: { + filter: { + term: { + 'result.evaluation.keyword': 'failed', + }, + }, + }, + }, + }, + }, + }, + _meta: { + managed: 'true', + }, +}; diff --git a/x-pack/plugins/cloud_security_posture/server/create_transforms/create_transforms.test.ts b/x-pack/plugins/cloud_security_posture/server/create_transforms/create_transforms.test.ts new file mode 100644 index 0000000000000..65a4507de2511 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/server/create_transforms/create_transforms.test.ts @@ -0,0 +1,129 @@ +/* + * 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. + */ + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from '@kbn/core/server/elasticsearch/client/mocks'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; +import { createTransformIfNotExists, startTransformIfNotStarted } from './create_transforms'; +import { latestFindingsTransform } from './latest_findings_transform'; + +const mockEsClient = elasticsearchClientMock.createClusterClient().asScoped().asInternalUser; + +describe('createTransformIfNotExist', () => { + let logger: ReturnType; + + beforeEach(() => { + logger = loggingSystemMock.createLogger(); + jest.resetAllMocks(); + }); + + it('expect not to create if already exists', async () => { + mockEsClient.transform.getTransform.mockResolvedValue({ transforms: [], count: 1 }); + await createTransformIfNotExists(mockEsClient, latestFindingsTransform, logger); + expect(mockEsClient.transform.getTransform).toHaveBeenCalledTimes(1); + expect(mockEsClient.transform.getTransform).toHaveBeenCalledWith({ + transform_id: latestFindingsTransform.transform_id, + }); + expect(mockEsClient.transform.putTransform).toHaveBeenCalledTimes(0); + }); + + it('expect to create if does not already exist', async () => { + mockEsClient.transform.getTransform.mockRejectedValue({ statusCode: 404 }); + await createTransformIfNotExists(mockEsClient, latestFindingsTransform, logger); + expect(mockEsClient.transform.getTransform).toHaveBeenCalledTimes(1); + expect(mockEsClient.transform.getTransform).toHaveBeenCalledWith({ + transform_id: latestFindingsTransform.transform_id, + }); + expect(mockEsClient.transform.putTransform).toHaveBeenCalledTimes(1); + expect(mockEsClient.transform.putTransform).toHaveBeenCalledWith(latestFindingsTransform); + }); + + it('expect not to create if get error is not 404', async () => { + mockEsClient.transform.getTransform.mockRejectedValue({ statusCode: 400 }); + await createTransformIfNotExists(mockEsClient, latestFindingsTransform, logger); + expect(mockEsClient.transform.getTransform).toHaveBeenCalledTimes(1); + expect(mockEsClient.transform.putTransform).toHaveBeenCalledTimes(0); + }); +}); + +function getTransformWithState(state: string) { + return { + state, + checkpointing: { last: { checkpoint: 1 } }, + id: '', + stats: { + documents_indexed: 0, + documents_processed: 0, + exponential_avg_checkpoint_duration_ms: 0, + exponential_avg_documents_indexed: 0, + exponential_avg_documents_processed: 0, + index_failures: 0, + index_time_in_ms: 0, + index_total: 0, + pages_processed: 0, + processing_time_in_ms: 0, + processing_total: 0, + search_failures: 0, + search_time_in_ms: 0, + search_total: 0, + trigger_count: 0, + }, + }; +} + +describe('startTransformIfNotStarted', () => { + let logger: ReturnType; + + beforeEach(() => { + logger = loggingSystemMock.createLogger(); + jest.resetAllMocks(); + }); + + ['failed', 'stopping', 'started', 'aborting', 'indexing'].forEach((state) => + it(`expect not to start if state is ${state}`, async () => { + mockEsClient.transform.getTransformStats.mockResolvedValue({ + transforms: [getTransformWithState(state)], + count: 1, + }); + await startTransformIfNotStarted(mockEsClient, latestFindingsTransform.transform_id, logger); + expect(mockEsClient.transform.getTransformStats).toHaveBeenCalledTimes(1); + expect(mockEsClient.transform.getTransformStats).toHaveBeenCalledWith({ + transform_id: latestFindingsTransform.transform_id, + }); + expect(mockEsClient.transform.startTransform).toHaveBeenCalledTimes(0); + }) + ); + + it('expect not to start if transform not found', async () => { + mockEsClient.transform.getTransformStats.mockResolvedValue({ + transforms: [], + count: 0, + }); + await startTransformIfNotStarted(mockEsClient, latestFindingsTransform.transform_id, logger); + expect(mockEsClient.transform.getTransformStats).toHaveBeenCalledTimes(1); + expect(mockEsClient.transform.getTransformStats).toHaveBeenCalledWith({ + transform_id: latestFindingsTransform.transform_id, + }); + expect(mockEsClient.transform.startTransform).toHaveBeenCalledTimes(0); + }); + + it('expect to start if state is stopped', async () => { + mockEsClient.transform.getTransformStats.mockResolvedValue({ + transforms: [getTransformWithState('stopped')], + count: 1, + }); + await startTransformIfNotStarted(mockEsClient, latestFindingsTransform.transform_id, logger); + expect(mockEsClient.transform.getTransformStats).toHaveBeenCalledTimes(1); + expect(mockEsClient.transform.getTransformStats).toHaveBeenCalledWith({ + transform_id: latestFindingsTransform.transform_id, + }); + expect(mockEsClient.transform.startTransform).toHaveBeenCalledTimes(1); + expect(mockEsClient.transform.startTransform).toHaveBeenCalledWith({ + transform_id: latestFindingsTransform.transform_id, + }); + }); +}); diff --git a/x-pack/plugins/cloud_security_posture/server/create_transforms/create_transforms.ts b/x-pack/plugins/cloud_security_posture/server/create_transforms/create_transforms.ts new file mode 100644 index 0000000000000..3347d5f36b5d8 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/server/create_transforms/create_transforms.ts @@ -0,0 +1,105 @@ +/* + * 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 { transformError } from '@kbn/securitysolution-es-utils'; +import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; +import type { ElasticsearchClient, Logger } from '@kbn/core/server'; +import { latestFindingsTransform } from './latest_findings_transform'; +import { benchmarkScoreTransform } from './benchmark_score_transform'; + +// TODO: Move transforms to integration package +export const initializeCspTransforms = async (esClient: ElasticsearchClient, logger: Logger) => { + return Promise.all([ + initializeTransform(esClient, latestFindingsTransform, logger), + initializeTransform(esClient, benchmarkScoreTransform, logger), + ]); +}; + +export const initializeTransform = async ( + esClient: ElasticsearchClient, + transform: TransformPutTransformRequest, + logger: Logger +) => { + return createTransformIfNotExists(esClient, transform, logger).then((succeeded) => { + if (succeeded) { + startTransformIfNotStarted(esClient, transform.transform_id, logger); + } + }); +}; + +/** + * Checks if a transform exists, And if not creates it + * + * @param transform - the transform to create. If a transform with the same transform_id already exists, nothing is created or updated. + * + * @return true if the transform exits or created, false otherwise. + */ +export const createTransformIfNotExists = async ( + esClient: ElasticsearchClient, + transform: TransformPutTransformRequest, + logger: Logger +) => { + try { + await esClient.transform.getTransform({ + transform_id: transform.transform_id, + }); + return true; + } catch (existErr) { + const existError = transformError(existErr); + if (existError.statusCode === 404) { + try { + await esClient.transform.putTransform(transform); + return true; + } catch (createErr) { + const createError = transformError(createErr); + logger.error( + `Failed to create transform ${transform.transform_id}: ${createError.message}` + ); + } + } else { + logger.error( + `Failed to check if transform ${transform.transform_id} exists: ${existError.message}` + ); + } + } + return false; +}; + +export const startTransformIfNotStarted = async ( + esClient: ElasticsearchClient, + transformId: string, + logger: Logger +) => { + try { + const transformStats = await esClient.transform.getTransformStats({ + transform_id: transformId, + }); + if (transformStats.count <= 0) { + logger.error(`Failed starting transform ${transformId}: couldn't find transform`); + return; + } + const fetchedTransformStats = transformStats.transforms[0]; + if (fetchedTransformStats.state === 'stopped') { + try { + return await esClient.transform.startTransform({ transform_id: transformId }); + } catch (startErr) { + const startError = transformError(startErr); + logger.error(`Failed starting transform ${transformId}: ${startError.message}`); + } + } else if ( + fetchedTransformStats.state === 'stopping' || + fetchedTransformStats.state === 'aborting' || + fetchedTransformStats.state === 'failed' + ) { + logger.error( + `Not starting transform ${transformId} since it's state is: ${fetchedTransformStats.state}` + ); + } + } catch (statsErr) { + const statsError = transformError(statsErr); + logger.error(`Failed to check if transform ${transformId} is started: ${statsError.message}`); + } +}; diff --git a/x-pack/plugins/cloud_security_posture/server/create_transforms/latest_findings_transform.ts b/x-pack/plugins/cloud_security_posture/server/create_transforms/latest_findings_transform.ts new file mode 100644 index 0000000000000..fc042149f3193 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/server/create_transforms/latest_findings_transform.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; +import { FINDINGS_INDEX_PATTERN, LATEST_FINDINGS_INDEX_PATTERN } from '../../common/constants'; + +export const latestFindingsTransform: TransformPutTransformRequest = { + transform_id: 'cloud_security_posture.findings_latest-default-0.0.1', + description: 'Defines findings transformation to view only the latest finding per resource', + source: { + index: FINDINGS_INDEX_PATTERN, + }, + dest: { + index: LATEST_FINDINGS_INDEX_PATTERN, + }, + frequency: '5m', + sync: { + time: { + field: 'event.ingested', + delay: '60s', + }, + }, + retention_policy: { + time: { + field: '@timestamp', + max_age: '3d', + }, + }, + latest: { + sort: '@timestamp', + unique_key: ['resource_id.keyword', 'rule.name.keyword', 'agent.id.keyword'], + }, + _meta: { + managed: 'true', + }, +}; diff --git a/x-pack/plugins/cloud_security_posture/server/plugin.ts b/x-pack/plugins/cloud_security_posture/server/plugin.ts index a76aefb5b2070..b2108dc24a926 100755 --- a/x-pack/plugins/cloud_security_posture/server/plugin.ts +++ b/x-pack/plugins/cloud_security_posture/server/plugin.ts @@ -25,6 +25,7 @@ import { cspRuleTemplateAssetType } from './saved_objects/csp_rule_template'; import { cspRuleAssetType } from './saved_objects/csp_rule_type'; import { initializeCspRules } from './saved_objects/initialize_rules'; import { initializeCspTransformsIndices } from './create_indices/create_transforms_indices'; +import { initializeCspTransforms } from './create_transforms/create_transforms'; export interface CspAppContext { logger: Logger; @@ -72,7 +73,10 @@ export class CspPlugin }); initializeCspRules(core.savedObjects.createInternalRepository()); - initializeCspTransformsIndices(core.elasticsearch.client.asInternalUser, this.logger); + initializeCspTransformsIndices(core.elasticsearch.client.asInternalUser, this.logger).then( + (_) => initializeCspTransforms(core.elasticsearch.client.asInternalUser, this.logger) + ); + return {}; } public stop() {} diff --git a/x-pack/plugins/synthetics/server/lib/saved_objects/service_api_key.ts b/x-pack/plugins/synthetics/server/lib/saved_objects/service_api_key.ts index b4ec1d56f97f0..1486c65f07b1a 100644 --- a/x-pack/plugins/synthetics/server/lib/saved_objects/service_api_key.ts +++ b/x-pack/plugins/synthetics/server/lib/saved_objects/service_api_key.ts @@ -20,7 +20,7 @@ export const syntheticsApiKeyObjectType = 'uptime-synthetics-api-key'; export const syntheticsServiceApiKey: SavedObjectsType = { name: syntheticsApiKeyObjectType, hidden: true, - namespaceType: 'single', + namespaceType: 'agnostic', mappings: { dynamic: false, properties: { diff --git a/x-pack/plugins/synthetics/server/lib/synthetics_service/synthetics_service.ts b/x-pack/plugins/synthetics/server/lib/synthetics_service/synthetics_service.ts index 05aa49de663f4..083f0d9f6126c 100644 --- a/x-pack/plugins/synthetics/server/lib/synthetics_service/synthetics_service.ts +++ b/x-pack/plugins/synthetics/server/lib/synthetics_service/synthetics_service.ts @@ -369,7 +369,10 @@ export class SyntheticsService { encryptedMonitors.map((monitor) => encryptedClient.getDecryptedAsInternalUser( syntheticsMonitor.name, - monitor.id + monitor.id, + { + namespace: monitor.namespaces?.[0], + } ) ) ); diff --git a/x-pack/test/api_integration/apis/uptime/rest/synthetics_enablement.ts b/x-pack/test/api_integration/apis/uptime/rest/synthetics_enablement.ts index 44574b5328f8f..04a718e5c7c8f 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/synthetics_enablement.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/synthetics_enablement.ts @@ -15,6 +15,7 @@ export default function ({ getService }: FtrProviderContext) { const supertestWithAuth = getService('supertest'); const supertest = getService('supertestWithoutAuth'); const security = getService('security'); + const kibanaServer = getService('kibanaServer'); before(async () => { await supertestWithAuth.delete(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true'); @@ -311,6 +312,86 @@ export default function ({ getService }: FtrProviderContext) { await security.role.delete(roleName); } }); + + it('is space agnostic', async () => { + const username = 'admin'; + const roleName = `synthetics_admin`; + const password = `${username}-password`; + const SPACE_ID = 'test-space'; + const SPACE_NAME = 'test-space-name'; + await kibanaServer.spaces.create({ id: SPACE_ID, name: SPACE_NAME }); + try { + await security.role.create(roleName, { + kibana: [ + { + feature: { + uptime: ['all'], + }, + spaces: ['*'], + }, + ], + elasticsearch: { + cluster: ['manage_security', ...serviceApiKeyPrivileges.cluster], + indices: serviceApiKeyPrivileges.index, + }, + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + // can disable synthetics in default space when enabled in a non default space + await supertest + .post(`/s/${SPACE_ID}${API_URLS.SYNTHETICS_ENABLEMENT}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(200); + await supertest + .delete(API_URLS.SYNTHETICS_ENABLEMENT) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(200); + const apiResponse = await supertest + .get(API_URLS.SYNTHETICS_ENABLEMENT) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(apiResponse.body).eql({ + areApiKeysEnabled: true, + canEnable: true, + isEnabled: false, + }); + + // can disable synthetics in non default space when enabled in default space + await supertest + .post(API_URLS.SYNTHETICS_ENABLEMENT) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(200); + await supertest + .delete(`/s/${SPACE_ID}${API_URLS.SYNTHETICS_ENABLEMENT}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(200); + const apiResponse2 = await supertest + .get(API_URLS.SYNTHETICS_ENABLEMENT) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(apiResponse2.body).eql({ + areApiKeysEnabled: true, + canEnable: true, + isEnabled: false, + }); + } finally { + await security.user.delete(username); + await security.role.delete(roleName); + } + }); }); }); } diff --git a/yarn.lock b/yarn.lock index 095bde19073aa..70460f4c8d036 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5455,14 +5455,7 @@ resolved "https://registry.yarnpkg.com/@types/cmd-shim/-/cmd-shim-2.0.0.tgz#c3d81d3c2a51af3c65c19b4f1d493a75abf07a5c" integrity sha512-WuV5bOQTGxqzewPnJbrDe51I5mnX2wTRYy+PduRuIvSuBWZlxDYD6Qt/f1m5sezxx1yyE9+1wHJ/0sRuNIoFaQ== -"@types/color-convert@*": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@types/color-convert/-/color-convert-1.9.0.tgz#bfa8203e41e7c65471e9841d7e306a7cd8b5172d" - integrity sha512-OKGEfULrvSL2VRbkl/gnjjgbbF7ycIlpSsX7Nkab4MOWi5XxmgBYvuiQ7lcCFY5cPDz7MUNaKgxte2VRmtr4Fg== - dependencies: - "@types/color-name" "*" - -"@types/color-convert@^2.0.0": +"@types/color-convert@*", "@types/color-convert@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/color-convert/-/color-convert-2.0.0.tgz#8f5ee6b9e863dcbee5703f5a517ffb13d3ea4e22" integrity sha512-m7GG7IKKGuJUXvkZ1qqG3ChccdIM/qBBo913z+Xft0nKCX4hAU/IxKwZBU4cpRZ7GS5kV4vOblUkILtSShCPXQ== @@ -5474,10 +5467,10 @@ resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== -"@types/color@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/color/-/color-3.0.0.tgz#40f8a6bf2fd86e969876b339a837d8ff1b0a6e30" - integrity sha512-5qqtNia+m2I0/85+pd2YzAXaTyKO8j+svirO5aN+XaQJ5+eZ8nx0jPtEWZLxCi50xwYsX10xUHetFzfb1WEs4Q== +"@types/color@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/color/-/color-3.0.3.tgz#e6d8d72b7aaef4bb9fe80847c26c7c786191016d" + integrity sha512-X//qzJ3d3Zj82J9sC/C18ZY5f43utPbAJ6PhYt/M7uG6etcF6MRpKdN880KBy43B0BMzSfeT96MzrsNjFI3GbA== dependencies: "@types/color-convert" "*" @@ -10420,7 +10413,7 @@ collection-visit@^1.0.0: map-visit "^1.0.0" object-visit "^1.0.0" -color-convert@^1.8.2, color-convert@^1.9.0, color-convert@^1.9.1: +color-convert@^1.9.0, color-convert@^1.9.1, color-convert@^1.9.3: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== @@ -10444,15 +10437,7 @@ color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.4.0, color-string@^1.5.2, color-string@^1.5.4: - version "1.5.5" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.5.tgz#65474a8f0e7439625f3d27a6a19d89fc45223014" - integrity sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg== - dependencies: - color-name "^1.0.0" - simple-swizzle "^0.2.2" - -color-string@^1.9.0: +color-string@^1.5.2, color-string@^1.6.0, color-string@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.0.tgz#63b6ebd1bec11999d1df3a79a7569451ac2be8aa" integrity sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ== @@ -10465,14 +10450,6 @@ color-support@^1.1.3: resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== -color@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/color/-/color-1.0.3.tgz#e48e832d85f14ef694fb468811c2d5cfe729b55d" - integrity sha1-5I6DLYXxTvaU+0aIEcLVz+cptV0= - dependencies: - color-convert "^1.8.2" - color-string "^1.4.0" - color@3.0.x: version "3.0.0" resolved "https://registry.yarnpkg.com/color/-/color-3.0.0.tgz#d920b4328d534a3ac8295d68f7bd4ba6c427be9a" @@ -10482,17 +10459,17 @@ color@3.0.x: color-string "^1.5.2" color@^3.0.0: - version "3.1.3" - resolved "https://registry.yarnpkg.com/color/-/color-3.1.3.tgz#ca67fb4e7b97d611dcde39eceed422067d91596e" - integrity sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ== + version "3.2.1" + resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164" + integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== dependencies: - color-convert "^1.9.1" - color-string "^1.5.4" + color-convert "^1.9.3" + color-string "^1.6.0" -color@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/color/-/color-4.2.1.tgz#498aee5fce7fc982606c8875cab080ac0547c884" - integrity sha512-MFJr0uY4RvTQUKvPq7dh9grVOTYSFeXja2mBXioCGjnjJoXrAp9jJ1NQTDR73c9nwBSAQiNKloKl5zq9WB9UPw== +color@^4.2.1, color@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" + integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== dependencies: color-convert "^2.0.1" color-string "^1.9.0"