From 417d093a29feff76fa1896a8584eb8af0f9d4a10 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Mon, 9 Aug 2021 15:17:08 +0200 Subject: [PATCH] [Security Solution] Move endpointdetails into its own middleware function (#107632) --- .../pages/endpoint_hosts/store/middleware.ts | 786 +++++++++--------- 1 file changed, 414 insertions(+), 372 deletions(-) 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 3219a071a0a0a..19658631e6cf2 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 @@ -94,7 +94,6 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory (next) => async (action) => { next(action); @@ -108,270 +107,12 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory(HOST_METADATA_LIST_ROUTE, { - body: JSON.stringify({ - paging_properties: [{ page_index: pageIndex }, { page_size: pageSize }], - filters: { kql: decodedQuery.query }, - }), - }); - endpointResponse.request_page_index = Number(pageIndex); - - dispatch({ - type: 'serverReturnedEndpointList', - payload: endpointResponse, - }); - - loadEndpointsPendingActions(store); - - try { - const endpointsTotalCount = await endpointsTotal(coreStart.http); - dispatch({ - type: 'serverReturnedEndpointsTotal', - payload: endpointsTotalCount, - }); - } catch (error) { - dispatch({ - type: 'serverFailedToReturnEndpointsTotal', - payload: error, - }); - } - - try { - const agentsWithEndpoint = await sendGetFleetAgentsWithEndpoint(coreStart.http); - dispatch({ - type: 'serverReturnedAgenstWithEndpointsTotal', - payload: agentsWithEndpoint.total, - }); - } catch (error) { - dispatch({ - type: 'serverFailedToReturnAgenstWithEndpointsTotal', - payload: error, - }); - } - - try { - const ingestPolicies = await getAgentAndPoliciesForEndpointsList( - coreStart.http, - endpointResponse.hosts, - nonExistingPolicies(getState()) - ); - if (ingestPolicies?.packagePolicy !== undefined) { - dispatch({ - type: 'serverReturnedEndpointNonExistingPolicies', - payload: ingestPolicies.packagePolicy, - }); - } - if (ingestPolicies?.agentPolicy !== undefined) { - dispatch({ - type: 'serverReturnedEndpointAgentPolicies', - payload: ingestPolicies.agentPolicy, - }); - } - } catch (error) { - // TODO should handle the error instead of logging it to the browser - // Also this is an anti-pattern we shouldn't use - // Ignore Errors, since this should not hinder the user's ability to use the UI - logError(error); - } - } catch (error) { - dispatch({ - type: 'serverFailedToReturnEndpointList', - payload: error, - }); - } - - // get index pattern and fields for search bar - if (patterns(getState()).length === 0) { - try { - const indexPatterns = await fetchIndexPatterns(); - if (indexPatterns !== undefined) { - dispatch({ - type: 'serverReturnedMetadataPatterns', - payload: indexPatterns, - }); - } - } catch (error) { - dispatch({ - type: 'serverFailedToReturnMetadataPatterns', - payload: error, - }); - } - } - - // No endpoints, so we should check to see if there are policies for onboarding - if (endpointResponse && endpointResponse.hosts.length === 0) { - const http = coreStart.http; - - // The original query to the list could have had an invalid param (ex. invalid page_size), - // so we check first if endpoints actually do exist before pulling in data for the onboarding - // messages. - if (await doEndpointsExist(http)) { - return; - } - - dispatch({ - type: 'serverReturnedEndpointExistValue', - payload: false, - }); - - try { - const policyDataResponse: GetPolicyListResponse = await sendGetEndpointSpecificPackagePolicies( - http, - { - query: { - perPage: 50, // Since this is an oboarding flow, we'll cap at 50 policies. - page: 1, - }, - } - ); - - dispatch({ - type: 'serverReturnedPoliciesForOnboarding', - payload: { - policyItems: policyDataResponse.items, - }, - }); - } catch (error) { - dispatch({ - type: 'serverFailedToReturnPoliciesForOnboarding', - payload: error.body ?? error, - }); - return; - } - } else { - dispatch({ - type: 'serverCancelledPolicyItemsLoading', - }); - - dispatch({ - type: 'serverReturnedEndpointExistValue', - payload: true, - }); - } + endpointDetailsListMiddleware({ coreStart, store, fetchIndexPatterns }); } // Endpoint Details if (action.type === 'userChangedUrl' && hasSelectedEndpoint(getState()) === true) { - dispatch({ - type: 'serverCancelledPolicyItemsLoading', - }); - - // If user navigated directly to a endpoint details page, load the endpoint list - if (listData(getState()).length === 0) { - const { page_index: pageIndex, page_size: pageSize } = uiQueryParams(getState()); - try { - const response = await coreStart.http.post(HOST_METADATA_LIST_ROUTE, { - body: JSON.stringify({ - paging_properties: [{ page_index: pageIndex }, { page_size: pageSize }], - }), - }); - response.request_page_index = Number(pageIndex); - dispatch({ - type: 'serverReturnedEndpointList', - payload: response, - }); - - try { - const ingestPolicies = await getAgentAndPoliciesForEndpointsList( - coreStart.http, - response.hosts, - nonExistingPolicies(getState()) - ); - if (ingestPolicies?.packagePolicy !== undefined) { - dispatch({ - type: 'serverReturnedEndpointNonExistingPolicies', - payload: ingestPolicies.packagePolicy, - }); - } - if (ingestPolicies?.agentPolicy !== undefined) { - dispatch({ - type: 'serverReturnedEndpointAgentPolicies', - payload: ingestPolicies.agentPolicy, - }); - } - } catch (error) { - // TODO should handle the error instead of logging it to the browser - // Also this is an anti-pattern we shouldn't use - // Ignore Errors, since this should not hinder the user's ability to use the UI - logError(error); - } - } catch (error) { - dispatch({ - type: 'serverFailedToReturnEndpointList', - payload: error, - }); - } - } else { - dispatch({ - type: 'serverCancelledEndpointListLoading', - }); - } - - // call the endpoint details api - const { selected_endpoint: selectedEndpoint } = uiQueryParams(getState()); - try { - const response = await coreStart.http.get( - resolvePathVariables(HOST_METADATA_GET_ROUTE, { id: selectedEndpoint as string }) - ); - dispatch({ - type: 'serverReturnedEndpointDetails', - payload: response, - }); - - try { - const ingestPolicies = await getAgentAndPoliciesForEndpointsList( - coreStart.http, - [response], - nonExistingPolicies(getState()) - ); - if (ingestPolicies !== undefined) { - dispatch({ - type: 'serverReturnedEndpointNonExistingPolicies', - payload: ingestPolicies.packagePolicy, - }); - } - if (ingestPolicies?.agentPolicy !== undefined) { - dispatch({ - type: 'serverReturnedEndpointAgentPolicies', - payload: ingestPolicies.agentPolicy, - }); - } - } catch (error) { - // TODO should handle the error instead of logging it to the browser - // Also this is an anti-pattern we shouldn't use - // Ignore Errors, since this should not hinder the user's ability to use the UI - logError(error); - } - } catch (error) { - dispatch({ - type: 'serverFailedToReturnEndpointDetails', - payload: error, - }); - } - - loadEndpointsPendingActions(store); - - // call the policy response api - try { - const policyResponse = await coreStart.http.get(BASE_POLICY_RESPONSE_ROUTE, { - query: { agentId: selectedEndpoint }, - }); - dispatch({ - type: 'serverReturnedEndpointPolicyResponse', - payload: policyResponse, - }); - } catch (error) { - dispatch({ - type: 'serverFailedToReturnEndpointPolicyResponse', - payload: error, - }); - } + endpointDetailsMiddleware({ store, coreStart }); } if ( @@ -379,32 +120,7 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory(getActivityLogData(getState())), - }); - - try { - const { page, pageSize } = getActivityLogDataPaging(getState()); - const route = resolvePathVariables(ENDPOINT_ACTION_LOG_ROUTE, { - agent_id: selectedAgent(getState()), - }); - const activityLog = await coreStart.http.get(route, { - query: { page, page_size: pageSize }, - }); - dispatch({ - type: 'endpointDetailsActivityLogChanged', - payload: createLoadedResourceState(activityLog), - }); - } catch (error) { - dispatch({ - type: 'endpointDetailsActivityLogChanged', - payload: createFailedResourceState(error.body ?? error), - }); - } + endpointDetailsActivityLogChangedMiddleware({ store, coreStart }); } // page activity log API @@ -412,91 +128,7 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory(getActivityLogData(getState())), - }); - const route = resolvePathVariables(ENDPOINT_ACTION_LOG_ROUTE, { - agent_id: selectedAgent(getState()), - }); - const activityLog = await coreStart.http.get(route, { - query: { - page, - page_size: pageSize, - start_date: startDate, - end_date: endDate, - }, - }); - - const lastLoadedLogData = getLastLoadedActivityLogData(getState()); - if (lastLoadedLogData !== undefined) { - const updatedLogDataItems = ([ - ...new Set([...lastLoadedLogData.data, ...activityLog.data]), - ] as ActivityLog['data']).sort((a, b) => - new Date(b.item.data['@timestamp']) > new Date(a.item.data['@timestamp']) ? 1 : -1 - ); - - const updatedLogData = { - page: activityLog.page, - pageSize: activityLog.pageSize, - startDate: activityLog.startDate, - endDate: activityLog.endDate, - data: activityLog.page === 1 ? activityLog.data : updatedLogDataItems, - }; - dispatch({ - type: 'endpointDetailsActivityLogChanged', - payload: createLoadedResourceState(updatedLogData), - }); - if (!activityLog.data.length) { - dispatch({ - type: 'endpointDetailsActivityLogUpdatePaging', - payload: { - disabled: true, - page: activityLog.page > 1 ? activityLog.page - 1 : 1, - pageSize: activityLog.pageSize, - startDate: activityLog.startDate, - endDate: activityLog.endDate, - }, - }); - } - } else { - dispatch({ - type: 'endpointDetailsActivityLogChanged', - payload: createLoadedResourceState(activityLog), - }); - } - } catch (error) { - dispatch({ - type: 'endpointDetailsActivityLogChanged', - payload: createFailedResourceState(error.body ?? error), - }); - } + endpointDetailsActivityLogPagingMiddleware({ store, coreStart }); } // Isolate Host @@ -730,6 +362,416 @@ const loadEndpointsPendingActions = async ({ } }; +async function endpointDetailsListMiddleware({ + store, + coreStart, + fetchIndexPatterns, +}: { + store: ImmutableMiddlewareAPI; + coreStart: CoreStart; + fetchIndexPatterns: () => Promise; +}) { + const { getState, dispatch } = store; + + const { page_index: pageIndex, page_size: pageSize } = uiQueryParams(getState()); + let endpointResponse; + + try { + const decodedQuery: Query = searchBarQuery(getState()); + + endpointResponse = await coreStart.http.post(HOST_METADATA_LIST_ROUTE, { + body: JSON.stringify({ + paging_properties: [{ page_index: pageIndex }, { page_size: pageSize }], + filters: { kql: decodedQuery.query }, + }), + }); + endpointResponse.request_page_index = Number(pageIndex); + + dispatch({ + type: 'serverReturnedEndpointList', + payload: endpointResponse, + }); + + loadEndpointsPendingActions(store); + + try { + const endpointsTotalCount = await endpointsTotal(coreStart.http); + dispatch({ + type: 'serverReturnedEndpointsTotal', + payload: endpointsTotalCount, + }); + } catch (error) { + dispatch({ + type: 'serverFailedToReturnEndpointsTotal', + payload: error, + }); + } + + try { + const agentsWithEndpoint = await sendGetFleetAgentsWithEndpoint(coreStart.http); + dispatch({ + type: 'serverReturnedAgenstWithEndpointsTotal', + payload: agentsWithEndpoint.total, + }); + } catch (error) { + dispatch({ + type: 'serverFailedToReturnAgenstWithEndpointsTotal', + payload: error, + }); + } + + try { + const ingestPolicies = await getAgentAndPoliciesForEndpointsList( + coreStart.http, + endpointResponse.hosts, + nonExistingPolicies(getState()) + ); + if (ingestPolicies?.packagePolicy !== undefined) { + dispatch({ + type: 'serverReturnedEndpointNonExistingPolicies', + payload: ingestPolicies.packagePolicy, + }); + } + if (ingestPolicies?.agentPolicy !== undefined) { + dispatch({ + type: 'serverReturnedEndpointAgentPolicies', + payload: ingestPolicies.agentPolicy, + }); + } + } catch (error) { + // TODO should handle the error instead of logging it to the browser + // Also this is an anti-pattern we shouldn't use + // Ignore Errors, since this should not hinder the user's ability to use the UI + logError(error); + } + } catch (error) { + dispatch({ + type: 'serverFailedToReturnEndpointList', + payload: error, + }); + } + + // get index pattern and fields for search bar + if (patterns(getState()).length === 0) { + try { + const indexPatterns = await fetchIndexPatterns(); + if (indexPatterns !== undefined) { + dispatch({ + type: 'serverReturnedMetadataPatterns', + payload: indexPatterns, + }); + } + } catch (error) { + dispatch({ + type: 'serverFailedToReturnMetadataPatterns', + payload: error, + }); + } + } + + // No endpoints, so we should check to see if there are policies for onboarding + if (endpointResponse && endpointResponse.hosts.length === 0) { + const http = coreStart.http; + + // The original query to the list could have had an invalid param (ex. invalid page_size), + // so we check first if endpoints actually do exist before pulling in data for the onboarding + // messages. + if (await doEndpointsExist(http)) { + return; + } + + dispatch({ + type: 'serverReturnedEndpointExistValue', + payload: false, + }); + + try { + const policyDataResponse: GetPolicyListResponse = await sendGetEndpointSpecificPackagePolicies( + http, + { + query: { + perPage: 50, // Since this is an oboarding flow, we'll cap at 50 policies. + page: 1, + }, + } + ); + + dispatch({ + type: 'serverReturnedPoliciesForOnboarding', + payload: { + policyItems: policyDataResponse.items, + }, + }); + } catch (error) { + dispatch({ + type: 'serverFailedToReturnPoliciesForOnboarding', + payload: error.body ?? error, + }); + } + } else { + dispatch({ + type: 'serverCancelledPolicyItemsLoading', + }); + + dispatch({ + type: 'serverReturnedEndpointExistValue', + payload: true, + }); + } +} + +async function endpointDetailsActivityLogPagingMiddleware({ + store, + coreStart, +}: { + store: ImmutableMiddlewareAPI; + coreStart: CoreStart; +}) { + const { getState, dispatch } = store; + try { + const { disabled, page, pageSize, startDate, endDate } = getActivityLogDataPaging(getState()); + // don't page when paging is disabled or when date ranges are invalid + if (disabled) { + return; + } + if (getIsInvalidDateRange({ startDate, endDate })) { + dispatch({ + type: 'endpointDetailsActivityLogUpdateIsInvalidDateRange', + payload: { + isInvalidDateRange: true, + }, + }); + return; + } + + dispatch({ + type: 'endpointDetailsActivityLogUpdateIsInvalidDateRange', + payload: { + isInvalidDateRange: false, + }, + }); + dispatch({ + type: 'endpointDetailsActivityLogChanged', + // ts error to be fixed when AsyncResourceState is refactored (#830) + // @ts-expect-error + payload: createLoadingResourceState(getActivityLogData(getState())), + }); + const route = resolvePathVariables(ENDPOINT_ACTION_LOG_ROUTE, { + agent_id: selectedAgent(getState()), + }); + const activityLog = await coreStart.http.get(route, { + query: { + page, + page_size: pageSize, + start_date: startDate, + end_date: endDate, + }, + }); + + const lastLoadedLogData = getLastLoadedActivityLogData(getState()); + if (lastLoadedLogData !== undefined) { + const updatedLogDataItems = ([ + ...new Set([...lastLoadedLogData.data, ...activityLog.data]), + ] as ActivityLog['data']).sort((a, b) => + new Date(b.item.data['@timestamp']) > new Date(a.item.data['@timestamp']) ? 1 : -1 + ); + + const updatedLogData = { + page: activityLog.page, + pageSize: activityLog.pageSize, + startDate: activityLog.startDate, + endDate: activityLog.endDate, + data: activityLog.page === 1 ? activityLog.data : updatedLogDataItems, + }; + dispatch({ + type: 'endpointDetailsActivityLogChanged', + payload: createLoadedResourceState(updatedLogData), + }); + if (!activityLog.data.length) { + dispatch({ + type: 'endpointDetailsActivityLogUpdatePaging', + payload: { + disabled: true, + page: activityLog.page > 1 ? activityLog.page - 1 : 1, + pageSize: activityLog.pageSize, + startDate: activityLog.startDate, + endDate: activityLog.endDate, + }, + }); + } + } else { + dispatch({ + type: 'endpointDetailsActivityLogChanged', + payload: createLoadedResourceState(activityLog), + }); + } + } catch (error) { + dispatch({ + type: 'endpointDetailsActivityLogChanged', + payload: createFailedResourceState(error.body ?? error), + }); + } +} + +async function endpointDetailsMiddleware({ + store, + coreStart, +}: { + store: ImmutableMiddlewareAPI; + coreStart: CoreStart; +}) { + const { getState, dispatch } = store; + dispatch({ + type: 'serverCancelledPolicyItemsLoading', + }); + + // If user navigated directly to a endpoint details page, load the endpoint list + if (listData(getState()).length === 0) { + const { page_index: pageIndex, page_size: pageSize } = uiQueryParams(getState()); + try { + const response = await coreStart.http.post(HOST_METADATA_LIST_ROUTE, { + body: JSON.stringify({ + paging_properties: [{ page_index: pageIndex }, { page_size: pageSize }], + }), + }); + response.request_page_index = Number(pageIndex); + dispatch({ + type: 'serverReturnedEndpointList', + payload: response, + }); + + try { + const ingestPolicies = await getAgentAndPoliciesForEndpointsList( + coreStart.http, + response.hosts, + nonExistingPolicies(getState()) + ); + if (ingestPolicies?.packagePolicy !== undefined) { + dispatch({ + type: 'serverReturnedEndpointNonExistingPolicies', + payload: ingestPolicies.packagePolicy, + }); + } + if (ingestPolicies?.agentPolicy !== undefined) { + dispatch({ + type: 'serverReturnedEndpointAgentPolicies', + payload: ingestPolicies.agentPolicy, + }); + } + } catch (error) { + // TODO should handle the error instead of logging it to the browser + // Also this is an anti-pattern we shouldn't use + // Ignore Errors, since this should not hinder the user's ability to use the UI + logError(error); + } + } catch (error) { + dispatch({ + type: 'serverFailedToReturnEndpointList', + payload: error, + }); + } + } else { + dispatch({ + type: 'serverCancelledEndpointListLoading', + }); + } + + // call the endpoint details api + const { selected_endpoint: selectedEndpoint } = uiQueryParams(getState()); + try { + const response = await coreStart.http.get( + resolvePathVariables(HOST_METADATA_GET_ROUTE, { id: selectedEndpoint as string }) + ); + dispatch({ + type: 'serverReturnedEndpointDetails', + payload: response, + }); + + try { + const ingestPolicies = await getAgentAndPoliciesForEndpointsList( + coreStart.http, + [response], + nonExistingPolicies(getState()) + ); + if (ingestPolicies !== undefined) { + dispatch({ + type: 'serverReturnedEndpointNonExistingPolicies', + payload: ingestPolicies.packagePolicy, + }); + } + if (ingestPolicies?.agentPolicy !== undefined) { + dispatch({ + type: 'serverReturnedEndpointAgentPolicies', + payload: ingestPolicies.agentPolicy, + }); + } + } catch (error) { + // TODO should handle the error instead of logging it to the browser + // Also this is an anti-pattern we shouldn't use + // Ignore Errors, since this should not hinder the user's ability to use the UI + logError(error); + } + } catch (error) { + dispatch({ + type: 'serverFailedToReturnEndpointDetails', + payload: error, + }); + } + + loadEndpointsPendingActions(store); + + // call the policy response api + try { + const policyResponse = await coreStart.http.get(BASE_POLICY_RESPONSE_ROUTE, { + query: { agentId: selectedEndpoint }, + }); + dispatch({ + type: 'serverReturnedEndpointPolicyResponse', + payload: policyResponse, + }); + } catch (error) { + dispatch({ + type: 'serverFailedToReturnEndpointPolicyResponse', + payload: error, + }); + } +} +async function endpointDetailsActivityLogChangedMiddleware({ + store, + coreStart, +}: { + store: ImmutableMiddlewareAPI; + coreStart: CoreStart; +}) { + const { getState, dispatch } = store; + // call the activity log api + dispatch({ + type: 'endpointDetailsActivityLogChanged', + // ts error to be fixed when AsyncResourceState is refactored (#830) + // @ts-expect-error + payload: createLoadingResourceState(getActivityLogData(getState())), + }); + + try { + const { page, pageSize } = getActivityLogDataPaging(getState()); + const route = resolvePathVariables(ENDPOINT_ACTION_LOG_ROUTE, { + agent_id: selectedAgent(getState()), + }); + const activityLog = await coreStart.http.get(route, { + query: { page, page_size: pageSize }, + }); + dispatch({ + type: 'endpointDetailsActivityLogChanged', + payload: createLoadedResourceState(activityLog), + }); + } catch (error) { + dispatch({ + type: 'endpointDetailsActivityLogChanged', + payload: createFailedResourceState(error.body ?? error), + }); + } +} + export async function handleLoadMetadataTransformStats(http: HttpStart, store: EndpointPageStore) { const { getState, dispatch } = store;