From 1f560c1ffa8f0a76afcab38173b16297b6515056 Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Tue, 29 Sep 2020 17:15:17 -0400 Subject: [PATCH] [SECURITY_SOLUTION] add condition and message for Endpoints enrolling (#77273) (#78848) --- .../pages/endpoint_hosts/store/action.ts | 26 ++++++++- .../pages/endpoint_hosts/store/index.test.ts | 4 ++ .../pages/endpoint_hosts/store/middleware.ts | 55 +++++++++++++++--- .../store/mock_endpoint_result_list.ts | 21 ++++++- .../pages/endpoint_hosts/store/reducer.ts | 26 +++++++++ .../pages/endpoint_hosts/store/selectors.ts | 8 +++ .../management/pages/endpoint_hosts/types.ts | 8 +++ .../pages/endpoint_hosts/view/index.test.tsx | 57 +++++++++++++++++++ .../pages/endpoint_hosts/view/index.tsx | 26 ++++++++- .../store/policy_list/services/ingest.ts | 22 +++++++ 10 files changed, 241 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts index c2a838404b0bba..dce135dd213b33 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts @@ -113,6 +113,26 @@ interface AppRequestedEndpointList { type: 'appRequestedEndpointList'; } +interface ServerReturnedAgenstWithEndpointsTotal { + type: 'serverReturnedAgenstWithEndpointsTotal'; + payload: number; +} + +interface ServerFailedToReturnAgenstWithEndpointsTotal { + type: 'serverFailedToReturnAgenstWithEndpointsTotal'; + payload: ServerApiError; +} + +interface ServerReturnedEndpointsTotal { + type: 'serverReturnedEndpointsTotal'; + payload: number; +} + +interface ServerFailedToReturnEndpointsTotal { + type: 'serverFailedToReturnEndpointsTotal'; + payload: ServerApiError; +} + export type EndpointAction = | ServerReturnedEndpointList | ServerFailedToReturnEndpointList @@ -131,5 +151,9 @@ export type EndpointAction = | ServerFailedToReturnMetadataPatterns | AppRequestedEndpointList | ServerReturnedEndpointNonExistingPolicies + | ServerReturnedAgenstWithEndpointsTotal | ServerReturnedEndpointAgentPolicies - | UserUpdatedEndpointListRefreshOptions; + | UserUpdatedEndpointListRefreshOptions + | ServerReturnedEndpointsTotal + | ServerFailedToReturnAgenstWithEndpointsTotal + | ServerFailedToReturnEndpointsTotal; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts index 61bcd222b1b1ee..84d1dabe869105 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts @@ -58,6 +58,10 @@ describe('EndpointList store concerns', () => { patternsError: undefined, isAutoRefreshEnabled: true, autoRefreshInterval: DEFAULT_POLL_INTERVAL, + agentsWithEndpointsTotal: 0, + endpointsTotal: 0, + agentsWithEndpointsTotalError: undefined, + endpointsTotalError: undefined, queryStrategyVersion: undefined, }); }); 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 7872c8824a8eef..17e0101426b07e 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 @@ -24,6 +24,7 @@ import { sendGetEndpointSpecificPackagePolicies, sendGetEndpointSecurityPackage, sendGetAgentPolicyList, + sendGetFleetAgentsWithEndpoint, } from '../../policy/store/policy_list/services/ingest'; import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../../../../../ingest_manager/common'; import { metadataCurrentIndexPattern } from '../../../../../common/endpoint/constants'; @@ -87,6 +88,32 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory => { +const endpointsTotal = async (http: HttpStart): Promise => { try { return ( - ( - await http.post('/api/endpoint/metadata', { - body: JSON.stringify({ - paging_properties: [{ page_index: 0 }, { page_size: 1 }], - }), - }) - ).hosts.length !== 0 - ); + await http.post('/api/endpoint/metadata', { + body: JSON.stringify({ + paging_properties: [{ page_index: 0 }, { page_size: 1 }], + }), + }) + ).total; + } catch (error) { + // eslint-disable-next-line no-console + console.error(`error while trying to check for total endpoints`); + // eslint-disable-next-line no-console + console.error(error); + } + return 0; +}; + +const doEndpointsExist = async (http: HttpStart): Promise => { + try { + return (await endpointsTotal(http)) > 0; } catch (error) { // eslint-disable-next-line no-console console.error(`error while trying to check if endpoints exist`); 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 43b6d4d349ddfa..ff3bd2d9973c73 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 @@ -18,11 +18,13 @@ import { INGEST_API_AGENT_POLICIES, INGEST_API_EPM_PACKAGES, INGEST_API_PACKAGE_POLICIES, + INGEST_API_FLEET_AGENTS, } from '../../policy/store/policy_list/services/ingest'; import { GetAgentPoliciesResponse, GetAgentPoliciesResponseItem, GetPackagesResponse, + GetAgentsResponse, } from '../../../../../../ingest_manager/common/types/rest_spec'; import { GetPolicyListResponse } from '../../policy/types'; @@ -87,6 +89,7 @@ const endpointListApiPathHandlerMocks = ({ policyResponse = generator.generatePolicyResponse(), agentPolicy = generator.generateAgentPolicy(), queryStrategyVersion = MetadataQueryStrategyVersions.VERSION_2, + totalAgentsUsingEndpoint = 0, }: { /** route handlers will be setup for each individual host in this array */ endpointsResults?: HostResultList['hosts']; @@ -95,6 +98,7 @@ const endpointListApiPathHandlerMocks = ({ policyResponse?: HostPolicyResponse; agentPolicy?: GetAgentPoliciesResponseItem; queryStrategyVersion?: MetadataQueryStrategyVersions; + totalAgentsUsingEndpoint?: number; } = {}) => { const apiHandlers = { // endpoint package info @@ -143,6 +147,17 @@ const endpointListApiPathHandlerMocks = ({ total: endpointPackagePolicies?.length, }; }, + + // List of Agents using Endpoint + [INGEST_API_FLEET_AGENTS]: (): GetAgentsResponse => { + return { + total: totalAgentsUsingEndpoint, + list: [], + totalInactive: 0, + page: 1, + perPage: 10, + }; + }, }; // Build a GET route handler for each endpoint details based on the list of Endpoints passed on input @@ -185,11 +200,15 @@ export const setEndpointListApiMockImplementation: ( throw new Error(`un-expected call to http.post: ${args}`); }) // First time called, return list of endpoints + .mockImplementationOnce(async () => { + return apiHandlers['/api/endpoint/metadata'](); + }) + // Metadata is called a second time to get the full total of Endpoints regardless of filters. .mockImplementationOnce(async () => { return apiHandlers['/api/endpoint/metadata'](); }); - // If the endpoints list results is zero, then mock the second call to `/metadata` to return + // If the endpoints list results is zero, then mock the third call to `/metadata` to return // empty list - indicating there are no endpoints currently present on the system if (!endpointsResults.length) { mockedHttpService.post.mockImplementationOnce(async () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts index 0f948f74a48e4b..26d8dda2f4aec1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts @@ -36,6 +36,10 @@ export const initialEndpointListState: Immutable = { patternsError: undefined, isAutoRefreshEnabled: true, autoRefreshInterval: DEFAULT_POLL_INTERVAL, + agentsWithEndpointsTotal: 0, + agentsWithEndpointsTotalError: undefined, + endpointsTotal: 0, + endpointsTotalError: undefined, queryStrategyVersion: undefined, }; @@ -160,6 +164,28 @@ export const endpointListReducer: ImmutableReducer = ( ...state, endpointsExist: action.payload, }; + } else if (action.type === 'serverReturnedAgenstWithEndpointsTotal') { + return { + ...state, + agentsWithEndpointsTotal: action.payload, + agentsWithEndpointsTotalError: undefined, + }; + } else if (action.type === 'serverFailedToReturnAgenstWithEndpointsTotal') { + return { + ...state, + agentsWithEndpointsTotalError: action.payload, + }; + } else if (action.type === 'serverReturnedEndpointsTotal') { + return { + ...state, + endpointsTotal: action.payload, + endpointsTotalError: undefined, + }; + } else if (action.type === 'serverFailedToReturnEndpointsTotal') { + return { + ...state, + endpointsTotalError: action.payload, + }; } else if (action.type === 'userUpdatedEndpointListRefreshOptions') { return { ...state, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts index fe47d60afc339e..29d9185b6cea55 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts @@ -55,6 +55,14 @@ export const isAutoRefreshEnabled = (state: Immutable) => state.i export const autoRefreshInterval = (state: Immutable) => state.autoRefreshInterval; +export const areEndpointsEnrolling = (state: Immutable) => { + return state.agentsWithEndpointsTotal > state.endpointsTotal; +}; + +export const agentsWithEndpointsTotalError = (state: Immutable) => + state.agentsWithEndpointsTotalError; + +export const endpointsTotalError = (state: Immutable) => state.endpointsTotalError; const queryStrategyVersion = (state: Immutable) => state.queryStrategyVersion; export const endpointPackageVersion = createSelector( diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts index bdd0d5e942cef2..e3e2dc7b55a5e9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts @@ -66,6 +66,14 @@ export interface EndpointState { isAutoRefreshEnabled: boolean; /** The current auto refresh interval for data in ms */ autoRefreshInterval: number; + /** The total Agents that contain an Endpoint package */ + agentsWithEndpointsTotal: number; + /** api error for total Agents that contain an Endpoint package */ + agentsWithEndpointsTotalError?: ServerApiError; + /** The total, actual number of Endpoints regardless of any filtering */ + endpointsTotal: number; + /** api error for total, actual Endpoints */ + endpointsTotalError?: ServerApiError; /** The query strategy version that informs whether the transform for KQL is enabled or not */ queryStrategyVersion?: MetadataQueryStrategyVersions; } diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index bb4be42b04d4ee..debdde901407af 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -150,6 +150,63 @@ describe('when on the list page', () => { }); }); + describe('when determining when to show the enrolling message', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should display the enrolling message when there are less Endpoints than Agents', async () => { + reactTestingLibrary.act(() => { + const mockedEndpointListData = mockEndpointResultList({ + total: 4, + }); + setEndpointListApiMockImplementation(coreStart.http, { + endpointsResults: mockedEndpointListData.hosts, + totalAgentsUsingEndpoint: 5, + }); + }); + const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedAgenstWithEndpointsTotal'); + }); + expect(renderResult.queryByTestId('endpointsEnrollingNotification')).not.toBeNull(); + }); + + it('should NOT display the enrolling message when there are equal Endpoints than Agents', async () => { + reactTestingLibrary.act(() => { + const mockedEndpointListData = mockEndpointResultList({ + total: 5, + }); + setEndpointListApiMockImplementation(coreStart.http, { + endpointsResults: mockedEndpointListData.hosts, + totalAgentsUsingEndpoint: 5, + }); + }); + const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedAgenstWithEndpointsTotal'); + }); + expect(renderResult.queryByTestId('endpointsEnrollingNotification')).toBeNull(); + }); + + it('should NOT display the enrolling message when there are more Endpoints than Agents', async () => { + reactTestingLibrary.act(() => { + const mockedEndpointListData = mockEndpointResultList({ + total: 6, + }); + setEndpointListApiMockImplementation(coreStart.http, { + endpointsResults: mockedEndpointListData.hosts, + totalAgentsUsingEndpoint: 5, + }); + }); + const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedAgenstWithEndpointsTotal'); + }); + expect(renderResult.queryByTestId('endpointsEnrollingNotification')).toBeNull(); + }); + }); + describe('when there is no selected host in the url', () => { it('should not show the flyout', () => { const renderResult = render(); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 3e1f08eee7b947..4bb9335496ef4e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -23,6 +23,7 @@ import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, + EuiCallOut, } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; @@ -135,6 +136,9 @@ export const EndpointList = () => { autoRefreshInterval, isAutoRefreshEnabled, patternsError, + areEndpointsEnrolling, + agentsWithEndpointsTotalError, + endpointsTotalError, isTransformEnabled, } = useEndpointSelector(selector); const { formatUrl, search } = useFormatUrl(SecurityPageName.administration); @@ -486,7 +490,7 @@ export const EndpointList = () => { }, [formatUrl, queryParams, search, agentPolicies, services?.application?.getUrlForApp]); const renderTableOrEmptyState = useMemo(() => { - if (endpointsExist) { + if (endpointsExist || areEndpointsEnrolling) { return ( { handleSelectableOnChange, selectionOptions, handleCreatePolicyClick, + areEndpointsEnrolling, ]); const hasListData = listData && listData.length > 0; @@ -544,6 +549,10 @@ export const EndpointList = () => { return !endpointsExist ? DEFAULT_POLL_INTERVAL : autoRefreshInterval; }, [endpointsExist, autoRefreshInterval]); + const hasErrorFindingTotals = useMemo(() => { + return endpointsTotalError || agentsWithEndpointsTotalError ? true : false; + }, [endpointsTotalError, agentsWithEndpointsTotalError]); + const shouldShowKQLBar = useMemo(() => { return endpointsExist && !patternsError && isTransformEnabled; }, [endpointsExist, patternsError, isTransformEnabled]); @@ -567,6 +576,21 @@ export const EndpointList = () => { > {hasSelectedEndpoint && } <> + {areEndpointsEnrolling && !hasErrorFindingTotals && ( + <> + + } + /> + + + )} {shouldShowKQLBar && ( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts index e3b12e06a7025f..48ee6e4f67ad02 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts @@ -8,6 +8,7 @@ import { HttpFetchOptions, HttpStart } from 'kibana/public'; import { GetPackagePoliciesRequest, GetAgentStatusResponse, + GetAgentsResponse, DeletePackagePoliciesResponse, DeletePackagePoliciesRequest, PACKAGE_POLICY_SAVED_OBJECT_TYPE, @@ -23,6 +24,7 @@ export const INGEST_API_PACKAGE_POLICIES = `${INGEST_API_ROOT}/package_policies` export const INGEST_API_AGENT_POLICIES = `${INGEST_API_ROOT}/agent_policies`; const INGEST_API_FLEET = `${INGEST_API_ROOT}/fleet`; const INGEST_API_FLEET_AGENT_STATUS = `${INGEST_API_FLEET}/agent-status`; +export const INGEST_API_FLEET_AGENTS = `${INGEST_API_FLEET}/agents`; export const INGEST_API_EPM_PACKAGES = `${INGEST_API_ROOT}/epm/packages`; const INGEST_API_DELETE_PACKAGE_POLICY = `${INGEST_API_PACKAGE_POLICIES}/delete`; @@ -131,6 +133,26 @@ export const sendGetFleetAgentStatusForPolicy = ( }); }; +/** + * Get a status summary for all Agents that are currently assigned to a given agent policy + * + * @param http + * @param options + */ +export const sendGetFleetAgentsWithEndpoint = ( + http: HttpStart, + options: Exclude = {} +): Promise => { + return http.get(INGEST_API_FLEET_AGENTS, { + ...options, + query: { + page: 1, + perPage: 1, + kuery: 'fleet-agents.packages : "endpoint"', + }, + }); +}; + /** * Get Endpoint Security Package information */