[SecuritySolution] Get endpoint metadata (#99452)

* getHostEndpoint

* add endpointContext

* add deps

* get endpoint info

* clean up

* fix tests error

* fix types

* fix unit tests

* fix unit tests

* fix unit tests

* fix types error

* fix types

* fix api integration test

* fix api integration tests

* add comment

* review

* add getHostInfo

* rename getHostInfo into getHostMetaData

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Angela Chuang 2021-05-10 20:21:36 +01:00 committed by GitHub
parent 65a2177dcf
commit 5893d67b4b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 516 additions and 185 deletions

View file

@ -414,6 +414,11 @@ export type PolicyInfo = Immutable<{
id: string;
}>;
export interface HostMetaDataInfo {
metadata: HostMetadata;
query_strategy_version: MetadataQueryStrategyVersions;
}
export type HostInfo = Immutable<{
metadata: HostMetadata;
host_status: HostStatus;

View file

@ -25,10 +25,16 @@ export interface EndpointFields {
endpointPolicy?: Maybe<string>;
sensorVersion?: Maybe<string>;
policyStatus?: Maybe<HostPolicyResponseActionStatus>;
id?: Maybe<string>;
}
interface AgentFields {
id?: Maybe<string>;
}
export interface HostItem {
_id?: Maybe<string>;
agent?: Maybe<AgentFields>;
cloud?: Maybe<CloudEcs>;
endpoint?: Maybe<EndpointFields>;
host?: Maybe<HostEcs>;
@ -70,6 +76,9 @@ export interface HostAggEsItem {
cloud_machine_type?: HostBuckets;
cloud_provider?: HostBuckets;
cloud_region?: HostBuckets;
endpoint?: {
id: HostBuckets;
};
host_architecture?: HostBuckets;
host_id?: HostBuckets;
host_ip?: HostBuckets;

View file

@ -9,6 +9,9 @@ import { HostItem } from '../../../../../common/search_strategy/security_solutio
import { CriteriaFields } from '../types';
export const hostToCriteria = (hostItem: HostItem): CriteriaFields[] => {
if (hostItem == null) {
return [];
}
if (hostItem.host != null && hostItem.host.name != null) {
const criteria: CriteriaFields[] = [
{

View file

@ -145,14 +145,14 @@ export const useHostDetails = ({
}
return prevRequest;
});
return () => {
searchSubscription$.current.unsubscribe();
abortCtrl.current.abort();
};
}, [endDate, hostName, indexNames, startDate]);
useEffect(() => {
hostDetailsSearch(hostDetailsRequest);
return () => {
searchSubscription$.current.unsubscribe();
abortCtrl.current.abort();
};
}, [hostDetailsRequest, hostDetailsSearch]);
return [loading, hostDetailsResponse];

View file

@ -50,6 +50,9 @@ import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
import { useSourcererScope } from '../../../common/containers/sourcerer';
import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector';
import { useHostDetails } from '../../containers/hosts/details';
import { manageQuery } from '../../../common/components/page/manage_query';
const HostOverviewManage = manageQuery(HostOverview);
const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDetailsPagePath }) => {
const dispatch = useDispatch();
@ -93,11 +96,12 @@ const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDeta
);
const { docValueFields, indicesExist, indexPattern, selectedPatterns } = useSourcererScope();
const [loading, { hostDetails: hostOverview, id }] = useHostDetails({
const [loading, { hostDetails: hostOverview, id, refetch }] = useHostDetails({
endDate: to,
startDate: from,
hostName: detailName,
indexNames: selectedPatterns,
skip: selectedPatterns.length === 0,
});
const filterQuery = convertToBuildEsQuery({
config: esQuery.getEsQueryConfig(kibana.services.uiSettings),
@ -141,7 +145,7 @@ const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDeta
skip={isInitializing}
>
{({ isLoadingAnomaliesData, anomaliesData }) => (
<HostOverview
<HostOverviewManage
docValueFields={docValueFields}
id={id}
isInDetailsSidePanel={false}
@ -160,6 +164,8 @@ const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDeta
to: fromTo.to,
});
}}
setQuery={setQuery}
refetch={refetch}
/>
)}
</AnomalyTableProvider>

View file

@ -321,7 +321,7 @@ export const EndpointList = () => {
render: (hostStatus: HostInfo['host_status']) => {
return (
<EuiBadge
color={HOST_STATUS_TO_BADGE_COLOR[hostStatus] || 'warning'}
color={hostStatus != null ? HOST_STATUS_TO_BADGE_COLOR[hostStatus] : 'warning'}
data-test-subj="rowHostStatus"
className="eui-textTruncate"
>

View file

@ -86,14 +86,15 @@ export const HostOverview = React.memo<HostSummaryProps>(
() => [
{
title: i18n.HOST_ID,
description: data.host
? hostIdRenderer({ host: data.host, noLink: true })
: getEmptyTagValue(),
description:
data && data.host
? hostIdRenderer({ host: data.host, noLink: true })
: getEmptyTagValue(),
},
{
title: i18n.FIRST_SEEN,
description:
data.host != null && data.host.name && data.host.name.length ? (
data && data.host != null && data.host.name && data.host.name.length ? (
<FirstLastSeenHost
docValueFields={docValueFields}
hostName={data.host.name[0]}
@ -107,7 +108,7 @@ export const HostOverview = React.memo<HostSummaryProps>(
{
title: i18n.LAST_SEEN,
description:
data.host != null && data.host.name && data.host.name.length ? (
data && data.host != null && data.host.name && data.host.name.length ? (
<FirstLastSeenHost
docValueFields={docValueFields}
hostName={data.host.name[0]}
@ -221,7 +222,7 @@ export const HostOverview = React.memo<HostSummaryProps>(
)}
</OverviewWrapper>
</InspectButtonContainer>
{data.endpoint != null ? (
{data && data.endpoint != null ? (
<>
<EuiHorizontalRule />
<OverviewWrapper direction={isInDetailsSidePanel ? 'column' : 'row'}>

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import { ILegacyScopedClusterClient, SavedObjectsClientContract } from 'kibana/server';
import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks';
import { loggingSystemMock, savedObjectsServiceMock } from '../../../../../src/core/server/mocks';
import { IScopedClusterClient, SavedObjectsClientContract } from '../../../../../src/core/server';
import { listMock } from '../../../lists/server/mocks';
import { securityMock } from '../../../security/server/mocks';
import { alertsMock } from '../../../alerting/server/mocks';
@ -131,11 +131,11 @@ export const createMockMetadataRequestContext = (): jest.Mocked<MetadataRequestC
};
export function createRouteHandlerContext(
dataClient: jest.Mocked<ILegacyScopedClusterClient>,
dataClient: jest.Mocked<IScopedClusterClient>,
savedObjectsClient: jest.Mocked<SavedObjectsClientContract>
) {
const context = xpackMocks.createRequestHandlerContext();
context.core.elasticsearch.legacy.client = dataClient;
const context = (xpackMocks.createRequestHandlerContext() as unknown) as jest.Mocked<SecuritySolutionRequestHandlerContext>;
context.core.elasticsearch.client = dataClient;
context.core.savedObjects.client = savedObjectsClient;
return context;
}

View file

@ -6,11 +6,18 @@
*/
import Boom from '@hapi/boom';
import type { Logger, RequestHandler } from 'kibana/server';
import { TypeOf } from '@kbn/config-schema';
import {
IScopedClusterClient,
Logger,
RequestHandler,
SavedObjectsClientContract,
} from '../../../../../../../src/core/server';
import {
HostInfo,
HostMetadata,
HostMetaDataInfo,
HostResultList,
HostStatus,
MetadataQueryStrategyVersions,
@ -27,9 +34,11 @@ import { findAgentIDsByStatus } from './support/agent_status';
import { EndpointAppContextService } from '../../endpoint_app_context_services';
export interface MetadataRequestContext {
esClient?: IScopedClusterClient;
endpointAppContextService: EndpointAppContextService;
logger: Logger;
requestHandlerContext: SecuritySolutionRequestHandlerContext;
requestHandlerContext?: SecuritySolutionRequestHandlerContext;
savedObjectsClient?: SavedObjectsClientContract;
}
const HOST_STATUS_MAPPING = new Map<AgentStatus, HostStatus>([
@ -75,9 +84,11 @@ export const getMetadataListRequestHandler = function (
}
const metadataRequestContext: MetadataRequestContext = {
esClient: context.core.elasticsearch.client,
endpointAppContextService: endpointAppContext.service,
logger,
requestHandlerContext: context,
savedObjectsClient: context.core.savedObjects.client,
};
const unenrolledAgentIds = await findAllUnenrolledAgentIds(
@ -110,9 +121,10 @@ export const getMetadataListRequestHandler = function (
}
);
const hostListQueryResult = queryStrategy!.queryResponseToHostListResult(
await context.core.elasticsearch.legacy.client.callAsCurrentUser('search', queryParams)
const result = await context.core.elasticsearch.client.asCurrentUser.search<HostMetadata>(
queryParams
);
const hostListQueryResult = queryStrategy!.queryResponseToHostListResult(result.body);
return response.ok({
body: await mapToHostResultList(queryParams, hostListQueryResult, metadataRequestContext),
});
@ -136,9 +148,11 @@ export const getMetadataRequestHandler = function (
}
const metadataRequestContext: MetadataRequestContext = {
esClient: context.core.elasticsearch.client,
endpointAppContextService: endpointAppContext.service,
logger,
requestHandlerContext: context,
savedObjectsClient: context.core.savedObjects.client,
};
try {
@ -164,42 +178,86 @@ export const getMetadataRequestHandler = function (
};
};
export async function getHostData(
export async function getHostMetaData(
metadataRequestContext: MetadataRequestContext,
id: string,
queryStrategyVersion?: MetadataQueryStrategyVersions
): Promise<HostInfo | undefined> {
): Promise<HostMetaDataInfo | undefined> {
if (
!metadataRequestContext.esClient &&
!metadataRequestContext.requestHandlerContext?.core.elasticsearch.client
) {
throw Boom.badRequest('esClient not found');
}
if (
!metadataRequestContext.savedObjectsClient &&
!metadataRequestContext.requestHandlerContext?.core.savedObjects
) {
throw Boom.badRequest('savedObjectsClient not found');
}
const esClient = (metadataRequestContext?.esClient ??
metadataRequestContext.requestHandlerContext?.core.elasticsearch
.client) as IScopedClusterClient;
const esSavedObjectClient =
metadataRequestContext?.savedObjectsClient ??
(metadataRequestContext.requestHandlerContext?.core.savedObjects
.client as SavedObjectsClientContract);
const queryStrategy = await metadataRequestContext.endpointAppContextService
?.getMetadataService()
?.queryStrategy(
metadataRequestContext.requestHandlerContext.core.savedObjects.client,
queryStrategyVersion
);
?.queryStrategy(esSavedObjectClient, queryStrategyVersion);
const query = getESQueryHostMetadataByID(id, queryStrategy!);
const hostResult = queryStrategy!.queryResponseToHostResult(
await metadataRequestContext.requestHandlerContext.core.elasticsearch.legacy.client.callAsCurrentUser(
'search',
query
)
);
const response = await esClient.asCurrentUser.search<HostMetadata>(query);
const hostResult = queryStrategy!.queryResponseToHostResult(response.body);
const hostMetadata = hostResult.result;
if (!hostMetadata) {
return undefined;
}
const agent = await findAgent(metadataRequestContext, hostMetadata);
return { metadata: hostMetadata, query_strategy_version: hostResult.queryStrategyVersion };
}
export async function getHostData(
metadataRequestContext: MetadataRequestContext,
id: string,
queryStrategyVersion?: MetadataQueryStrategyVersions
): Promise<HostInfo | undefined> {
if (!metadataRequestContext.savedObjectsClient) {
throw Boom.badRequest('savedObjectsClient not found');
}
if (
!metadataRequestContext.esClient &&
!metadataRequestContext.requestHandlerContext?.core.elasticsearch.client
) {
throw Boom.badRequest('esClient not found');
}
const hostResult = await getHostMetaData(metadataRequestContext, id, queryStrategyVersion);
if (!hostResult) {
return undefined;
}
const agent = await findAgent(metadataRequestContext, hostResult.metadata);
if (agent && !agent.active) {
throw Boom.badRequest('the requested endpoint is unenrolled');
}
const metadata = await enrichHostMetadata(
hostMetadata,
hostResult.metadata,
metadataRequestContext,
hostResult.queryStrategyVersion
hostResult.query_strategy_version
);
return { ...metadata, query_strategy_version: hostResult.queryStrategyVersion };
return { ...metadata, query_strategy_version: hostResult.query_strategy_version };
}
async function findAgent(
@ -207,12 +265,20 @@ async function findAgent(
hostMetadata: HostMetadata
): Promise<Agent | undefined> {
try {
if (
!metadataRequestContext.esClient &&
!metadataRequestContext.requestHandlerContext?.core.elasticsearch.client
) {
throw new Error('esClient not found');
}
const esClient = (metadataRequestContext?.esClient ??
metadataRequestContext.requestHandlerContext?.core.elasticsearch
.client) as IScopedClusterClient;
return await metadataRequestContext.endpointAppContextService
?.getAgentService()
?.getAgent(
metadataRequestContext.requestHandlerContext.core.elasticsearch.client.asCurrentUser,
hostMetadata.elastic.agent.id
);
?.getAgent(esClient.asCurrentUser, hostMetadata.elastic.agent.id);
} catch (e) {
if (e instanceof AgentNotFoundError) {
metadataRequestContext.logger.warn(
@ -232,7 +298,7 @@ export async function mapToHostResultList(
metadataRequestContext: MetadataRequestContext
): Promise<HostResultList> {
const totalNumberOfHosts = hostListQueryResult.resultLength;
if (hostListQueryResult.resultList.length > 0) {
if ((hostListQueryResult.resultList?.length ?? 0) > 0) {
return {
request_page_size: queryParams.size,
request_page_index: queryParams.from,
@ -267,6 +333,35 @@ export async function enrichHostMetadata(
let hostStatus = HostStatus.UNHEALTHY;
let elasticAgentId = hostMetadata?.elastic?.agent?.id;
const log = metadataRequestContext.logger;
try {
if (
!metadataRequestContext.esClient &&
!metadataRequestContext.requestHandlerContext?.core.elasticsearch.client
) {
throw new Error('esClient not found');
}
if (
!metadataRequestContext.savedObjectsClient &&
!metadataRequestContext.requestHandlerContext?.core.savedObjects
) {
throw new Error('esSavedObjectClient not found');
}
} catch (e) {
log.error(e);
throw e;
}
const esClient = (metadataRequestContext?.esClient ??
metadataRequestContext.requestHandlerContext?.core.elasticsearch
.client) as IScopedClusterClient;
const esSavedObjectClient =
metadataRequestContext?.savedObjectsClient ??
(metadataRequestContext.requestHandlerContext?.core.savedObjects
.client as SavedObjectsClientContract);
try {
/**
* Get agent status by elastic agent id if available or use the endpoint-agent id.
@ -279,10 +374,7 @@ export async function enrichHostMetadata(
const status = await metadataRequestContext.endpointAppContextService
?.getAgentService()
?.getAgentStatusById(
metadataRequestContext.requestHandlerContext.core.elasticsearch.client.asCurrentUser,
elasticAgentId
);
?.getAgentStatusById(esClient.asCurrentUser, elasticAgentId);
hostStatus = HOST_STATUS_MAPPING.get(status!) || HostStatus.UNHEALTHY;
} catch (e) {
if (e instanceof AgentNotFoundError) {
@ -297,17 +389,10 @@ export async function enrichHostMetadata(
try {
const agent = await metadataRequestContext.endpointAppContextService
?.getAgentService()
?.getAgent(
metadataRequestContext.requestHandlerContext.core.elasticsearch.client.asCurrentUser,
elasticAgentId
);
?.getAgent(esClient.asCurrentUser, elasticAgentId);
const agentPolicy = await metadataRequestContext.endpointAppContextService
.getAgentPolicyService()
?.get(
metadataRequestContext.requestHandlerContext.core.savedObjects.client,
agent?.policy_id!,
true
);
?.get(esSavedObjectClient, agent?.policy_id!, true);
const endpointPolicy = ((agentPolicy?.package_policies || []) as PackagePolicy[]).find(
(policy: PackagePolicy) => policy.package?.name === 'endpoint'
);

View file

@ -6,8 +6,6 @@
*/
import {
ILegacyClusterClient,
ILegacyScopedClusterClient,
KibanaResponseFactory,
RequestHandler,
RouteConfig,
@ -50,12 +48,17 @@ import { PackageService } from '../../../../../fleet/server/services';
import { metadataTransformPrefix } from '../../../../common/endpoint/constants';
import type { SecuritySolutionPluginRouter } from '../../../types';
import { PackagePolicyServiceInterface } from '../../../../../fleet/server';
import {
ClusterClientMock,
ScopedClusterClientMock,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../../../../src/core/server/elasticsearch/client/mocks';
describe('test endpoint route', () => {
let routerMock: jest.Mocked<SecuritySolutionPluginRouter>;
let mockResponse: jest.Mocked<KibanaResponseFactory>;
let mockClusterClient: jest.Mocked<ILegacyClusterClient>;
let mockScopedClient: jest.Mocked<ILegacyScopedClusterClient>;
let mockClusterClient: ClusterClientMock;
let mockScopedClient: ScopedClusterClientMock;
let mockSavedObjectClient: jest.Mocked<SavedObjectsClientContract>;
let mockPackageService: jest.Mocked<PackageService>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -76,8 +79,8 @@ describe('test endpoint route', () => {
};
beforeEach(() => {
mockScopedClient = elasticsearchServiceMock.createLegacyScopedClusterClient();
mockClusterClient = elasticsearchServiceMock.createLegacyClusterClient() as jest.Mocked<ILegacyClusterClient>;
mockScopedClient = elasticsearchServiceMock.createScopedClusterClient();
mockClusterClient = elasticsearchServiceMock.createClusterClient();
mockSavedObjectClient = savedObjectsClientMock.create();
mockClusterClient.asScoped.mockReturnValue(mockScopedClient);
routerMock = httpServiceMock.createRouter();
@ -119,7 +122,9 @@ describe('test endpoint route', () => {
it('test find the latest of all endpoints', async () => {
const mockRequest = httpServerMock.createKibanaRequest({});
const response = createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata());
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response));
(mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({ body: response })
);
[routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) =>
path.startsWith(`${METADATA_REQUEST_ROUTE}`)
)!;
@ -131,7 +136,7 @@ describe('test endpoint route', () => {
mockResponse
);
expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1);
expect(routeConfig.options).toEqual({
authRequired: true,
tags: ['access:securitySolution'],
@ -157,7 +162,9 @@ describe('test endpoint route', () => {
mockAgentService.getAgent = jest.fn().mockReturnValue(({
active: true,
} as unknown) as Agent);
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response));
(mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({ body: response })
);
[routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
path.startsWith(`${METADATA_REQUEST_ROUTE}`)
@ -169,7 +176,7 @@ describe('test endpoint route', () => {
mockResponse
);
expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1);
expect(routeConfig.options).toEqual({
authRequired: true,
tags: ['access:securitySolution'],
@ -214,7 +221,9 @@ describe('test endpoint route', () => {
it('test find the latest of all endpoints', async () => {
const mockRequest = httpServerMock.createKibanaRequest({});
const response = createV2SearchResponse(new EndpointDocGenerator().generateHostMetadata());
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response));
(mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({ body: response })
);
[routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) =>
path.startsWith(`${METADATA_REQUEST_ROUTE}`)
)!;
@ -226,7 +235,7 @@ describe('test endpoint route', () => {
mockResponse
);
expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1);
expect(routeConfig.options).toEqual({
authRequired: true,
tags: ['access:securitySolution'],
@ -258,8 +267,10 @@ describe('test endpoint route', () => {
mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error');
mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent);
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() =>
Promise.resolve(createV2SearchResponse(new EndpointDocGenerator().generateHostMetadata()))
(mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({
body: createV2SearchResponse(new EndpointDocGenerator().generateHostMetadata()),
})
);
[routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) =>
path.startsWith(`${METADATA_REQUEST_ROUTE}`)
@ -270,10 +281,10 @@ describe('test endpoint route', () => {
mockRequest,
mockResponse
);
expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1);
expect(
mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query.bool.must_not
(mockScopedClient.asCurrentUser.search as jest.Mock).mock.calls[0][0]?.body?.query.bool
.must_not
).toContainEqual({
terms: {
'elastic.agent.id': [
@ -315,8 +326,10 @@ describe('test endpoint route', () => {
mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error');
mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent);
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() =>
Promise.resolve(createV2SearchResponse(new EndpointDocGenerator().generateHostMetadata()))
(mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({
body: createV2SearchResponse(new EndpointDocGenerator().generateHostMetadata()),
})
);
[routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) =>
path.startsWith(`${METADATA_REQUEST_ROUTE}`)
@ -328,10 +341,10 @@ describe('test endpoint route', () => {
mockResponse
);
expect(mockScopedClient.callAsCurrentUser).toBeCalled();
expect(mockScopedClient.asCurrentUser.search).toBeCalled();
expect(
// KQL filter to be passed through
mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query.bool.must
(mockScopedClient.asCurrentUser.search as jest.Mock).mock.calls[0][0]?.body?.query.bool.must
).toContainEqual({
bool: {
must_not: {
@ -349,7 +362,7 @@ describe('test endpoint route', () => {
},
});
expect(
mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query.bool.must
(mockScopedClient.asCurrentUser.search as jest.Mock).mock.calls[0][0]?.body?.query.bool.must
).toContainEqual({
bool: {
must_not: [
@ -393,8 +406,8 @@ describe('test endpoint route', () => {
it('should return 404 on no results', async () => {
const mockRequest = httpServerMock.createKibanaRequest({ params: { id: 'BADID' } });
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() =>
Promise.resolve(createV2SearchResponse())
(mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({ body: createV2SearchResponse() })
);
mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error');
@ -411,7 +424,7 @@ describe('test endpoint route', () => {
mockResponse
);
expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1);
expect(routeConfig.options).toEqual({
authRequired: true,
tags: ['access:securitySolution'],
@ -431,7 +444,9 @@ describe('test endpoint route', () => {
mockAgentService.getAgent = jest.fn().mockReturnValue(({
active: true,
} as unknown) as Agent);
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response));
(mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({ body: response })
);
[routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
path.startsWith(`${METADATA_REQUEST_ROUTE}`)
@ -443,7 +458,7 @@ describe('test endpoint route', () => {
mockResponse
);
expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1);
expect(routeConfig.options).toEqual({
authRequired: true,
tags: ['access:securitySolution'],
@ -470,7 +485,9 @@ describe('test endpoint route', () => {
SavedObjectsErrorHelpers.createGenericNotFoundError();
});
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response));
(mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({ body: response })
);
[routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
path.startsWith(`${METADATA_REQUEST_ROUTE}`)
@ -482,7 +499,7 @@ describe('test endpoint route', () => {
mockResponse
);
expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1);
expect(routeConfig.options).toEqual({
authRequired: true,
tags: ['access:securitySolution'],
@ -503,7 +520,9 @@ describe('test endpoint route', () => {
mockAgentService.getAgent = jest.fn().mockReturnValue(({
active: true,
} as unknown) as Agent);
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response));
(mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({ body: response })
);
[routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
path.startsWith(`${METADATA_REQUEST_ROUTE}`)
@ -515,7 +534,7 @@ describe('test endpoint route', () => {
mockResponse
);
expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1);
expect(routeConfig.options).toEqual({
authRequired: true,
tags: ['access:securitySolution'],
@ -531,7 +550,9 @@ describe('test endpoint route', () => {
const mockRequest = httpServerMock.createKibanaRequest({
params: { id: response.hits.hits[0]._id },
});
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response));
(mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({ body: response })
);
mockAgentService.getAgent = jest.fn().mockReturnValue(({
active: false,
} as unknown) as Agent);
@ -546,7 +567,7 @@ describe('test endpoint route', () => {
mockResponse
);
expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1);
expect(mockResponse.customError).toBeCalled();
});
});

View file

@ -6,14 +6,17 @@
*/
import {
ILegacyClusterClient,
ILegacyScopedClusterClient,
KibanaResponseFactory,
RequestHandler,
RouteConfig,
SavedObjectsClientContract,
} from 'kibana/server';
import { SavedObjectsErrorHelpers } from '../../../../../../../src/core/server/';
SavedObjectsErrorHelpers,
} from '../../../../../../../src/core/server';
import {
ClusterClientMock,
ScopedClusterClientMock,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../../../../src/core/server/elasticsearch/client/mocks';
import {
elasticsearchServiceMock,
httpServerMock,
@ -49,8 +52,8 @@ import { PackagePolicyServiceInterface } from '../../../../../fleet/server';
describe('test endpoint route v1', () => {
let routerMock: jest.Mocked<SecuritySolutionPluginRouter>;
let mockResponse: jest.Mocked<KibanaResponseFactory>;
let mockClusterClient: jest.Mocked<ILegacyClusterClient>;
let mockScopedClient: jest.Mocked<ILegacyScopedClusterClient>;
let mockClusterClient: ClusterClientMock;
let mockScopedClient: ScopedClusterClientMock;
let mockSavedObjectClient: jest.Mocked<SavedObjectsClientContract>;
let mockPackageService: jest.Mocked<PackageService>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -71,8 +74,8 @@ describe('test endpoint route v1', () => {
};
beforeEach(() => {
mockClusterClient = elasticsearchServiceMock.createLegacyClusterClient() as jest.Mocked<ILegacyClusterClient>;
mockScopedClient = elasticsearchServiceMock.createLegacyScopedClusterClient();
mockClusterClient = elasticsearchServiceMock.createClusterClient();
mockScopedClient = elasticsearchServiceMock.createScopedClusterClient();
mockSavedObjectClient = savedObjectsClientMock.create();
mockClusterClient.asScoped.mockReturnValue(mockScopedClient);
routerMock = httpServiceMock.createRouter();
@ -110,7 +113,9 @@ describe('test endpoint route v1', () => {
it('test find the latest of all endpoints', async () => {
const mockRequest = httpServerMock.createKibanaRequest({});
const response = createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata());
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response));
(mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({ body: response })
);
[routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) =>
path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`)
)!;
@ -122,7 +127,7 @@ describe('test endpoint route v1', () => {
mockResponse
);
expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1);
expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'] });
expect(mockResponse.ok).toBeCalled();
const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as HostResultList;
@ -151,8 +156,10 @@ describe('test endpoint route v1', () => {
mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error');
mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent);
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() =>
Promise.resolve(createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()))
(mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({
body: createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()),
})
);
[routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) =>
path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`)
@ -164,9 +171,10 @@ describe('test endpoint route v1', () => {
mockResponse
);
expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1);
expect(
mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query.bool.must_not
(mockScopedClient.asCurrentUser.search as jest.Mock).mock.calls[0][0]?.body?.query.bool
.must_not
).toContainEqual({
terms: {
'elastic.agent.id': [
@ -205,8 +213,10 @@ describe('test endpoint route v1', () => {
mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error');
mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent);
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() =>
Promise.resolve(createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()))
(mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({
body: createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()),
})
);
[routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) =>
path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`)
@ -218,10 +228,10 @@ describe('test endpoint route v1', () => {
mockResponse
);
expect(mockScopedClient.callAsCurrentUser).toBeCalled();
expect(mockScopedClient.asCurrentUser.search).toBeCalled();
// needs to have the KQL filter passed through
expect(
mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query.bool.must
(mockScopedClient.asCurrentUser.search as jest.Mock).mock.calls[0][0]?.body?.query.bool.must
).toContainEqual({
bool: {
must_not: {
@ -240,7 +250,7 @@ describe('test endpoint route v1', () => {
});
// and unenrolled should be filtered out.
expect(
mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query.bool.must
(mockScopedClient.asCurrentUser.search as jest.Mock).mock.calls[0][0]?.body?.query.bool.must
).toContainEqual({
bool: {
must_not: [
@ -281,8 +291,8 @@ describe('test endpoint route v1', () => {
it('should return 404 on no results', async () => {
const mockRequest = httpServerMock.createKibanaRequest({ params: { id: 'BADID' } });
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() =>
Promise.resolve(createV1SearchResponse())
(mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({ body: createV1SearchResponse() })
);
mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error');
@ -299,7 +309,7 @@ describe('test endpoint route v1', () => {
mockResponse
);
expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1);
expect(routeConfig.options).toEqual({
authRequired: true,
tags: ['access:securitySolution'],
@ -319,7 +329,9 @@ describe('test endpoint route v1', () => {
mockAgentService.getAgent = jest.fn().mockReturnValue(({
active: true,
} as unknown) as Agent);
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response));
(mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({ body: response })
);
[routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`)
@ -331,7 +343,7 @@ describe('test endpoint route v1', () => {
mockResponse
);
expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1);
expect(routeConfig.options).toEqual({
authRequired: true,
tags: ['access:securitySolution'],
@ -357,7 +369,9 @@ describe('test endpoint route v1', () => {
SavedObjectsErrorHelpers.createGenericNotFoundError();
});
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response));
(mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({ body: response })
);
[routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`)
@ -369,7 +383,7 @@ describe('test endpoint route v1', () => {
mockResponse
);
expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1);
expect(routeConfig.options).toEqual({
authRequired: true,
tags: ['access:securitySolution'],
@ -390,7 +404,9 @@ describe('test endpoint route v1', () => {
mockAgentService.getAgent = jest.fn().mockReturnValue(({
active: true,
} as unknown) as Agent);
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response));
(mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({ body: response })
);
[routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`)
@ -402,7 +418,7 @@ describe('test endpoint route v1', () => {
mockResponse
);
expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1);
expect(routeConfig.options).toEqual({
authRequired: true,
tags: ['access:securitySolution'],
@ -418,7 +434,9 @@ describe('test endpoint route v1', () => {
const mockRequest = httpServerMock.createKibanaRequest({
params: { id: response.hits.hits[0]._id },
});
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response));
(mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({ body: response })
);
mockAgentService.getAgent = jest.fn().mockReturnValue(({
active: false,
} as unknown) as Agent);
@ -433,7 +451,7 @@ describe('test endpoint route v1', () => {
mockResponse
);
expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1);
expect(mockResponse.customError).toBeCalled();
});
});

View file

@ -12,6 +12,7 @@ import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__
import { metadataCurrentIndexPattern } from '../../../../common/endpoint/constants';
import { parseExperimentalConfigValue } from '../../../../common/experimental_features';
import { metadataQueryStrategyV2 } from './support/query_strategies';
import { get } from 'lodash';
describe('query builder', () => {
describe('MetadataListESQuery', () => {
@ -204,7 +205,7 @@ describe('query builder', () => {
const mockID = 'AABBCCDD-0011-2233-AA44-DEADBEEF8899';
const query = getESQueryHostMetadataByID(mockID, metadataQueryStrategyV2());
expect(query.body.query.bool.filter[0].bool.should).toContainEqual({
expect(get(query, 'body.query.bool.filter.0.bool.should')).toContainEqual({
term: { 'agent.id': mockID },
});
});
@ -213,7 +214,7 @@ describe('query builder', () => {
const mockID = 'AABBCCDD-0011-2233-AA44-DEADBEEF8899';
const query = getESQueryHostMetadataByID(mockID, metadataQueryStrategyV2());
expect(query.body.query.bool.filter[0].bool.should).toContainEqual({
expect(get(query, 'body.query.bool.filter.0.bool.should')).toContainEqual({
term: { 'HostDetails.agent.id': mockID },
});
});

View file

@ -5,7 +5,8 @@
* 2.0.
*/
import { KibanaRequest } from 'kibana/server';
import { SearchRequest, SortContainer } from '@elastic/elasticsearch/api/types';
import { KibanaRequest } from '../../../../../../../src/core/server';
import { esKuery } from '../../../../../../../src/plugins/data/server';
import { EndpointAppContext, MetadataQueryStrategy } from '../../types';
@ -19,7 +20,7 @@ export interface QueryBuilderOptions {
// using unmapped_type avoids errors when the given field doesn't exist, and sets to the 0-value for that type
// effectively ignoring it
// https://www.elastic.co/guide/en/elasticsearch/reference/current/sort-search-results.html#_ignoring_unmapped_fields
const MetadataSortMethod = [
const MetadataSortMethod: SortContainer[] = [
{
'event.created': {
order: 'desc',
@ -146,7 +147,7 @@ function buildQueryBody(
export function getESQueryHostMetadataByID(
agentID: string,
metadataQueryStrategy: MetadataQueryStrategy
) {
): SearchRequest {
return {
body: {
query: {

View file

@ -12,6 +12,7 @@ import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__
import { metadataIndexPattern } from '../../../../common/endpoint/constants';
import { parseExperimentalConfigValue } from '../../../../common/experimental_features';
import { metadataQueryStrategyV1 } from './support/query_strategies';
import { get } from 'lodash';
describe('query builder v1', () => {
describe('MetadataListESQuery', () => {
@ -179,7 +180,7 @@ describe('query builder v1', () => {
const mockID = 'AABBCCDD-0011-2233-AA44-DEADBEEF8899';
const query = getESQueryHostMetadataByID(mockID, metadataQueryStrategyV1());
expect(query.body.query.bool.filter[0].bool.should).toContainEqual({
expect(get(query, 'body.query.bool.filter.0.bool.should')).toContainEqual({
term: { 'agent.id': mockID },
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { SearchResponse } from 'elasticsearch';
import { SearchResponse } from '@elastic/elasticsearch/api/types';
import {
metadataCurrentIndexPattern,
metadataIndexPattern,
@ -13,10 +13,6 @@ import {
import { HostMetadata, MetadataQueryStrategyVersions } from '../../../../../common/endpoint/types';
import { HostListQueryResult, HostQueryResult, MetadataQueryStrategy } from '../../../types';
interface HitSource {
_source: HostMetadata;
}
export function metadataQueryStrategyV1(): MetadataQueryStrategy {
return {
index: metadataIndexPattern,
@ -42,11 +38,13 @@ export function metadataQueryStrategyV1(): MetadataQueryStrategy {
): HostListQueryResult => {
const response = searchResponse as SearchResponse<HostMetadata>;
return {
resultLength: response?.aggregations?.total?.value || 0,
resultLength:
((response?.aggregations?.total as unknown) as { value?: number; relation: string })
?.value || 0,
resultList: response.hits.hits
.map((hit) => hit.inner_hits.most_recent.hits.hits)
.flatMap((data) => data as HitSource)
.map((entry) => entry._source),
.map((hit) => hit.inner_hits?.most_recent.hits.hits)
.flatMap((data) => data)
.map((entry) => (entry?._source ?? {}) as HostMetadata),
queryStrategyVersion: MetadataQueryStrategyVersions.VERSION_1,
};
},
@ -75,7 +73,7 @@ export function metadataQueryStrategyV2(): MetadataQueryStrategy {
>;
const list =
response.hits.hits.length > 0
? response.hits.hits.map((entry) => stripHostDetails(entry._source))
? response.hits.hits.map((entry) => stripHostDetails(entry?._source as HostMetadata))
: [];
return {
@ -95,7 +93,7 @@ export function metadataQueryStrategyV2(): MetadataQueryStrategy {
resultLength: response.hits.hits.length,
result:
response.hits.hits.length > 0
? stripHostDetails(response.hits.hits[0]._source)
? stripHostDetails(response.hits.hits[0]._source as HostMetadata)
: undefined,
queryStrategyVersion: MetadataQueryStrategyVersions.VERSION_2,
};

View file

@ -13,10 +13,9 @@ import {
import { createMockAgentService } from '../../../../../fleet/server/mocks';
import { getHostPolicyResponseHandler, getAgentPolicySummaryHandler } from './handlers';
import {
ILegacyScopedClusterClient,
KibanaResponseFactory,
SavedObjectsClientContract,
} from 'kibana/server';
} from '../../../../../../../src/core/server';
import {
elasticsearchServiceMock,
httpServerMock,
@ -30,16 +29,19 @@ import { parseExperimentalConfigValue } from '../../../../common/experimental_fe
import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__';
import { Agent } from '../../../../../fleet/common/types/models';
import { AgentService } from '../../../../../fleet/server/services';
import { get } from 'lodash';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { ScopedClusterClientMock } from '../../../../../../../src/core/server/elasticsearch/client/mocks';
describe('test policy response handler', () => {
let endpointAppContextService: EndpointAppContextService;
let mockScopedClient: jest.Mocked<ILegacyScopedClusterClient>;
let mockScopedClient: ScopedClusterClientMock;
let mockSavedObjectClient: jest.Mocked<SavedObjectsClientContract>;
let mockResponse: jest.Mocked<KibanaResponseFactory>;
describe('test policy response handler', () => {
beforeEach(() => {
mockScopedClient = elasticsearchServiceMock.createLegacyScopedClusterClient();
mockScopedClient = elasticsearchServiceMock.createScopedClusterClient();
mockSavedObjectClient = savedObjectsClientMock.create();
mockResponse = httpServerMock.createResponseFactory();
endpointAppContextService = new EndpointAppContextService();
@ -52,7 +54,9 @@ describe('test policy response handler', () => {
const response = createSearchResponse(new EndpointDocGenerator().generatePolicyResponse());
const hostPolicyResponseHandler = getHostPolicyResponseHandler();
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response));
(mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({ body: response })
);
const mockRequest = httpServerMock.createKibanaRequest({
params: { agentId: 'id' },
});
@ -65,14 +69,16 @@ describe('test policy response handler', () => {
expect(mockResponse.ok).toBeCalled();
const result = mockResponse.ok.mock.calls[0][0]?.body as GetHostPolicyResponse;
expect(result.policy_response.agent.id).toEqual(response.hits.hits[0]._source.agent.id);
expect(result.policy_response.agent.id).toEqual(
get(response, 'hits.hits.0._source.agent.id')
);
});
it('should return not found when there is no response policy for host', async () => {
const hostPolicyResponseHandler = getHostPolicyResponseHandler();
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() =>
Promise.resolve(createSearchResponse())
(mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({ body: createSearchResponse() })
);
const mockRequest = httpServerMock.createKibanaRequest({
@ -109,7 +115,7 @@ describe('test policy response handler', () => {
};
beforeEach(() => {
mockScopedClient = elasticsearchServiceMock.createLegacyScopedClusterClient();
mockScopedClient = elasticsearchServiceMock.createScopedClusterClient();
mockSavedObjectClient = savedObjectsClientMock.create();
mockResponse = httpServerMock.createResponseFactory();
endpointAppContextService = new EndpointAppContextService();

View file

@ -25,7 +25,7 @@ export const getHostPolicyResponseHandler = function (): RequestHandler<
const doc = await getPolicyResponseByAgentId(
policyIndexPattern,
request.query.agentId,
context.core.elasticsearch.legacy.client
context.core.elasticsearch.client
);
if (doc) {

View file

@ -24,7 +24,7 @@ describe('test policy query', () => {
it('queries for the correct host', async () => {
const agentId = 'f757d3c0-e874-11ea-9ad9-015510b487f4';
const query = getESQueryPolicyResponseByAgentID(agentId, 'anyindex');
expect(query.body.query.bool.filter.term).toEqual({ 'agent.id': agentId });
expect(query.body?.query?.bool?.filter).toEqual({ term: { 'agent.id': agentId } });
});
it('filters out initial policy by ID', async () => {
@ -32,8 +32,10 @@ describe('test policy query', () => {
'f757d3c0-e874-11ea-9ad9-015510b487f4',
'anyindex'
);
expect(query.body.query.bool.must_not.term).toEqual({
'Endpoint.policy.applied.id': '00000000-0000-0000-0000-000000000000',
expect(query.body?.query?.bool?.must_not).toEqual({
term: {
'Endpoint.policy.applied.id': '00000000-0000-0000-0000-000000000000',
},
});
});
});

View file

@ -5,18 +5,21 @@
* 2.0.
*/
import { SearchResponse } from 'elasticsearch';
import {
ElasticsearchClient,
ILegacyScopedClusterClient,
IScopedClusterClient,
SavedObjectsClientContract,
} from 'kibana/server';
} from '../../../../../../../src/core/server';
import { GetHostPolicyResponse, HostPolicyResponse } from '../../../../common/endpoint/types';
import { INITIAL_POLICY_ID } from './index';
import { Agent } from '../../../../../fleet/common/types/models';
import { EndpointAppContext } from '../../types';
import { ISearchRequestParams } from '../../../../../../../src/plugins/data/common';
export function getESQueryPolicyResponseByAgentID(agentID: string, index: string) {
export const getESQueryPolicyResponseByAgentID = (
agentID: string,
index: string
): ISearchRequestParams => {
return {
body: {
query: {
@ -44,26 +47,23 @@ export function getESQueryPolicyResponseByAgentID(agentID: string, index: string
},
index,
};
}
};
export async function getPolicyResponseByAgentId(
index: string,
agentID: string,
dataClient: ILegacyScopedClusterClient
dataClient: IScopedClusterClient
): Promise<GetHostPolicyResponse | undefined> {
const query = getESQueryPolicyResponseByAgentID(agentID, index);
const response = (await dataClient.callAsCurrentUser(
'search',
query
)) as SearchResponse<HostPolicyResponse>;
const response = await dataClient.asCurrentUser.search<HostPolicyResponse>(query);
if (response.hits.hits.length === 0) {
return undefined;
if (response.body.hits.hits.length > 0 && response.body.hits.hits[0]._source != null) {
return {
policy_response: response.body.hits.hits[0]._source,
};
}
return {
policy_response: response.hits.hits[0]._source,
};
return undefined;
}
const transformAgentVersionMap = (versionMap: Map<string, number>): { [key: string]: number } => {

View file

@ -6,7 +6,8 @@
*/
import { LoggerFactory } from 'kibana/server';
import { SearchResponse } from 'elasticsearch';
import { SearchResponse } from '@elastic/elasticsearch/api/types';
import { ConfigType } from '../config';
import { EndpointAppContextService } from './endpoint_app_context_services';
import { JsonObject } from '../../../../../src/plugins/kibana_utils/common';

View file

@ -305,7 +305,10 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
compose(core, plugins, endpointContext);
core.getStartServices().then(([_, depsStart]) => {
const securitySolutionSearchStrategy = securitySolutionSearchStrategyProvider(depsStart.data);
const securitySolutionSearchStrategy = securitySolutionSearchStrategyProvider(
depsStart.data,
endpointContext
);
const securitySolutionTimelineSearchStrategy = securitySolutionTimelineSearchStrategyProvider(
depsStart.data
);

View file

@ -58,7 +58,6 @@ export const authentications: SecuritySolutionFactory<HostsQueries.authenticatio
dsl: [inspectStringifyObject(buildAuthenticationQuery(options))],
};
const showMorePagesIndicator = totalCount > fakeTotalCount;
return {
...response,
inspect,

View file

@ -1370,6 +1370,20 @@ export const formattedSearchStrategyResponse = {
terms: { field: 'cloud.region', size: 10, order: { timestamp: 'desc' } },
aggs: { timestamp: { max: { field: '@timestamp' } } },
},
endpoint_id: {
filter: {
term: {
'agent.type': 'endpoint',
},
},
aggs: {
value: {
terms: {
field: 'agent.id',
},
},
},
},
},
query: {
bool: {
@ -1413,6 +1427,20 @@ export const expectedDsl = {
track_total_hits: false,
body: {
aggregations: {
endpoint_id: {
filter: {
term: {
'agent.type': 'endpoint',
},
},
aggs: {
value: {
terms: {
field: 'agent.id',
},
},
},
},
host_architecture: {
terms: {
field: 'host.architecture',

View file

@ -7,16 +7,23 @@
import { set } from '@elastic/safer-lodash-set/fp';
import { get, has, head } from 'lodash/fp';
import {
IScopedClusterClient,
SavedObjectsClientContract,
} from '../../../../../../../../../src/core/server';
import { hostFieldsMap } from '../../../../../../common/ecs/ecs_fields';
import { toObjectArrayOfStrings } from '../../../../../../common/utils/to_array';
import { Direction } from '../../../../../../common/search_strategy/common';
import {
AggregationRequest,
EndpointFields,
HostAggEsItem,
HostBuckets,
HostItem,
HostValue,
} from '../../../../../../common/search_strategy/security_solution/hosts';
import { toObjectArrayOfStrings } from '../../../../../../common/utils/to_array';
import { getHostMetaData } from '../../../../../endpoint/routes/metadata/handlers';
import { EndpointAppContext } from '../../../../../endpoint/types';
export const HOST_FIELDS = [
'_id',
@ -38,6 +45,8 @@ export const HOST_FIELDS = [
'endpoint.endpointPolicy',
'endpoint.policyStatus',
'endpoint.sensorVersion',
'agent.type',
'endpoint.id',
];
export const buildFieldsTermAggregation = (esFields: readonly string[]): AggregationRequest =>
@ -99,8 +108,8 @@ const getTermsAggregationTypeFromField = (field: string): AggregationRequest =>
};
};
export const formatHostItem = (bucket: HostAggEsItem): HostItem =>
HOST_FIELDS.reduce<HostItem>((flattenedFields, fieldName) => {
export const formatHostItem = (bucket: HostAggEsItem): HostItem => {
return HOST_FIELDS.reduce<HostItem>((flattenedFields, fieldName) => {
const fieldValue = getHostFieldValue(fieldName, bucket);
if (fieldValue != null) {
if (fieldName === '_id') {
@ -114,11 +123,13 @@ export const formatHostItem = (bucket: HostAggEsItem): HostItem =>
}
return flattenedFields;
}, {});
};
const getHostFieldValue = (fieldName: string, bucket: HostAggEsItem): string | string[] | null => {
const aggField = hostFieldsMap[fieldName]
? hostFieldsMap[fieldName].replace(/\./g, '_')
: fieldName.replace(/\./g, '_');
if (
[
'host.ip',
@ -134,10 +145,7 @@ const getHostFieldValue = (fieldName: string, bucket: HostAggEsItem): string | s
return data.buckets.map((obj) => obj.key);
} else if (has(`${aggField}.buckets`, bucket)) {
return getFirstItem(get(`${aggField}`, bucket));
} else if (has(aggField, bucket)) {
const valueObj: HostValue = get(aggField, bucket);
return valueObj.value_as_string;
} else if (['host.name', 'host.os.name', 'host.os.version'].includes(fieldName)) {
} else if (['host.name', 'host.os.name', 'host.os.version', 'endpoint.id'].includes(fieldName)) {
switch (fieldName) {
case 'host.name':
return get('key', bucket) || null;
@ -145,7 +153,12 @@ const getHostFieldValue = (fieldName: string, bucket: HostAggEsItem): string | s
return get('os.hits.hits[0]._source.host.os.name', bucket) || null;
case 'host.os.version':
return get('os.hits.hits[0]._source.host.os.version', bucket) || null;
case 'endpoint.id':
return get('endpoint_id.value.buckets[0].key', bucket) || null;
}
} else if (has(aggField, bucket)) {
const valueObj: HostValue = get(aggField, bucket);
return valueObj.value_as_string;
} else if (aggField === '_id') {
const hostName = get(`host_name`, bucket);
return hostName ? getFirstItem(hostName) : null;
@ -160,3 +173,42 @@ const getFirstItem = (data: HostBuckets): string | null => {
}
return firstItem.key;
};
export const getHostEndpoint = async (
id: string | null,
deps: {
esClient: IScopedClusterClient;
savedObjectsClient: SavedObjectsClientContract;
endpointContext: EndpointAppContext;
}
): Promise<EndpointFields | null> => {
const { esClient, endpointContext, savedObjectsClient } = deps;
const logger = endpointContext.logFactory.get('metadata');
try {
const agentService = endpointContext.service.getAgentService();
if (agentService === undefined) {
throw new Error('agentService not available');
}
const metadataRequestContext = {
esClient,
endpointAppContextService: endpointContext.service,
logger,
savedObjectsClient,
};
const endpointData =
id != null && metadataRequestContext.endpointAppContextService.getAgentService() != null
? await getHostMetaData(metadataRequestContext, id, undefined)
: null;
return endpointData != null && endpointData.metadata
? {
endpointPolicy: endpointData.metadata.Endpoint.policy.applied.name,
policyStatus: endpointData.metadata.Endpoint.policy.applied.status,
sensorVersion: endpointData.metadata.agent.version,
}
: null;
} catch (err) {
logger.warn(JSON.stringify(err, null, 2));
return null;
}
};

View file

@ -12,6 +12,32 @@ import {
mockSearchStrategyResponse,
formattedSearchStrategyResponse,
} from './__mocks__';
import {
IScopedClusterClient,
SavedObjectsClientContract,
} from '../../../../../../../../../src/core/server';
import { EndpointAppContext } from '../../../../../endpoint/types';
import { EndpointAppContextService } from '../../../../../endpoint/endpoint_app_context_services';
const mockDeps = {
esClient: {} as IScopedClusterClient,
savedObjectsClient: {} as SavedObjectsClientContract,
endpointContext: {
logFactory: {
get: jest.fn().mockReturnValue({
warn: jest.fn(),
}),
},
config: jest.fn().mockResolvedValue({}),
experimentalFeatures: {
trustedAppsByPolicyEnabled: false,
metricsEntitiesEnabled: false,
eventFilteringEnabled: false,
hostIsolationEnabled: false,
},
service: {} as EndpointAppContextService,
} as EndpointAppContext,
};
describe('hostDetails search strategy', () => {
const buildHostDetailsQuery = jest.spyOn(buildQuery, 'buildHostDetailsQuery');
@ -29,7 +55,7 @@ describe('hostDetails search strategy', () => {
describe('parse', () => {
test('should parse data correctly', async () => {
const result = await hostDetails.parse(mockOptions, mockSearchStrategyResponse);
const result = await hostDetails.parse(mockOptions, mockSearchStrategyResponse, mockDeps);
expect(result).toMatchObject(formattedSearchStrategyResponse);
});
});

View file

@ -10,28 +10,58 @@ import { get } from 'lodash/fp';
import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common';
import {
HostAggEsData,
HostAggEsItem,
HostDetailsStrategyResponse,
HostsQueries,
HostDetailsRequestOptions,
EndpointFields,
} from '../../../../../../common/search_strategy/security_solution/hosts';
import { inspectStringifyObject } from '../../../../../utils/build_query';
import { SecuritySolutionFactory } from '../../types';
import { buildHostDetailsQuery } from './query.host_details.dsl';
import { formatHostItem } from './helpers';
import { formatHostItem, getHostEndpoint } from './helpers';
import { EndpointAppContext } from '../../../../../endpoint/types';
import {
IScopedClusterClient,
SavedObjectsClientContract,
} from '../../../../../../../../../src/core/server';
export const hostDetails: SecuritySolutionFactory<HostsQueries.details> = {
buildDsl: (options: HostDetailsRequestOptions) => buildHostDetailsQuery(options),
parse: async (
options: HostDetailsRequestOptions,
response: IEsSearchResponse<HostAggEsData>
response: IEsSearchResponse<HostAggEsData>,
deps?: {
esClient: IScopedClusterClient;
savedObjectsClient: SavedObjectsClientContract;
endpointContext: EndpointAppContext;
}
): Promise<HostDetailsStrategyResponse> => {
const aggregations: HostAggEsItem = get('aggregations', response.rawResponse) || {};
const aggregations = get('aggregations', response.rawResponse);
const inspect = {
dsl: [inspectStringifyObject(buildHostDetailsQuery(options))],
};
if (aggregations == null) {
return { ...response, inspect, hostDetails: {} };
}
const formattedHostItem = formatHostItem(aggregations);
return { ...response, inspect, hostDetails: formattedHostItem };
const ident = // endpoint-generated ID, NOT elastic-agent-id
formattedHostItem.endpoint && formattedHostItem.endpoint.id
? Array.isArray(formattedHostItem.endpoint.id)
? formattedHostItem.endpoint.id[0]
: formattedHostItem.endpoint.id
: null;
if (deps == null) {
return { ...response, inspect, hostDetails: { ...formattedHostItem } };
}
const endpoint: EndpointFields | null = await getHostEndpoint(ident, deps);
return {
...response,
inspect,
hostDetails: endpoint != null ? { ...formattedHostItem, endpoint } : formattedHostItem,
};
},
};

View file

@ -16,7 +16,10 @@ export const buildHostDetailsQuery = ({
defaultIndex,
timerange: { from, to },
}: HostDetailsRequestOptions): ISearchRequestParams => {
const esFields = reduceFields(HOST_FIELDS, { ...hostFieldsMap, ...cloudFieldsMap });
const esFields = reduceFields(HOST_FIELDS, {
...hostFieldsMap,
...cloudFieldsMap,
});
const filter = [
{ term: { 'host.name': hostName } },
@ -39,6 +42,20 @@ export const buildHostDetailsQuery = ({
body: {
aggregations: {
...buildFieldsTermAggregation(esFields.filter((field) => !['@timestamp'].includes(field))),
endpoint_id: {
filter: {
term: {
'agent.type': 'endpoint',
},
},
aggs: {
value: {
terms: {
field: 'agent.id',
},
},
},
},
},
query: { bool: { filter } },
size: 0,

View file

@ -5,6 +5,10 @@
* 2.0.
*/
import {
IScopedClusterClient,
SavedObjectsClientContract,
} from '../../../../../../../src/core/server';
import {
IEsSearchResponse,
ISearchRequestParams,
@ -14,11 +18,17 @@ import {
StrategyRequestType,
StrategyResponseType,
} from '../../../../common/search_strategy/security_solution';
import { EndpointAppContext } from '../../../endpoint/types';
export interface SecuritySolutionFactory<T extends FactoryQueryTypes> {
buildDsl: (options: StrategyRequestType<T>) => ISearchRequestParams;
parse: (
options: StrategyRequestType<T>,
response: IEsSearchResponse
response: IEsSearchResponse,
deps?: {
esClient: IScopedClusterClient;
savedObjectsClient: SavedObjectsClientContract;
endpointContext: EndpointAppContext;
}
) => Promise<StrategyResponseType<T>>;
}

View file

@ -19,9 +19,11 @@ import {
} from '../../../common/search_strategy/security_solution';
import { securitySolutionFactory } from './factory';
import { SecuritySolutionFactory } from './factory/types';
import { EndpointAppContext } from '../../endpoint/types';
export const securitySolutionSearchStrategyProvider = <T extends FactoryQueryTypes>(
data: PluginStart
data: PluginStart,
endpointContext: EndpointAppContext
): ISearchStrategy<StrategyRequestType<T>, StrategyResponseType<T>> => {
const es = data.search.getSearchStrategy(ENHANCED_ES_SEARCH_STRATEGY);
@ -42,7 +44,13 @@ export const securitySolutionSearchStrategyProvider = <T extends FactoryQueryTyp
},
};
}),
mergeMap((esSearchRes) => queryFactory.parse(request, esSearchRes))
mergeMap((esSearchRes) =>
queryFactory.parse(request, esSearchRes, {
esClient: deps.esClient,
savedObjectsClient: deps.savedObjectsClient,
endpointContext,
})
)
);
},
cancel: async (id, options, deps) => {