From 7fc30d548474ba08222bcafedf99cba044c52603 Mon Sep 17 00:00:00 2001 From: "Joey F. Poon" Date: Mon, 6 Dec 2021 12:11:58 -0600 Subject: [PATCH] [Security Solution] endpoint metadata transform stats API (#120429) --- .../security_solution/common/constants.ts | 3 - .../common/endpoint/constants.ts | 4 + .../management/pages/endpoint_hosts/mocks.ts | 5 +- .../pages/endpoint_hosts/store/middleware.ts | 4 +- .../store/mock_endpoint_result_list.ts | 4 +- .../server/endpoint/mocks.ts | 10 +- .../endpoint/routes/actions/isolation.ts | 4 +- .../endpoint/routes/metadata/handlers.ts | 30 +++ .../server/endpoint/routes/metadata/index.ts | 17 +- .../endpoint/routes/metadata/metadata.test.ts | 232 ++++++++---------- .../routes/__mocks__/request_context.ts | 13 +- .../apis/data_stream_helper.ts | 10 +- .../apis/metadata.ts | 62 ++++- 13 files changed, 244 insertions(+), 154 deletions(-) diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 25c6b07fd03af..ea2e7b058ed25 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -7,7 +7,6 @@ import type { TransformConfigSchema } from './transforms/types'; import { ENABLE_CASE_CONNECTOR } from '../../cases/common'; -import { METADATA_TRANSFORMS_PATTERN } from './endpoint/constants'; /** * as const @@ -362,8 +361,6 @@ export const showAllOthersBucket: string[] = [ */ export const ELASTIC_NAME = 'estc' as const; -export const METADATA_TRANSFORM_STATS_URL = `/api/transform/transforms/${METADATA_TRANSFORMS_PATTERN}/_stats`; - export const RISKY_HOSTS_INDEX_PREFIX = 'ml_host_risk_score_latest_' as const; export const TRANSFORM_STATES = { diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index a7fe91345dd14..bcd3b9524bf60 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -44,6 +44,7 @@ export const LIMITED_CONCURRENCY_ENDPOINT_COUNT = 100; export const BASE_ENDPOINT_ROUTE = '/api/endpoint'; export const HOST_METADATA_LIST_ROUTE = `${BASE_ENDPOINT_ROUTE}/metadata`; export const HOST_METADATA_GET_ROUTE = `${BASE_ENDPOINT_ROUTE}/metadata/{id}`; +export const METADATA_TRANSFORMS_STATUS_ROUTE = `${BASE_ENDPOINT_ROUTE}/metadata/transforms`; export const TRUSTED_APPS_GET_API = `${BASE_ENDPOINT_ROUTE}/trusted_apps/{id}`; export const TRUSTED_APPS_LIST_API = `${BASE_ENDPOINT_ROUTE}/trusted_apps`; @@ -68,3 +69,6 @@ export const failedFleetActionErrorCode = '424'; export const ENDPOINT_DEFAULT_PAGE = 0; export const ENDPOINT_DEFAULT_PAGE_SIZE = 10; + +export const FORBIDDEN_MESSAGE = + 'You do not have permission to perform this action or license level does not allow for this action'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts index 3aacd1db2f3dd..12685896d9535 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts @@ -24,12 +24,13 @@ import { ENDPOINT_ACTION_LOG_ROUTE, HOST_METADATA_GET_ROUTE, HOST_METADATA_LIST_ROUTE, + METADATA_TRANSFORMS_STATUS_ROUTE, } from '../../../../common/endpoint/constants'; import { pendingActionsHttpMock, PendingActionsHttpMockInterface, } from '../../../common/lib/endpoint_pending_actions/mocks'; -import { METADATA_TRANSFORM_STATS_URL, TRANSFORM_STATES } from '../../../../common/constants'; +import { TRANSFORM_STATES } from '../../../../common/constants'; import { TransformStatsResponse } from './types'; import { fleetGetAgentPolicyListHttpMock, @@ -162,7 +163,7 @@ export const failedTransformStateMock = { export const transformsHttpMocks = httpHandlerMockFactory([ { id: 'metadataTransformStats', - path: METADATA_TRANSFORM_STATS_URL, + path: METADATA_TRANSFORMS_STATUS_ROUTE, method: 'get', handler: () => failedTransformStateMock, }, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index 2759a35841524..9d839be623920 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -10,7 +10,6 @@ import { CoreStart, HttpStart } from 'kibana/public'; import { Dispatch } from 'redux'; import semverGte from 'semver/functions/gte'; import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../../../../../fleet/common'; -import { METADATA_TRANSFORM_STATS_URL } from '../../../../../common/constants'; import { BASE_POLICY_RESPONSE_ROUTE, ENDPOINT_ACTION_LOG_ROUTE, @@ -18,6 +17,7 @@ import { HOST_METADATA_LIST_ROUTE, metadataCurrentIndexPattern, METADATA_UNITED_INDEX, + METADATA_TRANSFORMS_STATUS_ROUTE, } from '../../../../../common/endpoint/constants'; import { ActivityLog, @@ -783,7 +783,7 @@ export async function handleLoadMetadataTransformStats(http: HttpStart, store: E try { const transformStatsResponse: TransformStatsResponse = await http.get( - METADATA_TRANSFORM_STATS_URL + METADATA_TRANSFORMS_STATUS_ROUTE ); dispatch({ diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts index 61eb5ad3c541d..1e8b55ef977e8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts @@ -32,8 +32,8 @@ import { pendingActionsResponseMock } from '../../../../common/lib/endpoint_pend import { ACTION_STATUS_ROUTE, HOST_METADATA_LIST_ROUTE, + METADATA_TRANSFORMS_STATUS_ROUTE, } from '../../../../../common/endpoint/constants'; -import { METADATA_TRANSFORM_STATS_URL } from '../../../../../common/constants'; import { TransformStats, TransformStatsResponse } from '../types'; const generator = new EndpointDocGenerator('seed'); @@ -162,7 +162,7 @@ const endpointListApiPathHandlerMocks = ({ return pendingActionsResponseMock(); }, - [METADATA_TRANSFORM_STATS_URL]: (): TransformStatsResponse => ({ + [METADATA_TRANSFORMS_STATUS_ROUTE]: (): TransformStatsResponse => ({ count: transforms.length, transforms, }), diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index dce08e2522beb..95a1f92ea94cd 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -40,6 +40,8 @@ import { createCasesClientMock } from '../../../cases/server/client/mocks'; import { requestContextFactoryMock } from '../request_context_factory.mock'; import { EndpointMetadataService } from './services/metadata'; import { createFleetAuthzMock } from '../../../fleet/common'; +import { createMockClients } from '../lib/detection_engine/routes/__mocks__/request_context'; +import type { EndpointAuthz } from '../../common/endpoint/types/authz'; /** * Creates a mocked EndpointAppContext. @@ -181,9 +183,13 @@ export const createMockMetadataRequestContext = (): jest.Mocked, - savedObjectsClient: jest.Mocked + savedObjectsClient: jest.Mocked, + overrides: { endpointAuthz?: Partial } = {} ) { - const context = requestContextMock.create() as jest.Mocked; + const context = requestContextMock.create( + createMockClients(), + overrides + ) as jest.Mocked; context.core.elasticsearch.client = dataClient; context.core.savedObjects.client = savedObjectsClient; return context; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts index 51f88730eb6fd..9c8decf6b90c2 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts @@ -18,6 +18,7 @@ import { ISOLATE_HOST_ROUTE, UNISOLATE_HOST_ROUTE, failedFleetActionErrorCode, + FORBIDDEN_MESSAGE, } from '../../../../common/endpoint/constants'; import { AGENT_ACTIONS_INDEX } from '../../../../../fleet/common'; import { @@ -105,8 +106,7 @@ export const isolationRequestHandler = function ( if ((!canIsolateHost && isolate) || (!canUnIsolateHost && !isolate)) { return res.forbidden({ body: { - message: - 'You do not have permission to perform this action or license level does not allow for this action', + message: FORBIDDEN_MESSAGE, }, }); } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts index 06e63c6b7ec59..b79b6985f68ea 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts @@ -41,6 +41,8 @@ import { GetMetadataListRequestQuery } from '../../../../common/endpoint/schema/ import { ENDPOINT_DEFAULT_PAGE, ENDPOINT_DEFAULT_PAGE_SIZE, + FORBIDDEN_MESSAGE, + METADATA_TRANSFORMS_PATTERN, } from '../../../../common/endpoint/constants'; import { EndpointFleetServicesInterface } from '../../services/endpoint_fleet_services'; @@ -185,6 +187,34 @@ export const getMetadataRequestHandler = function ( }; }; +export function getMetadataTransformStatsHandler( + logger: Logger +): RequestHandler { + return async (context, _, response) => { + const { canAccessEndpointManagement } = context.securitySolution.endpointAuthz; + if (!canAccessEndpointManagement) { + return response.forbidden({ + body: { + message: FORBIDDEN_MESSAGE, + }, + }); + } + + const esClient = context.core.elasticsearch.client.asInternalUser; + try { + const transformStats = await esClient.transform.getTransformStats({ + transform_id: METADATA_TRANSFORMS_PATTERN, + allow_no_match: true, + }); + return response.ok({ + body: transformStats.body, + }); + } catch (error) { + return errorHandler(logger, response, error); + } + }; +} + export async function mapToHostResultList( // eslint-disable-next-line @typescript-eslint/no-explicit-any queryParams: Record, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts index 6cd1ae275d592..d5a428638702e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts @@ -9,11 +9,17 @@ import { schema } from '@kbn/config-schema'; import { HostStatus } from '../../../../common/endpoint/types'; import { EndpointAppContext } from '../../types'; -import { getLogger, getMetadataRequestHandler, getMetadataListRequestHandler } from './handlers'; +import { + getLogger, + getMetadataRequestHandler, + getMetadataListRequestHandler, + getMetadataTransformStatsHandler, +} from './handlers'; import type { SecuritySolutionPluginRouter } from '../../../types'; import { HOST_METADATA_GET_ROUTE, HOST_METADATA_LIST_ROUTE, + METADATA_TRANSFORMS_STATUS_ROUTE, } from '../../../../common/endpoint/constants'; import { GetMetadataListRequestSchema } from '../../../../common/endpoint/schema/metadata'; @@ -60,4 +66,13 @@ export function registerEndpointRoutes( }, getMetadataRequestHandler(endpointAppContext, logger) ); + + router.get( + { + path: METADATA_TRANSFORMS_STATUS_ROUTE, + validate: false, + options: { authRequired: true, tags: ['access:securitySolution'] }, + }, + getMetadataTransformStatsHandler(logger) + ); } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index e324f66ad38f6..1050273a5ff75 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -44,8 +44,10 @@ import { HOST_METADATA_LIST_ROUTE, metadataCurrentIndexPattern, metadataTransformPrefix, + METADATA_TRANSFORMS_STATUS_ROUTE, METADATA_UNITED_INDEX, } from '../../../../common/endpoint/constants'; +import { TRANSFORM_STATES } from '../../../../common/constants'; import type { SecuritySolutionPluginRouter } from '../../../types'; import { AgentNotFoundError, PackagePolicyServiceInterface } from '../../../../../fleet/server'; import { @@ -56,6 +58,8 @@ import { import { EndpointHostNotFoundError } from '../../services/metadata'; import { FleetAgentGenerator } from '../../../../common/endpoint/data_generators/fleet_agent_generator'; import { createMockAgentClient } from '../../../../../fleet/server/mocks'; +import { TransformGetTransformStatsResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { getEndpointAuthzInitialStateMock } from '../../../../common/endpoint/service/authz'; class IndexNotFoundException extends Error { meta: { body: { error: { type: string } } }; @@ -114,54 +118,52 @@ describe('test endpoint routes', () => { perPage: 1000, }); }); + + endpointAppContextService = new EndpointAppContextService(); + mockPackageService = createMockPackageService(); + mockPackageService.getInstallation.mockReturnValue( + Promise.resolve({ + installed_kibana: [], + package_assets: [], + es_index_patterns: {}, + name: '', + version: '', + install_status: 'installed', + install_version: '', + install_started_at: '', + install_source: 'registry', + installed_es: [ + { + id: 'logs-endpoint.events.security', + type: ElasticsearchAssetType.indexTemplate, + }, + { + id: `${metadataTransformPrefix}-0.16.0-dev.0`, + type: ElasticsearchAssetType.transform, + }, + ], + keep_policies_up_to_date: false, + }) + ); + endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract()); + endpointAppContextService.start({ ...startContract, packageService: mockPackageService }); + mockAgentService = startContract.agentService!; + mockAgentClient = createMockAgentClient(); + mockAgentService.asScoped = () => mockAgentClient; + mockAgentPolicyService = startContract.agentPolicyService!; + + registerEndpointRoutes(routerMock, { + logFactory: loggingSystemMock.create(), + service: endpointAppContextService, + config: () => Promise.resolve(createMockConfig()), + experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), + }); }); + afterEach(() => endpointAppContextService.stop()); + describe('GET list endpoints route', () => { describe('with .metrics-endpoint.metadata_united_default index', () => { - beforeEach(() => { - endpointAppContextService = new EndpointAppContextService(); - mockPackageService = createMockPackageService(); - mockPackageService.getInstallation.mockReturnValue( - Promise.resolve({ - installed_kibana: [], - package_assets: [], - es_index_patterns: {}, - name: '', - version: '', - install_status: 'installed', - install_version: '', - install_started_at: '', - install_source: 'registry', - installed_es: [ - { - id: 'logs-endpoint.events.security', - type: ElasticsearchAssetType.indexTemplate, - }, - { - id: `${metadataTransformPrefix}-0.16.0-dev.0`, - type: ElasticsearchAssetType.transform, - }, - ], - keep_policies_up_to_date: false, - }) - ); - endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract()); - endpointAppContextService.start({ ...startContract, packageService: mockPackageService }); - mockAgentService = startContract.agentService!; - mockAgentClient = createMockAgentClient(); - mockAgentService.asScoped = () => mockAgentClient; - mockAgentPolicyService = startContract.agentPolicyService!; - - registerEndpointRoutes(routerMock, { - logFactory: loggingSystemMock.create(), - service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), - }); - }); - - afterEach(() => endpointAppContextService.stop()); - it('should fallback to legacy index if index not found', async () => { const mockRequest = httpServerMock.createKibanaRequest({ query: { @@ -380,49 +382,6 @@ describe('test endpoint routes', () => { }); describe('with metrics-endpoint.metadata_current_default index', () => { - beforeEach(() => { - endpointAppContextService = new EndpointAppContextService(); - mockPackageService = createMockPackageService(); - mockPackageService.getInstallation.mockReturnValue( - Promise.resolve({ - installed_kibana: [], - package_assets: [], - es_index_patterns: {}, - name: '', - version: '', - install_status: 'installed', - install_version: '', - install_started_at: '', - install_source: 'registry', - installed_es: [ - { - id: 'logs-endpoint.events.security', - type: ElasticsearchAssetType.indexTemplate, - }, - { - id: `${metadataTransformPrefix}-0.16.0-dev.0`, - type: ElasticsearchAssetType.transform, - }, - ], - keep_policies_up_to_date: false, - }) - ); - endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract()); - endpointAppContextService.start({ ...startContract, packageService: mockPackageService }); - mockAgentService = startContract.agentService!; - mockAgentClient = createMockAgentClient(); - mockAgentService.asScoped = () => mockAgentClient; - - registerEndpointRoutes(routerMock, { - logFactory: loggingSystemMock.create(), - service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), - }); - }); - - afterEach(() => endpointAppContextService.stop()); - it('test find the latest of all endpoints', async () => { const mockRequest = httpServerMock.createKibanaRequest({ query: { @@ -611,49 +570,6 @@ describe('test endpoint routes', () => { }); describe('GET endpoint details route', () => { - beforeEach(() => { - endpointAppContextService = new EndpointAppContextService(); - mockPackageService = createMockPackageService(); - mockPackageService.getInstallation.mockReturnValue( - Promise.resolve({ - installed_kibana: [], - package_assets: [], - es_index_patterns: {}, - name: '', - version: '', - install_status: 'installed', - install_version: '', - install_started_at: '', - install_source: 'registry', - installed_es: [ - { - id: 'logs-endpoint.events.security', - type: ElasticsearchAssetType.indexTemplate, - }, - { - id: `${metadataTransformPrefix}-0.16.0-dev.0`, - type: ElasticsearchAssetType.transform, - }, - ], - keep_policies_up_to_date: false, - }) - ); - endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract()); - endpointAppContextService.start({ ...startContract, packageService: mockPackageService }); - mockAgentService = startContract.agentService!; - mockAgentClient = createMockAgentClient(); - mockAgentService.asScoped = () => mockAgentClient; - - registerEndpointRoutes(routerMock, { - logFactory: loggingSystemMock.create(), - service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), - }); - }); - - afterEach(() => endpointAppContextService.stop()); - it('should return 404 on no results', async () => { const mockRequest = httpServerMock.createKibanaRequest({ params: { id: 'BADID' } }); @@ -821,4 +737,60 @@ describe('test endpoint routes', () => { expect(mockResponse.badRequest).toBeCalled(); }); }); + + describe('GET metadata transform stats route', () => { + it('should get forbidden if no fleet access', async () => { + const mockRequest = httpServerMock.createKibanaRequest(); + + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => + path.startsWith(METADATA_TRANSFORMS_STATUS_ROUTE) + )!; + + const contextOverrides = { + endpointAuthz: getEndpointAuthzInitialStateMock({ canAccessEndpointManagement: false }), + }; + await routeHandler( + createRouteHandlerContext(mockScopedClient, mockSavedObjectClient, contextOverrides), + mockRequest, + mockResponse + ); + + expect(mockResponse.forbidden).toBeCalled(); + }); + + it('should correctly return metadata transform stats', async () => { + const mockRequest = httpServerMock.createKibanaRequest(); + const expectedResponse = { + count: 1, + transforms: [ + { + id: 'someid', + state: TRANSFORM_STATES.STARTED, + }, + ], + }; + const esClientMock = mockScopedClient.asInternalUser; + (esClientMock.transform.getTransformStats as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ body: expectedResponse }) + ); + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => + path.startsWith(METADATA_TRANSFORMS_STATUS_ROUTE) + )!; + await routeHandler( + createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), + mockRequest, + mockResponse + ); + + expect(esClientMock.transform.getTransformStats).toHaveBeenCalledTimes(1); + expect(routeConfig.options).toEqual({ + authRequired: true, + tags: ['access:securitySolution'], + }); + expect(mockResponse.ok).toBeCalled(); + const response = mockResponse.ok.mock.calls[0][0]?.body as TransformGetTransformStatsResponse; + expect(response.count).toEqual(expectedResponse.count); + expect(response.transforms).toEqual(expectedResponse.transforms); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts index 8abe054daeaf5..778efa05f692e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts @@ -31,8 +31,9 @@ import type { SecuritySolutionRequestHandlerContext, } from '../../../../types'; import { getEndpointAuthzInitialStateMock } from '../../../../../common/endpoint/service/authz'; +import { EndpointAuthz } from '../../../../../common/endpoint/types/authz'; -const createMockClients = () => { +export const createMockClients = () => { const core = coreMock.createRequestHandlerContext(); const license = licensingMock.createLicenseMock(); @@ -67,11 +68,12 @@ type SecuritySolutionRequestHandlerContextMock = }; const createRequestContextMock = ( - clients: MockClients = createMockClients() + clients: MockClients = createMockClients(), + overrides: { endpointAuthz?: Partial } = {} ): SecuritySolutionRequestHandlerContextMock => { return { core: clients.core, - securitySolution: createSecuritySolutionRequestContextMock(clients), + securitySolution: createSecuritySolutionRequestContextMock(clients, overrides), actions: { getActionsClient: jest.fn(() => clients.actionsClient), } as unknown as jest.Mocked, @@ -87,14 +89,15 @@ const createRequestContextMock = ( }; const createSecuritySolutionRequestContextMock = ( - clients: MockClients + clients: MockClients, + overrides: { endpointAuthz?: Partial } = {} ): jest.Mocked => { const core = clients.core; const kibanaRequest = requestMock.create(); return { core, - endpointAuthz: getEndpointAuthzInitialStateMock(), + endpointAuthz: getEndpointAuthzInitialStateMock(overrides.endpointAuthz), getConfig: jest.fn(() => clients.config), getFrameworkRequest: jest.fn(() => { return { diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/data_stream_helper.ts b/x-pack/test/security_solution_endpoint_api_int/apis/data_stream_helper.ts index dc4b4113c6b11..63e9430679f80 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/data_stream_helper.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/data_stream_helper.ts @@ -119,10 +119,12 @@ export async function startTransform( const transformsResponse = await client.transform.getTransform({ transform_id: `${transformId}*`, }); - return transformsResponse.transforms.map((transform) => { - const t = transform as unknown as { id: string }; - return client.transform.startTransform({ transform_id: t.id }); - }); + return Promise.all( + transformsResponse.transforms.map((transform) => { + const t = transform as unknown as { id: string }; + return client.transform.startTransform({ transform_id: t.id }); + }) + ); } export function bulkIndex( diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts index 93f3756fc111c..b0aaf71ef3257 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts @@ -7,6 +7,7 @@ import uuid from 'uuid'; import expect from '@kbn/expect'; +import { TransformGetTransformStatsTransformStats } from '@elastic/elasticsearch/lib/api/types'; import { FtrProviderContext } from '../ftr_provider_context'; import { deleteAllDocsFromMetadataCurrentIndex, @@ -24,10 +25,13 @@ import { HOST_METADATA_LIST_ROUTE, METADATA_UNITED_INDEX, METADATA_UNITED_TRANSFORM, + METADATA_TRANSFORMS_STATUS_ROUTE, + metadataTransformPrefix, } from '../../../plugins/security_solution/common/endpoint/constants'; import { AGENTS_INDEX } from '../../../plugins/fleet/common'; import { generateAgentDocs, generateMetadataDocs } from './metadata.fixtures'; import { indexFleetEndpointPolicy } from '../../../plugins/security_solution/common/endpoint/data_loaders/index_fleet_endpoint_policy'; +import { TRANSFORM_STATES } from '../../../plugins/security_solution/common/constants'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -71,7 +75,6 @@ export default function ({ getService }: FtrProviderContext) { await deleteAllDocsFromFleetAgents(getService); await deleteAllDocsFromMetadataDatastream(getService); await deleteAllDocsFromMetadataCurrentIndex(getService); - await stopTransform(getService, `${METADATA_UNITED_TRANSFORM}*`); await deleteAllDocsFromIndex(getService, METADATA_UNITED_INDEX); }); @@ -504,5 +507,62 @@ export default function ({ getService }: FtrProviderContext) { }); }); }); + + describe('get metadata transforms', () => { + it('should respond forbidden if no fleet access', async () => { + await getService('supertestWithoutAuth') + .get(METADATA_TRANSFORMS_STATUS_ROUTE) + .set('kbn-xsrf', 'xxx') + .expect(401); + }); + + it('correctly returns stopped transform stats', async () => { + await stopTransform(getService, `${metadataTransformPrefix}*`); + await stopTransform(getService, `${METADATA_UNITED_TRANSFORM}*`); + + const { body } = await supertest + .get(METADATA_TRANSFORMS_STATUS_ROUTE) + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(body.count).to.eql(2); + + const transforms: TransformGetTransformStatsTransformStats[] = body.transforms.sort( + ( + a: TransformGetTransformStatsTransformStats, + b: TransformGetTransformStatsTransformStats + ) => a.id > b.id + ); + + expect(transforms[0].id).to.contain(metadataTransformPrefix); + expect(transforms[0].state).to.eql(TRANSFORM_STATES.STOPPED); + expect(transforms[1].id).to.contain(METADATA_UNITED_TRANSFORM); + expect(transforms[1].state).to.eql(TRANSFORM_STATES.STOPPED); + + await startTransform(getService, metadataTransformPrefix); + await startTransform(getService, METADATA_UNITED_TRANSFORM); + }); + + it('correctly returns started transform stats', async () => { + const { body } = await supertest + .get(METADATA_TRANSFORMS_STATUS_ROUTE) + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(body.count).to.eql(2); + + const transforms: TransformGetTransformStatsTransformStats[] = body.transforms.sort( + ( + a: TransformGetTransformStatsTransformStats, + b: TransformGetTransformStatsTransformStats + ) => a.id > b.id + ); + + expect(transforms[0].id).to.contain(metadataTransformPrefix); + expect(transforms[0].state).to.eql(TRANSFORM_STATES.STARTED); + expect(transforms[1].id).to.contain(METADATA_UNITED_TRANSFORM); + expect(transforms[1].state).to.eql(TRANSFORM_STATES.STARTED); + }); + }); }); }