mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[SECURITY_SOLUTION] add condition and message for Endpoints enrolling (#77273)
This commit is contained in:
parent
bf93974edc
commit
70d5157d62
10 changed files with 241 additions and 12 deletions
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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`);
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue