Skip to content

Commit

Permalink
[SECURITY_SOLUTION] add condition and message for Endpoints enrolling (
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinlog authored Sep 29, 2020
1 parent 2bb44a6 commit 1f560c1
Show file tree
Hide file tree
Showing 10 changed files with 241 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -131,5 +151,9 @@ export type EndpointAction =
| ServerFailedToReturnMetadataPatterns
| AppRequestedEndpointList
| ServerReturnedEndpointNonExistingPolicies
| ServerReturnedAgenstWithEndpointsTotal
| ServerReturnedEndpointAgentPolicies
| UserUpdatedEndpointListRefreshOptions;
| UserUpdatedEndpointListRefreshOptions
| ServerReturnedEndpointsTotal
| ServerFailedToReturnAgenstWithEndpointsTotal
| ServerFailedToReturnEndpointsTotal;
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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'];
Expand All @@ -95,6 +98,7 @@ const endpointListApiPathHandlerMocks = ({
policyResponse?: HostPolicyResponse;
agentPolicy?: GetAgentPoliciesResponseItem;
queryStrategyVersion?: MetadataQueryStrategyVersions;
totalAgentsUsingEndpoint?: number;
} = {}) => {
const apiHandlers = {
// endpoint package info
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiCallOut,
} from '@elastic/eui';
import { useHistory } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
Expand Down Expand Up @@ -135,6 +136,9 @@ export const EndpointList = () => {
autoRefreshInterval,
isAutoRefreshEnabled,
patternsError,
areEndpointsEnrolling,
agentsWithEndpointsTotalError,
endpointsTotalError,
isTransformEnabled,
} = useEndpointSelector(selector);
const { formatUrl, search } = useFormatUrl(SecurityPageName.administration);
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -528,6 +532,7 @@ export const EndpointList = () => {
handleSelectableOnChange,
selectionOptions,
handleCreatePolicyClick,
areEndpointsEnrolling,
]);

const hasListData = listData && listData.length > 0;
Expand All @@ -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]);
Expand All @@ -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>
Expand Down
Loading

0 comments on commit 1f560c1

Please sign in to comment.