Skip to content

Commit

Permalink
[Security Solution] add new GET endpoint metadata list api (#118968)
Browse files Browse the repository at this point in the history
  • Loading branch information
joeypoon authored and dmlemeshko committed Nov 29, 2021
1 parent 730d5a8 commit 7ca3749
Show file tree
Hide file tree
Showing 11 changed files with 2,088 additions and 966 deletions.
21 changes: 20 additions & 1 deletion x-pack/plugins/security_solution/common/endpoint/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export interface ResolverPaginatedEvents {
}

/**
* Returned by the server via /api/endpoint/metadata
* Returned by the server via POST /api/endpoint/metadata
*/
export interface HostResultList {
/* the hosts restricted by the page size */
Expand Down Expand Up @@ -1231,3 +1231,22 @@ export interface ListPageRouteState {
/** The label for the button */
backButtonLabel?: string;
}

/**
* REST API standard base response for list types
*/
export interface BaseListResponse {
data: unknown[];
page: number;
pageSize: number;
total: number;
sort?: string;
sortOrder?: 'asc' | 'desc';
}

/**
* Returned by the server via GET /api/endpoint/metadata
*/
export interface MetadataListResponse extends BaseListResponse {
data: HostInfo[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { TypeOf } from '@kbn/config-schema';
import {
IKibanaResponse,
IScopedClusterClient,
KibanaRequest,
KibanaResponseFactory,
Logger,
RequestHandler,
Expand All @@ -22,6 +21,7 @@ import {
HostMetadata,
HostResultList,
HostStatus,
MetadataListResponse,
} from '../../../../common/endpoint/types';
import type { SecuritySolutionRequestHandlerContext } from '../../../types';

Expand All @@ -33,7 +33,11 @@ import {
import { Agent, PackagePolicy } from '../../../../../fleet/common/types/models';
import { AgentNotFoundError } from '../../../../../fleet/server';
import { EndpointAppContext, HostListQueryResult } from '../../types';
import { GetMetadataListRequestSchema, GetMetadataRequestSchema } from './index';
import {
GetMetadataListRequestSchema,
GetMetadataListRequestSchemaV2,
GetMetadataRequestSchema,
} from './index';
import { findAllUnenrolledAgentIds } from './support/unenroll';
import { getAllEndpointPackagePolicies } from './support/endpoint_package_policies';
import { findAgentIdsByStatus } from './support/agent_status';
Expand Down Expand Up @@ -125,33 +129,35 @@ export const getMetadataListRequestHandler = function (
context.core.savedObjects.client
);

body = await legacyListMetadataQuery(
context,
request,
endpointAppContext,
logger,
endpointPolicies
);
const pagingProperties = await getPagingProperties(request, endpointAppContext);

body = await legacyListMetadataQuery(context, endpointAppContext, logger, endpointPolicies, {
page: pagingProperties.pageIndex,
pageSize: pagingProperties.pageSize,
kuery: request?.body?.filters?.kql || '',
hostStatuses: request?.body?.filters?.host_status || [],
});
return response.ok({ body });
}

// Unified index is installed and being used - perform search using new approach
try {
const pagingProperties = await getPagingProperties(request, endpointAppContext);
const { data, page, total, pageSize } = await endpointMetadataService.getHostMetadataList(
const { data, total } = await endpointMetadataService.getHostMetadataList(
context.core.elasticsearch.client.asCurrentUser,
{
page: pagingProperties.pageIndex + 1,
page: pagingProperties.pageIndex,
pageSize: pagingProperties.pageSize,
filters: request.body?.filters || {},
hostStatuses: request.body?.filters.host_status || [],
kuery: request.body?.filters.kql || '',
}
);

body = {
hosts: data,
request_page_index: page - 1,
total,
request_page_size: pageSize,
request_page_index: pagingProperties.pageIndex * pagingProperties.pageSize,
request_page_size: pagingProperties.pageSize,
};
} catch (error) {
return errorHandler(logger, response, error);
Expand All @@ -161,6 +167,83 @@ export const getMetadataListRequestHandler = function (
};
};

export function getMetadataListRequestHandlerV2(
endpointAppContext: EndpointAppContext,
logger: Logger
): RequestHandler<
unknown,
TypeOf<typeof GetMetadataListRequestSchemaV2.query>,
unknown,
SecuritySolutionRequestHandlerContext
> {
return async (context, request, response) => {
const endpointMetadataService = endpointAppContext.service.getEndpointMetadataService();
if (!endpointMetadataService) {
throw new EndpointError('endpoint metadata service not available');
}

let doesUnitedIndexExist = false;
let didUnitedIndexError = false;
let body: MetadataListResponse = {
data: [],
total: 0,
page: 0,
pageSize: 0,
};

try {
doesUnitedIndexExist = await endpointMetadataService.doesUnitedIndexExist(
context.core.elasticsearch.client.asCurrentUser
);
} catch (error) {
// for better UX, try legacy query instead of immediately failing on united index error
didUnitedIndexError = true;
}

// If no unified Index present, then perform a search using the legacy approach
if (!doesUnitedIndexExist || didUnitedIndexError) {
const endpointPolicies = await getAllEndpointPackagePolicies(
endpointAppContext.service.getPackagePolicyService(),
context.core.savedObjects.client
);

const legacyResponse = await legacyListMetadataQuery(
context,
endpointAppContext,
logger,
endpointPolicies,
request.query
);
body = {
data: legacyResponse.hosts,
total: legacyResponse.total,
page: request.query.page,
pageSize: request.query.pageSize,
};
return response.ok({ body });
}

// Unified index is installed and being used - perform search using new approach
try {
const { data, total } = await endpointMetadataService.getHostMetadataList(
context.core.elasticsearch.client.asCurrentUser,
request.query
);

body = {
data,
total,
page: request.query.page,
pageSize: request.query.pageSize,
};
} catch (error) {
return errorHandler(logger, response, error);
}

return response.ok({ body });
};
}

export const getMetadataRequestHandler = function (
endpointAppContext: EndpointAppContext,
logger: Logger
Expand Down Expand Up @@ -420,11 +503,10 @@ export async function enrichHostMetadata(

async function legacyListMetadataQuery(
context: SecuritySolutionRequestHandlerContext,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
request: KibanaRequest<any, any, any>,
endpointAppContext: EndpointAppContext,
logger: Logger,
endpointPolicies: PackagePolicy[]
endpointPolicies: PackagePolicy[],
queryOptions: TypeOf<typeof GetMetadataListRequestSchemaV2.query>
): Promise<HostResultList> {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const agentService = endpointAppContext.service.getAgentService()!;
Expand All @@ -447,14 +529,16 @@ async function legacyListMetadataQuery(
endpointPolicyIds
);

const statusesToFilter = request?.body?.filters?.host_status ?? [];
const statusAgentIds = await findAgentIdsByStatus(
agentService,
context.core.elasticsearch.client.asCurrentUser,
statusesToFilter
queryOptions.hostStatuses
);

const queryParams = await kibanaRequestToMetadataListESQuery(request, endpointAppContext, {
const queryParams = await kibanaRequestToMetadataListESQuery({
page: queryOptions.page,
pageSize: queryOptions.pageSize,
kuery: queryOptions.kuery,
unenrolledAgentIds,
statusAgentIds,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import { schema } from '@kbn/config-schema';

import { HostStatus } from '../../../../common/endpoint/types';
import { EndpointAppContext } from '../../types';
import { getLogger, getMetadataListRequestHandler, getMetadataRequestHandler } from './handlers';
import {
getLogger,
getMetadataListRequestHandler,
getMetadataRequestHandler,
getMetadataListRequestHandlerV2,
} from './handlers';
import type { SecuritySolutionPluginRouter } from '../../../types';
import {
HOST_METADATA_GET_ROUTE,
Expand Down Expand Up @@ -60,27 +65,54 @@ export const GetMetadataListRequestSchema = {
),
};

export const GetMetadataListRequestSchemaV2 = {
query: schema.object({
page: schema.number({ defaultValue: 0 }),
pageSize: schema.number({ defaultValue: 10, min: 1, max: 10000 }),
kuery: schema.maybe(schema.string()),
hostStatuses: schema.arrayOf(
schema.oneOf([
schema.literal(HostStatus.HEALTHY.toString()),
schema.literal(HostStatus.OFFLINE.toString()),
schema.literal(HostStatus.UPDATING.toString()),
schema.literal(HostStatus.UNHEALTHY.toString()),
schema.literal(HostStatus.INACTIVE.toString()),
]),
{ defaultValue: [] }
),
}),
};

export function registerEndpointRoutes(
router: SecuritySolutionPluginRouter,
endpointAppContext: EndpointAppContext
) {
const logger = getLogger(endpointAppContext);

router.post(
router.get(
{
path: `${HOST_METADATA_LIST_ROUTE}`,
validate: GetMetadataListRequestSchema,
path: HOST_METADATA_LIST_ROUTE,
validate: GetMetadataListRequestSchemaV2,
options: { authRequired: true, tags: ['access:securitySolution'] },
},
getMetadataListRequestHandler(endpointAppContext, logger)
getMetadataListRequestHandlerV2(endpointAppContext, logger)
);

router.get(
{
path: `${HOST_METADATA_GET_ROUTE}`,
path: HOST_METADATA_GET_ROUTE,
validate: GetMetadataRequestSchema,
options: { authRequired: true, tags: ['access:securitySolution'] },
},
getMetadataRequestHandler(endpointAppContext, logger)
);

router.post(
{
path: HOST_METADATA_LIST_ROUTE,
validate: GetMetadataListRequestSchema,
options: { authRequired: true, tags: ['access:securitySolution'] },
},
getMetadataListRequestHandler(endpointAppContext, logger)
);
}
Loading

0 comments on commit 7ca3749

Please sign in to comment.