[SECURITY_SOLUTION] add condition and message for Endpoints enrolling (#77273)

This commit is contained in:
Kevin Logan 2020-09-29 15:13:12 -04:00 committed by GitHub
parent bf93974edc
commit 70d5157d62
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 241 additions and 12 deletions

View file

@ -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;

View file

@ -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,
});
});

View file

@ -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<EndpointState
payload: endpointResponse,
});
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,
@ -371,17 +398,27 @@ const getAgentAndPoliciesForEndpointsList = async (
return nonExistingPackagePoliciesAndExistingAgentPolicies;
};
const doEndpointsExist = async (http: HttpStart): Promise<boolean> => {
const endpointsTotal = async (http: HttpStart): Promise<number> => {
try {
return (
(
await http.post<HostResultList>('/api/endpoint/metadata', {
body: JSON.stringify({
paging_properties: [{ page_index: 0 }, { page_size: 1 }],
}),
})
).hosts.length !== 0
);
await http.post<HostResultList>('/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<boolean> => {
try {
return (await endpointsTotal(http)) > 0;
} catch (error) {
// eslint-disable-next-line no-console
console.error(`error while trying to check if endpoints exist`);

View file

@ -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 () => {

View file

@ -36,6 +36,10 @@ export const initialEndpointListState: Immutable<EndpointState> = {
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<EndpointState, AppAction> = (
...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,

View file

@ -55,6 +55,14 @@ export const isAutoRefreshEnabled = (state: Immutable<EndpointState>) => state.i
export const autoRefreshInterval = (state: Immutable<EndpointState>) => state.autoRefreshInterval;
export const areEndpointsEnrolling = (state: Immutable<EndpointState>) => {
return state.agentsWithEndpointsTotal > state.endpointsTotal;
};
export const agentsWithEndpointsTotalError = (state: Immutable<EndpointState>) =>
state.agentsWithEndpointsTotalError;
export const endpointsTotalError = (state: Immutable<EndpointState>) => state.endpointsTotalError;
const queryStrategyVersion = (state: Immutable<EndpointState>) => state.queryStrategyVersion;
export const endpointPackageVersion = createSelector(

View file

@ -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;
}

View file

@ -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();

View file

@ -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 (
<EuiBasicTable
data-test-subj="endpointListTable"
@ -528,6 +532,7 @@ export const EndpointList = () => {
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 && <EndpointDetailsFlyout />}
<>
{areEndpointsEnrolling && !hasErrorFindingTotals && (
<>
<EuiCallOut
size="s"
data-test-subj="endpointsEnrollingNotification"
title={
<FormattedMessage
id="xpack.securitySolution.endpoint.list.endpointsEnrolling"
defaultMessage="Endpoints are enrolling and will display soon"
/>
}
/>
<EuiSpacer size="m" />
</>
)}
<EuiFlexGroup>
{shouldShowKQLBar && (
<EuiFlexItem>

View file

@ -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<HttpFetchOptions, 'query'> = {}
): Promise<GetAgentsResponse> => {
return http.get(INGEST_API_FLEET_AGENTS, {
...options,
query: {
page: 1,
perPage: 1,
kuery: 'fleet-agents.packages : "endpoint"',
},
});
};
/**
* Get Endpoint Security Package information
*/