[Security Solution] remove query strategy v1 (#104196)

This commit is contained in:
Joey F. Poon 2021-07-10 18:34:03 -05:00 committed by GitHub
parent 857dc9f2e1
commit e4ba52928c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 125 additions and 1593 deletions

View file

@ -178,8 +178,6 @@ export interface HostResultList {
request_page_size: number;
/* the page index requested */
request_page_index: number;
/* the version of the query strategy */
query_strategy_version: MetadataQueryStrategyVersions;
/* policy IDs and versions */
policy_info?: HostInfo['policy_info'];
}
@ -404,21 +402,11 @@ export enum HostStatus {
INACTIVE = 'inactive',
}
export enum MetadataQueryStrategyVersions {
VERSION_1 = 'v1',
VERSION_2 = 'v2',
}
export type PolicyInfo = Immutable<{
revision: number;
id: string;
}>;
export interface HostMetadataInfo {
metadata: HostMetadata;
query_strategy_version: MetadataQueryStrategyVersions;
}
export type HostInfo = Immutable<{
metadata: HostMetadata;
host_status: HostStatus;
@ -438,8 +426,6 @@ export type HostInfo = Immutable<{
*/
endpoint: PolicyInfo;
};
/* the version of the query strategy */
query_strategy_version: MetadataQueryStrategyVersions;
}>;
// HostMetadataDetails is now just HostMetadata

View file

@ -15,7 +15,6 @@ import {
HostPolicyResponse,
HostResultList,
HostStatus,
MetadataQueryStrategyVersions,
} from '../../../../common/endpoint/types';
import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data';
import {
@ -54,7 +53,6 @@ export const endpointMetadataHttpMocks = httpHandlerMockFactory<EndpointMetadata
const endpoint = {
metadata: generator.generateHostMetadata(),
host_status: HostStatus.UNHEALTHY,
query_strategy_version: MetadataQueryStrategyVersions.VERSION_2,
};
generator.updateCommonInfo();
@ -64,7 +62,6 @@ export const endpointMetadataHttpMocks = httpHandlerMockFactory<EndpointMetadata
total: 10,
request_page_size: 10,
request_page_index: 0,
query_strategy_version: MetadataQueryStrategyVersions.VERSION_2,
};
},
},
@ -78,7 +75,6 @@ export const endpointMetadataHttpMocks = httpHandlerMockFactory<EndpointMetadata
return {
metadata: generator.generateHostMetadata(),
host_status: HostStatus.UNHEALTHY,
query_strategy_version: MetadataQueryStrategyVersions.VERSION_2,
};
},
},

View file

@ -56,7 +56,6 @@ export const initialEndpointPageState = (): Immutable<EndpointState> => {
agentsWithEndpointsTotalError: undefined,
endpointsTotal: 0,
endpointsTotalError: undefined,
queryStrategyVersion: undefined,
policyVersionInfo: undefined,
hostStatus: undefined,
isolationRequestState: createUninitialisedResourceState(),

View file

@ -28,7 +28,6 @@ import {
nonExistingPolicies,
patterns,
searchBarQuery,
isTransformEnabled,
getIsIsolationRequestPending,
getCurrentIsolationRequestState,
getActivityLogData,
@ -180,7 +179,7 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory<EndpointState
}
// get index pattern and fields for search bar
if (patterns(getState()).length === 0 && isTransformEnabled(getState())) {
if (patterns(getState()).length === 0) {
try {
const indexPatterns = await fetchIndexPatterns();
if (indexPatterns !== undefined) {

View file

@ -12,7 +12,6 @@ import {
HostPolicyResponse,
HostResultList,
HostStatus,
MetadataQueryStrategyVersions,
PendingActionsResponse,
} from '../../../../../common/endpoint/types';
import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data';
@ -38,13 +37,11 @@ export const mockEndpointResultList: (options?: {
total?: number;
request_page_size?: number;
request_page_index?: number;
query_strategy_version?: MetadataQueryStrategyVersions;
}) => HostResultList = (options = {}) => {
const {
total = 1,
request_page_size: requestPageSize = 10,
request_page_index: requestPageIndex = 0,
query_strategy_version: queryStrategyVersion = MetadataQueryStrategyVersions.VERSION_2,
} = options;
// Skip any that are before the page we're on
@ -58,7 +55,6 @@ export const mockEndpointResultList: (options?: {
hosts.push({
metadata: generator.generateHostMetadata(),
host_status: HostStatus.UNHEALTHY,
query_strategy_version: queryStrategyVersion,
});
}
const mock: HostResultList = {
@ -66,7 +62,6 @@ export const mockEndpointResultList: (options?: {
total,
request_page_size: requestPageSize,
request_page_index: requestPageIndex,
query_strategy_version: queryStrategyVersion,
};
return mock;
};
@ -78,7 +73,6 @@ export const mockEndpointDetailsApiResult = (): HostInfo => {
return {
metadata: generator.generateHostMetadata(),
host_status: HostStatus.UNHEALTHY,
query_strategy_version: MetadataQueryStrategyVersions.VERSION_2,
};
};
@ -92,7 +86,6 @@ const endpointListApiPathHandlerMocks = ({
endpointPackagePolicies = [],
policyResponse = generator.generatePolicyResponse(),
agentPolicy = generator.generateAgentPolicy(),
queryStrategyVersion = MetadataQueryStrategyVersions.VERSION_2,
totalAgentsUsingEndpoint = 0,
}: {
/** route handlers will be setup for each individual host in this array */
@ -101,7 +94,6 @@ const endpointListApiPathHandlerMocks = ({
endpointPackagePolicies?: GetPolicyListResponse['items'];
policyResponse?: HostPolicyResponse;
agentPolicy?: GetAgentPoliciesResponseItem;
queryStrategyVersion?: MetadataQueryStrategyVersions;
totalAgentsUsingEndpoint?: number;
} = {}) => {
const apiHandlers = {
@ -119,7 +111,6 @@ const endpointListApiPathHandlerMocks = ({
request_page_size: 10,
request_page_index: 0,
total: endpointsResults?.length || 0,
query_strategy_version: queryStrategyVersion,
};
},
@ -192,16 +183,11 @@ export const setEndpointListApiMockImplementation: (
apiResponses?: Parameters<typeof endpointListApiPathHandlerMocks>[0]
) => void = (
mockedHttpService,
{
endpointsResults = mockEndpointResultList({ total: 3 }).hosts,
queryStrategyVersion = MetadataQueryStrategyVersions.VERSION_2,
...pathHandlersOptions
} = {}
{ endpointsResults = mockEndpointResultList({ total: 3 }).hosts, ...pathHandlersOptions } = {}
) => {
const apiHandlers = endpointListApiPathHandlerMocks({
...pathHandlersOptions,
endpointsResults,
queryStrategyVersion,
});
mockedHttpService.post

View file

@ -89,7 +89,6 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
total,
request_page_size: pageSize,
request_page_index: pageIndex,
query_strategy_version: queryStrategyVersion,
policy_info: policyVersionInfo,
} = action.payload;
return {
@ -98,7 +97,6 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
total,
pageSize,
pageIndex,
queryStrategyVersion,
policyVersionInfo,
loading: false,
error: undefined,

View file

@ -15,7 +15,6 @@ import {
HostPolicyResponseAppliedAction,
HostPolicyResponseConfiguration,
HostPolicyResponseActionStatus,
MetadataQueryStrategyVersions,
HostStatus,
ActivityLog,
HostMetadata,
@ -90,17 +89,11 @@ 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(endpointPackageInfo, (info) =>
isLoadedResourceState(info) ? info.data.version : undefined
);
export const isTransformEnabled = createSelector(
queryStrategyVersion,
(version) => version !== MetadataQueryStrategyVersions.VERSION_1
);
/**
* Returns the index patterns for the SearchBar to use for autosuggest
*/

View file

@ -13,7 +13,6 @@ import {
HostPolicyResponse,
AppLocation,
PolicyData,
MetadataQueryStrategyVersions,
HostStatus,
HostIsolationResponse,
EndpointPendingActions,
@ -96,8 +95,6 @@ export interface EndpointState {
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;
/** The policy IDs and revision number of the corresponding agent, and endpoint. May be more recent than what's running */
policyVersionInfo?: HostInfo['policy_info'];
/** The status of the host, which is mapped to the Elastic Agent status in Fleet */

View file

@ -23,7 +23,6 @@ import {
HostPolicyResponseActionStatus,
HostPolicyResponseAppliedAction,
HostStatus,
MetadataQueryStrategyVersions,
} from '../../../../../common/endpoint/types';
import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data';
import { POLICY_STATUS_TO_TEXT } from './host_constants';
@ -167,31 +166,6 @@ describe('when on the endpoint list page', () => {
});
});
describe('when loading data with the query_strategy_version is `v1`', () => {
beforeEach(() => {
reactTestingLibrary.act(() => {
const mockedEndpointListData = mockEndpointResultList({
total: 4,
query_strategy_version: MetadataQueryStrategyVersions.VERSION_1,
});
setEndpointListApiMockImplementation(coreStart.http, {
endpointsResults: mockedEndpointListData.hosts,
queryStrategyVersion: mockedEndpointListData.query_strategy_version,
});
});
});
afterEach(() => {
jest.clearAllMocks();
});
it('should not display the KQL bar', async () => {
const renderResult = render();
await reactTestingLibrary.act(async () => {
await middlewareSpy.waitForAction('serverReturnedEndpointList');
});
expect(renderResult.queryByTestId('adminSearchBar')).toBeNull();
});
});
describe('when determining when to show the enrolling message', () => {
afterEach(() => {
jest.clearAllMocks();
@ -268,7 +242,6 @@ describe('when on the endpoint list page', () => {
reactTestingLibrary.act(() => {
const mockedEndpointData = mockEndpointResultList({ total: 5 });
const hostListData = mockedEndpointData.hosts;
const queryStrategyVersion = mockedEndpointData.query_strategy_version;
firstPolicyID = hostListData[0].metadata.Endpoint.policy.applied.id;
firstPolicyRev = hostListData[0].metadata.Endpoint.policy.applied.endpoint_policy_version;
@ -329,7 +302,6 @@ describe('when on the endpoint list page', () => {
hostListData[index].metadata.Endpoint.policy.applied,
setup.policy
),
query_strategy_version: queryStrategyVersion,
};
});
hostListData.forEach((item, index) => {
@ -535,8 +507,6 @@ describe('when on the endpoint list page', () => {
// eslint-disable-next-line @typescript-eslint/naming-convention
host_status,
metadata: { agent, Endpoint, ...details },
// eslint-disable-next-line @typescript-eslint/naming-convention
query_strategy_version,
} = mockEndpointDetailsApiResult();
hostDetails = {
@ -555,7 +525,6 @@ describe('when on the endpoint list page', () => {
id: '1',
},
},
query_strategy_version,
};
const policy = docGenerator.generatePolicyPackagePolicy();
@ -1198,7 +1167,7 @@ describe('when on the endpoint list page', () => {
let renderResult: ReturnType<AppContextTestRender['render']>;
const mockEndpointListApi = () => {
const { hosts, query_strategy_version: queryStrategyVersion } = mockEndpointResultList();
const { hosts } = mockEndpointResultList();
hostInfo = {
host_status: hosts[0].host_status,
metadata: {
@ -1222,7 +1191,6 @@ describe('when on the endpoint list page', () => {
version: '7.14.0',
},
},
query_strategy_version: queryStrategyVersion,
};
const packagePolicy = docGenerator.generatePolicyPackagePolicy();

View file

@ -120,7 +120,6 @@ export const EndpointList = () => {
areEndpointsEnrolling,
agentsWithEndpointsTotalError,
endpointsTotalError,
isTransformEnabled,
} = useEndpointSelector(selector);
const { search } = useFormatUrl(SecurityPageName.administration);
const { getAppUrl } = useAppUrl();
@ -476,8 +475,8 @@ export const EndpointList = () => {
const hasListData = listData && listData.length > 0;
const refreshStyle = useMemo(() => {
return { display: endpointsExist && isTransformEnabled ? 'flex' : 'none', maxWidth: 200 };
}, [endpointsExist, isTransformEnabled]);
return { display: endpointsExist ? 'flex' : 'none', maxWidth: 200 };
}, [endpointsExist]);
const refreshIsPaused = useMemo(() => {
return !endpointsExist ? false : hasSelectedEndpoint ? true : !isAutoRefreshEnabled;
@ -492,8 +491,8 @@ export const EndpointList = () => {
}, [endpointsTotalError, agentsWithEndpointsTotalError]);
const shouldShowKQLBar = useMemo(() => {
return endpointsExist && !patternsError && isTransformEnabled;
}, [endpointsExist, patternsError, isTransformEnabled]);
return endpointsExist && !patternsError;
}, [endpointsExist, patternsError]);
return (
<AdministrationListPage

View file

@ -20,7 +20,6 @@ import { SecurityPluginStart } from '../../../security/server';
import {
AgentService,
FleetStartContract,
PackageService,
AgentPolicyServiceInterface,
PackagePolicyServiceInterface,
} from '../../../fleet/server';
@ -30,14 +29,6 @@ import {
getPackagePolicyUpdateCallback,
} from '../fleet_integration/fleet_integration';
import { ManifestManager } from './services/artifacts';
import { MetadataQueryStrategy } from './types';
import { MetadataQueryStrategyVersions } from '../../common/endpoint/types';
import {
metadataQueryStrategyV1,
metadataQueryStrategyV2,
} from './routes/metadata/support/query_strategies';
import { ElasticsearchAssetType } from '../../../fleet/common/types/models';
import { metadataTransformPrefix } from '../../common/endpoint/constants';
import { AppClientFactory } from '../client';
import { ConfigType } from '../config';
import { LicenseService } from '../../common/license';
@ -46,45 +37,6 @@ import {
parseExperimentalConfigValue,
} from '../../common/experimental_features';
export interface MetadataService {
queryStrategy(
savedObjectsClient: SavedObjectsClientContract,
version?: MetadataQueryStrategyVersions
): Promise<MetadataQueryStrategy>;
}
export const createMetadataService = (packageService: PackageService): MetadataService => {
return {
async queryStrategy(
savedObjectsClient: SavedObjectsClientContract,
version?: MetadataQueryStrategyVersions
): Promise<MetadataQueryStrategy> {
if (version === MetadataQueryStrategyVersions.VERSION_1) {
return metadataQueryStrategyV1();
}
if (!packageService) {
throw new Error('package service is uninitialized');
}
if (version === MetadataQueryStrategyVersions.VERSION_2 || !version) {
const assets =
(await packageService.getInstallation({ savedObjectsClient, pkgName: 'endpoint' }))
?.installed_es ?? [];
const expectedTransformAssets = assets.filter(
(ref) =>
ref.type === ElasticsearchAssetType.transform &&
ref.id.startsWith(metadataTransformPrefix)
);
if (expectedTransformAssets && expectedTransformAssets.length === 1) {
return metadataQueryStrategyV2();
}
return metadataQueryStrategyV1();
}
return metadataQueryStrategyV1();
},
};
};
export type EndpointAppContextServiceStartContract = Partial<
Pick<
FleetStartContract,
@ -114,7 +66,6 @@ export class EndpointAppContextService {
private packagePolicyService: PackagePolicyServiceInterface | undefined;
private agentPolicyService: AgentPolicyServiceInterface | undefined;
private savedObjectsStart: SavedObjectsServiceStart | undefined;
private metadataService: MetadataService | undefined;
private config: ConfigType | undefined;
private license: LicenseService | undefined;
public security: SecurityPluginStart | undefined;
@ -128,7 +79,6 @@ export class EndpointAppContextService {
this.agentPolicyService = dependencies.agentPolicyService;
this.manifestManager = dependencies.manifestManager;
this.savedObjectsStart = dependencies.savedObjectsStart;
this.metadataService = createMetadataService(dependencies.packageService!);
this.config = dependencies.config;
this.license = dependencies.licenseService;
this.security = dependencies.security;
@ -176,10 +126,6 @@ export class EndpointAppContextService {
return this.agentPolicyService;
}
public getMetadataService(): MetadataService | undefined {
return this.metadataService;
}
public getManifestManager(): ManifestManager | undefined {
return this.manifestManager;
}

View file

@ -83,7 +83,7 @@ export const isolationRequestHandler = function (
// fetch the Agent IDs to send the commands to
const endpointIDs = [...new Set(req.body.endpoint_ids)]; // dedupe
const endpointData = await getMetadataForEndpoints(endpointIDs, context, endpointContext);
const endpointData = await getMetadataForEndpoints(endpointIDs, context);
const casesClient = await endpointContext.service.getCasesClient(req);

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { HostStatus, MetadataQueryStrategyVersions } from '../../../../common/endpoint/types';
import { HostStatus } from '../../../../common/endpoint/types';
import { createMockMetadataRequestContext } from '../../mocks';
import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data';
import { enrichHostMetadata, MetadataRequestContext } from './handlers';
@ -18,30 +18,6 @@ describe('test document enrichment', () => {
metaReqCtx = createMockMetadataRequestContext();
});
// verify query version passed through
describe('metadata query strategy enrichment', () => {
it('should match v1 strategy when directed', async () => {
const enrichedHostList = await enrichHostMetadata(
docGen.generateHostMetadata(),
metaReqCtx,
MetadataQueryStrategyVersions.VERSION_1
);
expect(enrichedHostList.query_strategy_version).toEqual(
MetadataQueryStrategyVersions.VERSION_1
);
});
it('should match v2 strategy when directed', async () => {
const enrichedHostList = await enrichHostMetadata(
docGen.generateHostMetadata(),
metaReqCtx,
MetadataQueryStrategyVersions.VERSION_2
);
expect(enrichedHostList.query_strategy_version).toEqual(
MetadataQueryStrategyVersions.VERSION_2
);
});
});
describe('host status enrichment', () => {
let statusFn: jest.Mock;
@ -57,77 +33,49 @@ describe('test document enrichment', () => {
it('should return host healthy for online agent', async () => {
statusFn.mockImplementation(() => 'online');
const enrichedHostList = await enrichHostMetadata(
docGen.generateHostMetadata(),
metaReqCtx,
MetadataQueryStrategyVersions.VERSION_2
);
const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx);
expect(enrichedHostList.host_status).toEqual(HostStatus.HEALTHY);
});
it('should return host offline for offline agent', async () => {
statusFn.mockImplementation(() => 'offline');
const enrichedHostList = await enrichHostMetadata(
docGen.generateHostMetadata(),
metaReqCtx,
MetadataQueryStrategyVersions.VERSION_2
);
const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx);
expect(enrichedHostList.host_status).toEqual(HostStatus.OFFLINE);
});
it('should return host updating for unenrolling agent', async () => {
statusFn.mockImplementation(() => 'unenrolling');
const enrichedHostList = await enrichHostMetadata(
docGen.generateHostMetadata(),
metaReqCtx,
MetadataQueryStrategyVersions.VERSION_2
);
const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx);
expect(enrichedHostList.host_status).toEqual(HostStatus.UPDATING);
});
it('should return host unhealthy for degraded agent', async () => {
statusFn.mockImplementation(() => 'degraded');
const enrichedHostList = await enrichHostMetadata(
docGen.generateHostMetadata(),
metaReqCtx,
MetadataQueryStrategyVersions.VERSION_2
);
const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx);
expect(enrichedHostList.host_status).toEqual(HostStatus.UNHEALTHY);
});
it('should return host unhealthy for erroring agent', async () => {
statusFn.mockImplementation(() => 'error');
const enrichedHostList = await enrichHostMetadata(
docGen.generateHostMetadata(),
metaReqCtx,
MetadataQueryStrategyVersions.VERSION_2
);
const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx);
expect(enrichedHostList.host_status).toEqual(HostStatus.UNHEALTHY);
});
it('should return host unhealthy for warning agent', async () => {
statusFn.mockImplementation(() => 'warning');
const enrichedHostList = await enrichHostMetadata(
docGen.generateHostMetadata(),
metaReqCtx,
MetadataQueryStrategyVersions.VERSION_2
);
const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx);
expect(enrichedHostList.host_status).toEqual(HostStatus.UNHEALTHY);
});
it('should return host unhealthy for invalid agent', async () => {
statusFn.mockImplementation(() => 'asliduasofb');
const enrichedHostList = await enrichHostMetadata(
docGen.generateHostMetadata(),
metaReqCtx,
MetadataQueryStrategyVersions.VERSION_2
);
const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx);
expect(enrichedHostList.host_status).toEqual(HostStatus.UNHEALTHY);
});
});
@ -164,11 +112,7 @@ describe('test document enrichment', () => {
};
});
const enrichedHostList = await enrichHostMetadata(
docGen.generateHostMetadata(),
metaReqCtx,
MetadataQueryStrategyVersions.VERSION_2
);
const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx);
expect(enrichedHostList.policy_info).toBeDefined();
expect(enrichedHostList.policy_info!.agent.applied.id).toEqual(policyID);
expect(enrichedHostList.policy_info!.agent.applied.revision).toEqual(policyRev);
@ -184,11 +128,7 @@ describe('test document enrichment', () => {
};
});
const enrichedHostList = await enrichHostMetadata(
docGen.generateHostMetadata(),
metaReqCtx,
MetadataQueryStrategyVersions.VERSION_2
);
const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx);
expect(enrichedHostList.policy_info).toBeDefined();
expect(enrichedHostList.policy_info!.agent.configured.id).toEqual(policyID);
expect(enrichedHostList.policy_info!.agent.configured.revision).toEqual(policyRev);
@ -209,11 +149,7 @@ describe('test document enrichment', () => {
};
});
const enrichedHostList = await enrichHostMetadata(
docGen.generateHostMetadata(),
metaReqCtx,
MetadataQueryStrategyVersions.VERSION_2
);
const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx);
expect(enrichedHostList.policy_info).toBeDefined();
expect(enrichedHostList.policy_info!.endpoint.id).toEqual(policyID);
expect(enrichedHostList.policy_info!.endpoint.revision).toEqual(policyRev);

View file

@ -17,10 +17,8 @@ import {
import {
HostInfo,
HostMetadata,
HostMetadataInfo,
HostResultList,
HostStatus,
MetadataQueryStrategyVersions,
} from '../../../../common/endpoint/types';
import type { SecuritySolutionRequestHandlerContext } from '../../../types';
@ -33,6 +31,10 @@ import { findAllUnenrolledAgentIds } from './support/unenroll';
import { findAgentIDsByStatus } from './support/agent_status';
import { EndpointAppContextService } from '../../endpoint_app_context_services';
import { fleetAgentStatusToEndpointHostStatus } from '../../utils';
import {
queryResponseToHostListResult,
queryResponseToHostResult,
} from './support/query_strategies';
export interface MetadataRequestContext {
esClient?: IScopedClusterClient;
@ -58,8 +60,7 @@ export const getLogger = (endpointAppContext: EndpointAppContext): Logger => {
export const getMetadataListRequestHandler = function (
endpointAppContext: EndpointAppContext,
logger: Logger,
queryStrategyVersion?: MetadataQueryStrategyVersions
logger: Logger
): RequestHandler<
unknown,
unknown,
@ -96,24 +97,15 @@ export const getMetadataListRequestHandler = function (
)
: undefined;
const queryStrategy = await endpointAppContext.service
?.getMetadataService()
?.queryStrategy(context.core.savedObjects.client, queryStrategyVersion);
const queryParams = await kibanaRequestToMetadataListESQuery(
request,
endpointAppContext,
queryStrategy!,
{
unenrolledAgentIds: unenrolledAgentIds.concat(IGNORED_ELASTIC_AGENT_IDS),
statusAgentIDs: statusIDs,
}
);
const queryParams = await kibanaRequestToMetadataListESQuery(request, endpointAppContext, {
unenrolledAgentIds: unenrolledAgentIds.concat(IGNORED_ELASTIC_AGENT_IDS),
statusAgentIDs: statusIDs,
});
const result = await context.core.elasticsearch.client.asCurrentUser.search<HostMetadata>(
queryParams
);
const hostListQueryResult = queryStrategy!.queryResponseToHostListResult(result.body);
const hostListQueryResult = queryResponseToHostListResult(result.body);
return response.ok({
body: await mapToHostResultList(queryParams, hostListQueryResult, metadataRequestContext),
});
@ -122,8 +114,7 @@ export const getMetadataListRequestHandler = function (
export const getMetadataRequestHandler = function (
endpointAppContext: EndpointAppContext,
logger: Logger,
queryStrategyVersion?: MetadataQueryStrategyVersions
logger: Logger
): RequestHandler<
TypeOf<typeof GetMetadataRequestSchema.params>,
unknown,
@ -145,11 +136,7 @@ export const getMetadataRequestHandler = function (
};
try {
const doc = await getHostData(
metadataRequestContext,
request?.params?.id,
queryStrategyVersion
);
const doc = await getHostData(metadataRequestContext, request?.params?.id);
if (doc) {
return response.ok({ body: doc });
}
@ -169,9 +156,8 @@ export const getMetadataRequestHandler = function (
export async function getHostMetaData(
metadataRequestContext: MetadataRequestContext,
id: string,
queryStrategyVersion?: MetadataQueryStrategyVersions
): Promise<HostMetadataInfo | undefined> {
id: string
): Promise<HostMetadata | undefined> {
if (
!metadataRequestContext.esClient &&
!metadataRequestContext.requestHandlerContext?.core.elasticsearch.client
@ -190,32 +176,23 @@ export async function getHostMetaData(
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(esSavedObjectClient, queryStrategyVersion);
const query = getESQueryHostMetadataByID(id, queryStrategy!);
const query = getESQueryHostMetadataByID(id);
const response = await esClient.asCurrentUser.search<HostMetadata>(query);
const hostResult = queryStrategy!.queryResponseToHostResult(response.body);
const hostResult = queryResponseToHostResult(response.body);
const hostMetadata = hostResult.result;
if (!hostMetadata) {
return undefined;
}
return { metadata: hostMetadata, query_strategy_version: hostResult.queryStrategyVersion };
return hostMetadata;
}
export async function getHostData(
metadataRequestContext: MetadataRequestContext,
id: string,
queryStrategyVersion?: MetadataQueryStrategyVersions
id: string
): Promise<HostInfo | undefined> {
if (!metadataRequestContext.savedObjectsClient) {
throw Boom.badRequest('savedObjectsClient not found');
@ -228,25 +205,21 @@ export async function getHostData(
throw Boom.badRequest('esClient not found');
}
const hostResult = await getHostMetaData(metadataRequestContext, id, queryStrategyVersion);
const hostMetadata = await getHostMetaData(metadataRequestContext, id);
if (!hostResult) {
if (!hostMetadata) {
return undefined;
}
const agent = await findAgent(metadataRequestContext, hostResult.metadata);
const agent = await findAgent(metadataRequestContext, hostMetadata);
if (agent && !agent.active) {
throw Boom.badRequest('the requested endpoint is unenrolled');
}
const metadata = await enrichHostMetadata(
hostResult.metadata,
metadataRequestContext,
hostResult.query_strategy_version
);
const metadata = await enrichHostMetadata(hostMetadata, metadataRequestContext);
return { ...metadata, query_strategy_version: hostResult.query_strategy_version };
return metadata;
}
async function findAgent(
@ -293,15 +266,10 @@ export async function mapToHostResultList(
request_page_index: queryParams.from,
hosts: await Promise.all(
hostListQueryResult.resultList.map(async (entry) =>
enrichHostMetadata(
entry,
metadataRequestContext,
hostListQueryResult.queryStrategyVersion
)
enrichHostMetadata(entry, metadataRequestContext)
)
),
total: totalNumberOfHosts,
query_strategy_version: hostListQueryResult.queryStrategyVersion,
};
} else {
return {
@ -309,15 +277,13 @@ export async function mapToHostResultList(
request_page_index: queryParams.from,
total: totalNumberOfHosts,
hosts: [],
query_strategy_version: hostListQueryResult.queryStrategyVersion,
};
}
}
export async function enrichHostMetadata(
hostMetadata: HostMetadata,
metadataRequestContext: MetadataRequestContext,
metadataQueryStrategyVersion: MetadataQueryStrategyVersions
metadataRequestContext: MetadataRequestContext
): Promise<HostInfo> {
let hostStatus = HostStatus.UNHEALTHY;
let elasticAgentId = hostMetadata?.elastic?.agent?.id;
@ -413,6 +379,5 @@ export async function enrichHostMetadata(
metadata: hostMetadata,
host_status: hostStatus,
policy_info: policyInfo,
query_strategy_version: metadataQueryStrategyVersion,
};
}

View file

@ -7,19 +7,15 @@
import { schema } from '@kbn/config-schema';
import { HostStatus, MetadataQueryStrategyVersions } from '../../../../common/endpoint/types';
import { HostStatus } from '../../../../common/endpoint/types';
import { EndpointAppContext } from '../../types';
import { getLogger, getMetadataListRequestHandler, getMetadataRequestHandler } from './handlers';
import type { SecuritySolutionPluginRouter } from '../../../types';
import {
BASE_ENDPOINT_ROUTE,
HOST_METADATA_GET_ROUTE,
HOST_METADATA_LIST_ROUTE,
} from '../../../../common/endpoint/constants';
export const METADATA_REQUEST_V1_ROUTE = `${BASE_ENDPOINT_ROUTE}/v1/metadata`;
export const GET_METADATA_REQUEST_V1_ROUTE = `${METADATA_REQUEST_V1_ROUTE}/{id}`;
/* Filters that can be applied to the endpoint fetch route */
export const endpointFilters = schema.object({
kql: schema.nullable(schema.string()),
@ -69,18 +65,6 @@ export function registerEndpointRoutes(
endpointAppContext: EndpointAppContext
) {
const logger = getLogger(endpointAppContext);
router.post(
{
path: `${METADATA_REQUEST_V1_ROUTE}`,
validate: GetMetadataListRequestSchema,
options: { authRequired: true, tags: ['access:securitySolution'] },
},
getMetadataListRequestHandler(
endpointAppContext,
logger,
MetadataQueryStrategyVersions.VERSION_1
)
);
router.post(
{
@ -91,15 +75,6 @@ export function registerEndpointRoutes(
getMetadataListRequestHandler(endpointAppContext, logger)
);
router.get(
{
path: `${GET_METADATA_REQUEST_V1_ROUTE}`,
validate: GetMetadataRequestSchema,
options: { authRequired: true, tags: ['access:securitySolution'] },
},
getMetadataRequestHandler(endpointAppContext, logger, MetadataQueryStrategyVersions.VERSION_1)
);
router.get(
{
path: `${HOST_METADATA_GET_ROUTE}`,

View file

@ -19,12 +19,7 @@ import {
loggingSystemMock,
savedObjectsClientMock,
} from '../../../../../../../src/core/server/mocks';
import {
HostInfo,
HostResultList,
HostStatus,
MetadataQueryStrategyVersions,
} from '../../../../common/endpoint/types';
import { HostInfo, HostResultList, HostStatus } from '../../../../common/endpoint/types';
import { parseExperimentalConfigValue } from '../../../../common/experimental_features';
import { registerEndpointRoutes } from './index';
import {
@ -39,7 +34,7 @@ import {
import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__';
import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data';
import { Agent, ElasticsearchAssetType } from '../../../../../fleet/common/types/models';
import { createV1SearchResponse, createV2SearchResponse } from './support/test_support';
import { createV2SearchResponse } from './support/test_support';
import { PackageService } from '../../../../../fleet/server/services';
import {
HOST_METADATA_LIST_ROUTE,
@ -98,94 +93,6 @@ describe('test endpoint route', () => {
);
});
describe('with no transform package', () => {
beforeEach(() => {
endpointAppContextService = new EndpointAppContextService();
mockPackageService = createMockPackageService();
mockPackageService.getInstallation.mockReturnValue(Promise.resolve(undefined));
endpointAppContextService.start({ ...startContract, packageService: mockPackageService });
mockAgentService = startContract.agentService!;
registerEndpointRoutes(routerMock, {
logFactory: loggingSystemMock.create(),
service: endpointAppContextService,
config: () => Promise.resolve(createMockConfig()),
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
});
});
afterEach(() => endpointAppContextService.stop());
it('test find the latest of all endpoints', async () => {
const mockRequest = httpServerMock.createKibanaRequest({});
const response = createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata());
(mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({ body: response })
);
[routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) =>
path.startsWith(`${HOST_METADATA_LIST_ROUTE}`)
)!;
mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error');
mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent);
await routeHandler(
createRouteHandlerContext(mockScopedClient, mockSavedObjectClient),
mockRequest,
mockResponse
);
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;
expect(endpointResultList.hosts.length).toEqual(1);
expect(endpointResultList.total).toEqual(1);
expect(endpointResultList.request_page_index).toEqual(0);
expect(endpointResultList.request_page_size).toEqual(10);
expect(endpointResultList.query_strategy_version).toEqual(
MetadataQueryStrategyVersions.VERSION_1
);
});
it('should return a single endpoint with status healthy', async () => {
const response = createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata());
const mockRequest = httpServerMock.createKibanaRequest({
params: { id: response.hits.hits[0]._id },
});
mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('online');
mockAgentService.getAgent = jest.fn().mockReturnValue(({
active: true,
} as unknown) as Agent);
(mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({ body: response })
);
[routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
path.startsWith(`${HOST_METADATA_LIST_ROUTE}`)
)!;
await routeHandler(
createRouteHandlerContext(mockScopedClient, mockSavedObjectClient),
mockRequest,
mockResponse
);
expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1);
expect(routeConfig.options).toEqual({
authRequired: true,
tags: ['access:securitySolution'],
});
expect(mockResponse.ok).toBeCalled();
const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo;
expect(result).toHaveProperty('metadata.Endpoint');
expect(result.host_status).toEqual(HostStatus.HEALTHY);
expect(result.query_strategy_version).toEqual(MetadataQueryStrategyVersions.VERSION_1);
});
});
describe('with new transform package', () => {
beforeEach(() => {
endpointAppContextService = new EndpointAppContextService();
@ -254,9 +161,6 @@ describe('test endpoint route', () => {
expect(endpointResultList.total).toEqual(1);
expect(endpointResultList.request_page_index).toEqual(0);
expect(endpointResultList.request_page_size).toEqual(10);
expect(endpointResultList.query_strategy_version).toEqual(
MetadataQueryStrategyVersions.VERSION_2
);
});
it('test find the latest of all endpoints with paging properties', async () => {
@ -311,9 +215,6 @@ describe('test endpoint route', () => {
expect(endpointResultList.total).toEqual(1);
expect(endpointResultList.request_page_index).toEqual(10);
expect(endpointResultList.request_page_size).toEqual(10);
expect(endpointResultList.query_strategy_version).toEqual(
MetadataQueryStrategyVersions.VERSION_2
);
});
it('test find the latest of all endpoints with paging and filters properties', async () => {
@ -405,9 +306,6 @@ describe('test endpoint route', () => {
expect(endpointResultList.total).toEqual(1);
expect(endpointResultList.request_page_index).toEqual(10);
expect(endpointResultList.request_page_size).toEqual(10);
expect(endpointResultList.query_strategy_version).toEqual(
MetadataQueryStrategyVersions.VERSION_2
);
});
describe('Endpoint Details route', () => {
@ -475,7 +373,6 @@ describe('test endpoint route', () => {
const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo;
expect(result).toHaveProperty('metadata.Endpoint');
expect(result.host_status).toEqual(HostStatus.HEALTHY);
expect(result.query_strategy_version).toEqual(MetadataQueryStrategyVersions.VERSION_2);
});
it('should return a single endpoint with status unhealthy when AgentService throw 404', async () => {

View file

@ -1,456 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
KibanaResponseFactory,
RequestHandler,
RouteConfig,
SavedObjectsClientContract,
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,
httpServiceMock,
loggingSystemMock,
savedObjectsClientMock,
} from '../../../../../../../src/core/server/mocks';
import {
HostInfo,
HostResultList,
HostStatus,
MetadataQueryStrategyVersions,
} from '../../../../common/endpoint/types';
import { registerEndpointRoutes, METADATA_REQUEST_V1_ROUTE } from './index';
import {
createMockEndpointAppContextServiceStartContract,
createMockPackageService,
createRouteHandlerContext,
} from '../../mocks';
import {
EndpointAppContextService,
EndpointAppContextServiceStartContract,
} from '../../endpoint_app_context_services';
import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__';
import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data';
import { parseExperimentalConfigValue } from '../../../../common/experimental_features';
import { Agent } from '../../../../../fleet/common/types/models';
import { createV1SearchResponse } from './support/test_support';
import { PackageService } from '../../../../../fleet/server/services';
import type { SecuritySolutionPluginRouter } from '../../../types';
import { PackagePolicyServiceInterface } from '../../../../../fleet/server';
describe('test endpoint route v1', () => {
let routerMock: jest.Mocked<SecuritySolutionPluginRouter>;
let mockResponse: jest.Mocked<KibanaResponseFactory>;
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
let routeHandler: RequestHandler<any, any, any, any>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let routeConfig: RouteConfig<any, any, any, any>;
// tests assume that fleet is enabled, and thus agentService is available
let mockAgentService: Required<
ReturnType<typeof createMockEndpointAppContextServiceStartContract>
>['agentService'];
let endpointAppContextService: EndpointAppContextService;
let startContract: EndpointAppContextServiceStartContract;
const noUnenrolledAgent = {
agents: [],
total: 0,
page: 1,
perPage: 1,
};
beforeEach(() => {
mockClusterClient = elasticsearchServiceMock.createClusterClient();
mockScopedClient = elasticsearchServiceMock.createScopedClusterClient();
mockSavedObjectClient = savedObjectsClientMock.create();
mockClusterClient.asScoped.mockReturnValue(mockScopedClient);
routerMock = httpServiceMock.createRouter();
mockResponse = httpServerMock.createResponseFactory();
endpointAppContextService = new EndpointAppContextService();
mockPackageService = createMockPackageService();
mockPackageService.getInstallation.mockReturnValue(Promise.resolve(undefined));
startContract = createMockEndpointAppContextServiceStartContract();
endpointAppContextService.start({ ...startContract, packageService: mockPackageService });
mockAgentService = startContract.agentService!;
(startContract.packagePolicyService as jest.Mocked<PackagePolicyServiceInterface>).list.mockImplementation(
() => {
return Promise.resolve({
items: [],
total: 0,
page: 1,
perPage: 1000,
});
}
);
registerEndpointRoutes(routerMock, {
logFactory: loggingSystemMock.create(),
service: endpointAppContextService,
config: () => Promise.resolve(createMockConfig()),
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
});
});
afterEach(() => endpointAppContextService.stop());
it('test find the latest of all endpoints', async () => {
const mockRequest = httpServerMock.createKibanaRequest({});
const response = createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata());
(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}`)
)!;
mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error');
mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent);
await routeHandler(
createRouteHandlerContext(mockScopedClient, mockSavedObjectClient),
mockRequest,
mockResponse
);
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;
expect(endpointResultList.hosts.length).toEqual(1);
expect(endpointResultList.total).toEqual(1);
expect(endpointResultList.request_page_index).toEqual(0);
expect(endpointResultList.request_page_size).toEqual(10);
expect(endpointResultList.query_strategy_version).toEqual(
MetadataQueryStrategyVersions.VERSION_1
);
});
it('test find the latest of all endpoints with paging properties', async () => {
const mockRequest = httpServerMock.createKibanaRequest({
body: {
paging_properties: [
{
page_size: 10,
},
{
page_index: 1,
},
],
},
});
mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error');
mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent);
(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}`)
)!;
await routeHandler(
createRouteHandlerContext(mockScopedClient, mockSavedObjectClient),
mockRequest,
mockResponse
);
expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1);
expect(
(mockScopedClient.asCurrentUser.search as jest.Mock).mock.calls[0][0]?.body?.query.bool
.must_not
).toContainEqual({
terms: {
'elastic.agent.id': [
'00000000-0000-0000-0000-000000000000',
'11111111-1111-1111-1111-111111111111',
],
},
});
expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'] });
expect(mockResponse.ok).toBeCalled();
const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as HostResultList;
expect(endpointResultList.hosts.length).toEqual(1);
expect(endpointResultList.total).toEqual(1);
expect(endpointResultList.request_page_index).toEqual(10);
expect(endpointResultList.request_page_size).toEqual(10);
expect(endpointResultList.query_strategy_version).toEqual(
MetadataQueryStrategyVersions.VERSION_1
);
});
it('test find the latest of all endpoints with paging and filters properties', async () => {
const mockRequest = httpServerMock.createKibanaRequest({
body: {
paging_properties: [
{
page_size: 10,
},
{
page_index: 1,
},
],
filters: { kql: 'not host.ip:10.140.73.246' },
},
});
mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error');
mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent);
(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}`)
)!;
await routeHandler(
createRouteHandlerContext(mockScopedClient, mockSavedObjectClient),
mockRequest,
mockResponse
);
expect(mockScopedClient.asCurrentUser.search).toBeCalled();
// needs to have the KQL filter passed through
expect(
(mockScopedClient.asCurrentUser.search as jest.Mock).mock.calls[0][0]?.body?.query.bool.must
).toContainEqual({
bool: {
must_not: {
bool: {
should: [
{
match: {
'host.ip': '10.140.73.246',
},
},
],
minimum_should_match: 1,
},
},
},
});
// and unenrolled should be filtered out.
expect(
(mockScopedClient.asCurrentUser.search as jest.Mock).mock.calls[0][0]?.body?.query.bool.must
).toContainEqual({
bool: {
must_not: [
{
terms: {
'elastic.agent.id': [
'00000000-0000-0000-0000-000000000000',
'11111111-1111-1111-1111-111111111111',
],
},
},
{
terms: {
// we actually don't care about HostDetails in v1 queries, but
// harder to set up the expectation to ignore its inclusion succinctly
'HostDetails.elastic.agent.id': [
'00000000-0000-0000-0000-000000000000',
'11111111-1111-1111-1111-111111111111',
],
},
},
],
},
});
expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'] });
expect(mockResponse.ok).toBeCalled();
const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as HostResultList;
expect(endpointResultList.hosts.length).toEqual(1);
expect(endpointResultList.total).toEqual(1);
expect(endpointResultList.request_page_index).toEqual(10);
expect(endpointResultList.request_page_size).toEqual(10);
expect(endpointResultList.query_strategy_version).toEqual(
MetadataQueryStrategyVersions.VERSION_1
);
});
describe('Endpoint Details route', () => {
it('should return 404 on no results', async () => {
const mockRequest = httpServerMock.createKibanaRequest({ params: { id: 'BADID' } });
(mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({ body: createV1SearchResponse() })
);
mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error');
mockAgentService.getAgent = jest.fn().mockReturnValue(({
active: true,
} as unknown) as Agent);
[routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`)
)!;
await routeHandler(
createRouteHandlerContext(mockScopedClient, mockSavedObjectClient),
mockRequest,
mockResponse
);
expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1);
expect(routeConfig.options).toEqual({
authRequired: true,
tags: ['access:securitySolution'],
});
expect(mockResponse.notFound).toBeCalled();
const message = mockResponse.notFound.mock.calls[0][0]?.body;
expect(message).toEqual('Endpoint Not Found');
});
it('should return a single endpoint with status healthy', async () => {
const response = createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata());
const mockRequest = httpServerMock.createKibanaRequest({
params: { id: response.hits.hits[0]._id },
});
mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('online');
mockAgentService.getAgent = jest.fn().mockReturnValue(({
active: true,
} as unknown) as Agent);
(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}`)
)!;
await routeHandler(
createRouteHandlerContext(mockScopedClient, mockSavedObjectClient),
mockRequest,
mockResponse
);
expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1);
expect(routeConfig.options).toEqual({
authRequired: true,
tags: ['access:securitySolution'],
});
expect(mockResponse.ok).toBeCalled();
const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo;
expect(result).toHaveProperty('metadata.Endpoint');
expect(result.host_status).toEqual(HostStatus.HEALTHY);
});
it('should return a single endpoint with status unhealthy when AgentService throw 404', async () => {
const response = createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata());
const mockRequest = httpServerMock.createKibanaRequest({
params: { id: response.hits.hits[0]._id },
});
mockAgentService.getAgentStatusById = jest.fn().mockImplementation(() => {
SavedObjectsErrorHelpers.createGenericNotFoundError();
});
mockAgentService.getAgent = jest.fn().mockImplementation(() => {
SavedObjectsErrorHelpers.createGenericNotFoundError();
});
(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}`)
)!;
await routeHandler(
createRouteHandlerContext(mockScopedClient, mockSavedObjectClient),
mockRequest,
mockResponse
);
expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1);
expect(routeConfig.options).toEqual({
authRequired: true,
tags: ['access:securitySolution'],
});
expect(mockResponse.ok).toBeCalled();
const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo;
expect(result.host_status).toEqual(HostStatus.UNHEALTHY);
});
it('should return a single endpoint with status unhealthy when status is not offline, online or enrolling', async () => {
const response = createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata());
const mockRequest = httpServerMock.createKibanaRequest({
params: { id: response.hits.hits[0]._id },
});
mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('warning');
mockAgentService.getAgent = jest.fn().mockReturnValue(({
active: true,
} as unknown) as Agent);
(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}`)
)!;
await routeHandler(
createRouteHandlerContext(mockScopedClient, mockSavedObjectClient),
mockRequest,
mockResponse
);
expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1);
expect(routeConfig.options).toEqual({
authRequired: true,
tags: ['access:securitySolution'],
});
expect(mockResponse.ok).toBeCalled();
const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo;
expect(result.host_status).toEqual(HostStatus.UNHEALTHY);
});
it('should throw error when endpoint agent is not active', async () => {
const response = createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata());
const mockRequest = httpServerMock.createKibanaRequest({
params: { id: response.hits.hits[0]._id },
});
(mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({ body: response })
);
mockAgentService.getAgent = jest.fn().mockReturnValue(({
active: false,
} as unknown) as Agent);
[routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`)
)!;
await routeHandler(
createRouteHandlerContext(mockScopedClient, mockSavedObjectClient),
mockRequest,
mockResponse
);
expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1);
expect(mockResponse.customError).toBeCalled();
});
});
});

View file

@ -11,38 +11,29 @@ import { EndpointAppContextService } from '../../endpoint_app_context_services';
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', () => {
it('queries the correct index', async () => {
const mockRequest = httpServerMock.createKibanaRequest({ body: {} });
const query = await kibanaRequestToMetadataListESQuery(
mockRequest,
{
logFactory: loggingSystemMock.create(),
service: new EndpointAppContextService(),
config: () => Promise.resolve(createMockConfig()),
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
},
metadataQueryStrategyV2()
);
const query = await kibanaRequestToMetadataListESQuery(mockRequest, {
logFactory: loggingSystemMock.create(),
service: new EndpointAppContextService(),
config: () => Promise.resolve(createMockConfig()),
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
});
expect(query.index).toEqual(metadataCurrentIndexPattern);
});
it('sorts using *event.created', async () => {
const mockRequest = httpServerMock.createKibanaRequest({ body: {} });
const query = await kibanaRequestToMetadataListESQuery(
mockRequest,
{
logFactory: loggingSystemMock.create(),
service: new EndpointAppContextService(),
config: () => Promise.resolve(createMockConfig()),
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
},
metadataQueryStrategyV2()
);
const query = await kibanaRequestToMetadataListESQuery(mockRequest, {
logFactory: loggingSystemMock.create(),
service: new EndpointAppContextService(),
config: () => Promise.resolve(createMockConfig()),
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
});
expect(query.body.sort).toContainEqual({
'event.created': {
order: 'desc',
@ -61,16 +52,12 @@ describe('query builder', () => {
const mockRequest = httpServerMock.createKibanaRequest({
body: {},
});
const query = await kibanaRequestToMetadataListESQuery(
mockRequest,
{
logFactory: loggingSystemMock.create(),
service: new EndpointAppContextService(),
config: () => Promise.resolve(createMockConfig()),
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
},
metadataQueryStrategyV2()
);
const query = await kibanaRequestToMetadataListESQuery(mockRequest, {
logFactory: loggingSystemMock.create(),
service: new EndpointAppContextService(),
config: () => Promise.resolve(createMockConfig()),
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
});
expect(query.body.query).toHaveProperty('match_all');
});
@ -87,7 +74,6 @@ describe('query builder', () => {
config: () => Promise.resolve(createMockConfig()),
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
},
metadataQueryStrategyV2(),
{
unenrolledAgentIds: [unenrolledElasticAgentId],
}
@ -111,16 +97,12 @@ describe('query builder', () => {
filters: { kql: 'not host.ip:10.140.73.246' },
},
});
const query = await kibanaRequestToMetadataListESQuery(
mockRequest,
{
logFactory: loggingSystemMock.create(),
service: new EndpointAppContextService(),
config: () => Promise.resolve(createMockConfig()),
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
},
metadataQueryStrategyV2()
);
const query = await kibanaRequestToMetadataListESQuery(mockRequest, {
logFactory: loggingSystemMock.create(),
service: new EndpointAppContextService(),
config: () => Promise.resolve(createMockConfig()),
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
});
expect(query.body.query.bool.must).toContainEqual({
bool: {
@ -160,7 +142,6 @@ describe('query builder', () => {
createMockConfig().enableExperimental
),
},
metadataQueryStrategyV2(),
{
unenrolledAgentIds: [unenrolledElasticAgentId],
}
@ -197,13 +178,13 @@ describe('query builder', () => {
describe('MetadataGetQuery', () => {
it('searches the correct index', () => {
const query = getESQueryHostMetadataByID('nonsense-id', metadataQueryStrategyV2());
const query = getESQueryHostMetadataByID('nonsense-id');
expect(query.index).toEqual(metadataCurrentIndexPattern);
});
it('searches for the correct ID', () => {
const mockID = 'AABBCCDD-0011-2233-AA44-DEADBEEF8899';
const query = getESQueryHostMetadataByID(mockID, metadataQueryStrategyV2());
const query = getESQueryHostMetadataByID(mockID);
expect(get(query, 'body.query.bool.filter.0.bool.should')).toContainEqual({
term: { 'agent.id': mockID },
@ -212,7 +193,7 @@ describe('query builder', () => {
it('supports HostDetails in schema for backwards compat', () => {
const mockID = 'AABBCCDD-0011-2233-AA44-DEADBEEF8899';
const query = getESQueryHostMetadataByID(mockID, metadataQueryStrategyV2());
const query = getESQueryHostMetadataByID(mockID);
expect(get(query, 'body.query.bool.filter.0.bool.should')).toContainEqual({
term: { 'HostDetails.agent.id': mockID },

View file

@ -6,9 +6,10 @@
*/
import type { estypes } from '@elastic/elasticsearch';
import { metadataCurrentIndexPattern } from '../../../../common/endpoint/constants';
import { KibanaRequest } from '../../../../../../../src/core/server';
import { esKuery } from '../../../../../../../src/plugins/data/server';
import { EndpointAppContext, MetadataQueryStrategy } from '../../types';
import { EndpointAppContext } from '../../types';
export interface QueryBuilderOptions {
unenrolledAgentIds?: string[];
@ -39,7 +40,6 @@ export async function kibanaRequestToMetadataListESQuery(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
request: KibanaRequest<any, any, any>,
endpointAppContext: EndpointAppContext,
metadataQueryStrategy: MetadataQueryStrategy,
queryBuilderOptions?: QueryBuilderOptions
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): Promise<Record<string, any>> {
@ -49,16 +49,15 @@ export async function kibanaRequestToMetadataListESQuery(
body: {
query: buildQueryBody(
request,
metadataQueryStrategy,
queryBuilderOptions?.unenrolledAgentIds!,
queryBuilderOptions?.statusAgentIDs!
),
...metadataQueryStrategy.extraBodyProperties,
track_total_hits: true,
sort: MetadataSortMethod,
},
from: pagingProperties.pageIndex * pagingProperties.pageSize,
size: pagingProperties.pageSize,
index: metadataQueryStrategy.index,
index: metadataCurrentIndexPattern,
};
}
@ -86,7 +85,6 @@ async function getPagingProperties(
function buildQueryBody(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
request: KibanaRequest<any, any, any>,
metadataQueryStrategy: MetadataQueryStrategy,
unerolledAgentIds: string[] | undefined,
statusAgentIDs: string[] | undefined
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -144,10 +142,7 @@ function buildQueryBody(
};
}
export function getESQueryHostMetadataByID(
agentID: string,
metadataQueryStrategy: MetadataQueryStrategy
): estypes.SearchRequest {
export function getESQueryHostMetadataByID(agentID: string): estypes.SearchRequest {
return {
body: {
query: {
@ -167,14 +162,11 @@ export function getESQueryHostMetadataByID(
sort: MetadataSortMethod,
size: 1,
},
index: metadataQueryStrategy.index,
index: metadataCurrentIndexPattern,
};
}
export function getESQueryHostMetadataByIDs(
agentIDs: string[],
metadataQueryStrategy: MetadataQueryStrategy
) {
export function getESQueryHostMetadataByIDs(agentIDs: string[]) {
return {
body: {
query: {
@ -193,6 +185,6 @@ export function getESQueryHostMetadataByIDs(
},
sort: MetadataSortMethod,
},
index: metadataQueryStrategy.index,
index: metadataCurrentIndexPattern,
};
}

View file

@ -1,188 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { httpServerMock, loggingSystemMock } from '../../../../../../../src/core/server/mocks';
import { kibanaRequestToMetadataListESQuery, getESQueryHostMetadataByID } from './query_builders';
import { EndpointAppContextService } from '../../endpoint_app_context_services';
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', () => {
it('test default query params for all endpoints metadata when no params or body is provided', async () => {
const mockRequest = httpServerMock.createKibanaRequest({
body: {},
});
const query = await kibanaRequestToMetadataListESQuery(
mockRequest,
{
logFactory: loggingSystemMock.create(),
service: new EndpointAppContextService(),
config: () => Promise.resolve(createMockConfig()),
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
},
metadataQueryStrategyV1()
);
expect(query.body.query).toHaveProperty('match_all'); // no filtering
expect(query.body.collapse).toEqual({
field: 'agent.id',
inner_hits: {
name: 'most_recent',
size: 1,
sort: [{ 'event.created': 'desc' }],
},
});
expect(query.body.aggs).toEqual({
total: {
cardinality: {
field: 'agent.id',
},
},
});
expect(query.index).toEqual(metadataIndexPattern);
});
it(
'test default query params for all endpoints metadata when no params or body is provided ' +
'with unenrolled host ids excluded',
async () => {
const unenrolledElasticAgentId = '1fdca33f-799f-49f4-939c-ea4383c77672';
const mockRequest = httpServerMock.createKibanaRequest({
body: {},
});
const query = await kibanaRequestToMetadataListESQuery(
mockRequest,
{
logFactory: loggingSystemMock.create(),
service: new EndpointAppContextService(),
config: () => Promise.resolve(createMockConfig()),
experimentalFeatures: parseExperimentalConfigValue(
createMockConfig().enableExperimental
),
},
metadataQueryStrategyV1(),
{
unenrolledAgentIds: [unenrolledElasticAgentId],
}
);
expect(Object.keys(query.body.query.bool)).toEqual(['must_not']); // only filtering out unenrolled
expect(query.body.query.bool.must_not).toContainEqual({
terms: { 'elastic.agent.id': [unenrolledElasticAgentId] },
});
}
);
});
describe('test query builder with kql filter', () => {
it('test default query params for all endpoints metadata when body filter is provided', async () => {
const mockRequest = httpServerMock.createKibanaRequest({
body: {
filters: { kql: 'not host.ip:10.140.73.246' },
},
});
const query = await kibanaRequestToMetadataListESQuery(
mockRequest,
{
logFactory: loggingSystemMock.create(),
service: new EndpointAppContextService(),
config: () => Promise.resolve(createMockConfig()),
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
},
metadataQueryStrategyV1()
);
expect(query.body.query.bool.must).toHaveLength(1); // should not be any other filtering happening
expect(query.body.query.bool.must).toContainEqual({
bool: {
must_not: {
bool: {
should: [
{
match: {
'host.ip': '10.140.73.246',
},
},
],
minimum_should_match: 1,
},
},
},
});
});
it(
'test default query params for all endpoints endpoint metadata excluding unerolled endpoint ' +
'and when body filter is provided',
async () => {
const unenrolledElasticAgentId = '1fdca33f-799f-49f4-939c-ea4383c77672';
const mockRequest = httpServerMock.createKibanaRequest({
body: {
filters: { kql: 'not host.ip:10.140.73.246' },
},
});
const query = await kibanaRequestToMetadataListESQuery(
mockRequest,
{
logFactory: loggingSystemMock.create(),
service: new EndpointAppContextService(),
config: () => Promise.resolve(createMockConfig()),
experimentalFeatures: parseExperimentalConfigValue(
createMockConfig().enableExperimental
),
},
metadataQueryStrategyV1(),
{
unenrolledAgentIds: [unenrolledElasticAgentId],
}
);
expect(query.body.query.bool.must.length).toBeGreaterThan(1);
// unenrollment filter should be there
expect(query.body.query.bool.must).toContainEqual({
bool: {
must_not: [
{ terms: { 'elastic.agent.id': [unenrolledElasticAgentId] } },
// below is not actually necessary behavior for v1, but hard to structure the test to ignore it
{ terms: { 'HostDetails.elastic.agent.id': [unenrolledElasticAgentId] } },
],
},
});
// and KQL should also be there
expect(query.body.query.bool.must).toContainEqual({
bool: {
must_not: {
bool: {
should: [
{
match: {
'host.ip': '10.140.73.246',
},
},
],
minimum_should_match: 1,
},
},
},
});
}
);
});
describe('MetadataGetQuery', () => {
it('searches for the correct ID', () => {
const mockID = 'AABBCCDD-0011-2233-AA44-DEADBEEF8899';
const query = getESQueryHostMetadataByID(mockID, metadataQueryStrategyV1());
expect(get(query, 'body.query.bool.filter.0.bool.should')).toContainEqual({
term: { 'agent.id': mockID },
});
});
});
});

View file

@ -6,102 +6,39 @@
*/
import { SearchResponse } from '@elastic/elasticsearch/api/types';
import {
metadataCurrentIndexPattern,
metadataIndexPattern,
} from '../../../../../common/endpoint/constants';
import { HostMetadata, MetadataQueryStrategyVersions } from '../../../../../common/endpoint/types';
import { HostListQueryResult, HostQueryResult, MetadataQueryStrategy } from '../../../types';
export function metadataQueryStrategyV1(): MetadataQueryStrategy {
return {
index: metadataIndexPattern,
extraBodyProperties: {
collapse: {
field: 'agent.id',
inner_hits: {
name: 'most_recent',
size: 1,
sort: [{ 'event.created': 'desc' }],
},
},
aggs: {
total: {
cardinality: {
field: 'agent.id',
},
},
},
},
queryResponseToHostListResult: (
searchResponse: SearchResponse<HostMetadata>
): HostListQueryResult => {
const response = searchResponse as SearchResponse<HostMetadata>;
return {
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)
.map((entry) => (entry?._source ?? {}) as HostMetadata),
queryStrategyVersion: MetadataQueryStrategyVersions.VERSION_1,
};
},
queryResponseToHostResult: (searchResponse: SearchResponse<HostMetadata>): HostQueryResult => {
const response = searchResponse as SearchResponse<HostMetadata>;
return {
resultLength: response.hits.hits.length,
result: response.hits.hits.length > 0 ? response.hits.hits[0]._source : undefined,
queryStrategyVersion: MetadataQueryStrategyVersions.VERSION_1,
};
},
};
}
export function metadataQueryStrategyV2(): MetadataQueryStrategy {
return {
index: metadataCurrentIndexPattern,
extraBodyProperties: {
track_total_hits: true,
},
queryResponseToHostListResult: (
searchResponse: SearchResponse<HostMetadata | { HostDetails: HostMetadata }>
): HostListQueryResult => {
const response = searchResponse as SearchResponse<
HostMetadata | { HostDetails: HostMetadata }
>;
const list =
response.hits.hits.length > 0
? response.hits.hits.map((entry) => stripHostDetails(entry?._source as HostMetadata))
: [];
return {
resultLength:
((response.hits?.total as unknown) as { value: number; relation: string }).value || 0,
resultList: list,
queryStrategyVersion: MetadataQueryStrategyVersions.VERSION_2,
};
},
queryResponseToHostResult: (
searchResponse: SearchResponse<HostMetadata | { HostDetails: HostMetadata }>
): HostQueryResult => {
const response = searchResponse as SearchResponse<
HostMetadata | { HostDetails: HostMetadata }
>;
return {
resultLength: response.hits.hits.length,
result:
response.hits.hits.length > 0
? stripHostDetails(response.hits.hits[0]._source as HostMetadata)
: undefined,
queryStrategyVersion: MetadataQueryStrategyVersions.VERSION_2,
};
},
};
}
import { HostMetadata } from '../../../../../common/endpoint/types';
import { HostListQueryResult, HostQueryResult } from '../../../types';
// remove the top-level 'HostDetails' property if found, from previous schemas
function stripHostDetails(host: HostMetadata | { HostDetails: HostMetadata }): HostMetadata {
return 'HostDetails' in host ? host.HostDetails : host;
}
export const queryResponseToHostResult = (
searchResponse: SearchResponse<HostMetadata | { HostDetails: HostMetadata }>
): HostQueryResult => {
const response = searchResponse as SearchResponse<HostMetadata | { HostDetails: HostMetadata }>;
return {
resultLength: response.hits.hits.length,
result:
response.hits.hits.length > 0
? stripHostDetails(response.hits.hits[0]._source as HostMetadata)
: undefined,
};
};
export const queryResponseToHostListResult = (
searchResponse: SearchResponse<HostMetadata | { HostDetails: HostMetadata }>
): HostListQueryResult => {
const response = searchResponse as SearchResponse<HostMetadata | { HostDetails: HostMetadata }>;
const list =
response.hits.hits.length > 0
? response.hits.hits.map((entry) => stripHostDetails(entry?._source as HostMetadata))
: [];
return {
resultLength:
((response.hits?.total as unknown) as { value: number; relation: string }).value || 0,
resultList: list,
};
};

View file

@ -8,62 +8,6 @@
import { SearchResponse } from 'elasticsearch';
import { HostMetadata } from '../../../../../common/endpoint/types';
export function createV1SearchResponse(hostMetadata?: HostMetadata): SearchResponse<HostMetadata> {
return ({
took: 15,
timed_out: false,
_shards: {
total: 1,
successful: 1,
skipped: 0,
failed: 0,
},
hits: {
total: {
value: 5,
relation: 'eq',
},
max_score: null,
hits: hostMetadata
? [
{
_index: 'metrics-endpoint.metadata-default',
_id: '8FhM0HEBYyRTvb6lOQnw',
_score: null,
_source: hostMetadata,
sort: [1588337587997],
inner_hits: {
most_recent: {
hits: {
total: {
value: 2,
relation: 'eq',
},
max_score: null,
hits: [
{
_index: 'metrics-endpoint.metadata-default',
_id: 'W6Vo1G8BYQH1gtPUgYkC',
_score: null,
_source: hostMetadata,
sort: [1579816615336],
},
],
},
},
},
},
]
: [],
},
aggregations: {
total: {
value: 1,
},
},
} as unknown) as SearchResponse<HostMetadata>;
}
export function createV2SearchResponse(hostMetadata?: HostMetadata): SearchResponse<HostMetadata> {
return ({
took: 15,

View file

@ -10,20 +10,15 @@ import { SearchResponse } from 'elasticsearch';
import { HostMetadata } from '../../../common/endpoint/types';
import { SecuritySolutionRequestHandlerContext } from '../../types';
import { getESQueryHostMetadataByIDs } from '../routes/metadata/query_builders';
import { EndpointAppContext } from '../types';
import { queryResponseToHostListResult } from '../routes/metadata/support/query_strategies';
export async function getMetadataForEndpoints(
endpointIDs: string[],
requestHandlerContext: SecuritySolutionRequestHandlerContext,
endpointAppContext: EndpointAppContext
requestHandlerContext: SecuritySolutionRequestHandlerContext
): Promise<HostMetadata[]> {
const queryStrategy = await endpointAppContext.service
?.getMetadataService()
?.queryStrategy(requestHandlerContext.core.savedObjects.client);
const query = getESQueryHostMetadataByIDs(endpointIDs, queryStrategy!);
const query = getESQueryHostMetadataByIDs(endpointIDs);
const esClient = requestHandlerContext.core.elasticsearch.client.asCurrentUser;
const { body } = await esClient.search<HostMetadata>(query as SearchRequest);
const hosts = queryStrategy!.queryResponseToHostListResult(body as SearchResponse<HostMetadata>);
const hosts = queryResponseToHostListResult(body as SearchResponse<HostMetadata>);
return hosts.resultList;
}

View file

@ -7,11 +7,9 @@
import { LoggerFactory } from 'kibana/server';
import { SearchResponse } from '@elastic/elasticsearch/api/types';
import { JsonObject } from '@kbn/common-utils';
import { ConfigType } from '../config';
import { EndpointAppContextService } from './endpoint_app_context_services';
import { HostMetadata, MetadataQueryStrategyVersions } from '../../common/endpoint/types';
import { HostMetadata } from '../../common/endpoint/types';
import { ExperimentalFeatures } from '../../common/experimental_features';
/**
@ -31,20 +29,9 @@ export interface EndpointAppContext {
export interface HostListQueryResult {
resultLength: number;
resultList: HostMetadata[];
queryStrategyVersion: MetadataQueryStrategyVersions;
}
export interface HostQueryResult {
resultLength: number;
result: HostMetadata | undefined;
queryStrategyVersion: MetadataQueryStrategyVersions;
}
export interface MetadataQueryStrategy {
index: string;
extraBodyProperties?: JsonObject;
queryResponseToHostListResult: (
searchResponse: SearchResponse<HostMetadata>
) => HostListQueryResult;
queryResponseToHostResult: (searchResponse: SearchResponse<HostMetadata>) => HostQueryResult;
}

View file

@ -199,10 +199,10 @@ export const getHostEndpoint = async (
};
const endpointData =
id != null && metadataRequestContext.endpointAppContextService.getAgentService() != null
? await getHostMetaData(metadataRequestContext, id, undefined)
? await getHostMetaData(metadataRequestContext, id)
: null;
const fleetAgentId = endpointData?.metadata.elastic.agent.id;
const fleetAgentId = endpointData?.elastic.agent.id;
const [fleetAgentStatus, pendingActions] = !fleetAgentId
? [undefined, {}]
: await Promise.all([
@ -214,13 +214,13 @@ export const getHostEndpoint = async (
}),
]);
return endpointData != null && endpointData.metadata
return endpointData != null && endpointData
? {
endpointPolicy: endpointData.metadata.Endpoint.policy.applied.name,
policyStatus: endpointData.metadata.Endpoint.policy.applied.status,
sensorVersion: endpointData.metadata.agent.version,
endpointPolicy: endpointData.Endpoint.policy.applied.name,
policyStatus: endpointData.Endpoint.policy.applied.status,
sensorVersion: endpointData.agent.version,
elasticAgentStatus: fleetAgentStatusToEndpointHostStatus(fleetAgentStatus!),
isolation: endpointData.metadata.Endpoint.state?.isolation ?? false,
isolation: endpointData.Endpoint.state?.isolation ?? false,
pendingActions,
}
: null;

View file

@ -29,7 +29,6 @@ export default function endpointAPIIntegrationTests(providerContext: FtrProvider
});
loadTestFile(require.resolve('./resolver/index'));
loadTestFile(require.resolve('./metadata'));
loadTestFile(require.resolve('./metadata_v1'));
loadTestFile(require.resolve('./policy'));
loadTestFile(require.resolve('./package'));
});

View file

@ -12,7 +12,6 @@ import {
deleteAllDocsFromMetadataIndex,
deleteMetadataStream,
} from './data_stream_helper';
import { MetadataQueryStrategyVersions } from '../../../plugins/security_solution/common/endpoint/types';
import { HOST_METADATA_LIST_ROUTE } from '../../../plugins/security_solution/common/endpoint/constants';
/**
@ -88,7 +87,6 @@ export default function ({ getService }: FtrProviderContext) {
expect(body.hosts.length).to.eql(1);
expect(body.request_page_size).to.eql(1);
expect(body.request_page_index).to.eql(1);
expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_2);
});
/* test that when paging properties produces no result, the total should reflect the actual number of metadata
@ -113,7 +111,6 @@ export default function ({ getService }: FtrProviderContext) {
expect(body.hosts.length).to.eql(0);
expect(body.request_page_size).to.eql(10);
expect(body.request_page_index).to.eql(30);
expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_2);
});
it('metadata api should return 400 when pagingProperties is below boundaries.', async () => {
@ -148,7 +145,6 @@ export default function ({ getService }: FtrProviderContext) {
expect(body.hosts.length).to.eql(2);
expect(body.request_page_size).to.eql(10);
expect(body.request_page_index).to.eql(0);
expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_2);
});
it('metadata api should return page based on filters and paging passed.', async () => {
@ -186,7 +182,6 @@ export default function ({ getService }: FtrProviderContext) {
expect(body.hosts.length).to.eql(2);
expect(body.request_page_size).to.eql(10);
expect(body.request_page_index).to.eql(0);
expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_2);
});
it('metadata api should return page based on host.os.Ext.variant filter.', async () => {
@ -208,7 +203,6 @@ export default function ({ getService }: FtrProviderContext) {
expect(body.hosts.length).to.eql(2);
expect(body.request_page_size).to.eql(10);
expect(body.request_page_index).to.eql(0);
expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_2);
});
it('metadata api should return the latest event for all the events for an endpoint', async () => {
@ -231,7 +225,6 @@ export default function ({ getService }: FtrProviderContext) {
expect(body.hosts.length).to.eql(1);
expect(body.request_page_size).to.eql(10);
expect(body.request_page_index).to.eql(0);
expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_2);
});
it('metadata api should return the latest event for all the events where policy status is not success', async () => {
@ -275,7 +268,6 @@ export default function ({ getService }: FtrProviderContext) {
expect(body.hosts.length).to.eql(1);
expect(body.request_page_size).to.eql(10);
expect(body.request_page_index).to.eql(0);
expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_2);
});
it('metadata api should return all hosts when filter is empty string', async () => {
@ -292,7 +284,6 @@ export default function ({ getService }: FtrProviderContext) {
expect(body.hosts.length).to.eql(numberOfHostsInFixture);
expect(body.request_page_size).to.eql(10);
expect(body.request_page_index).to.eql(0);
expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_2);
});
});
});

View file

@ -1,290 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../ftr_provider_context';
import { deleteMetadataStream } from './data_stream_helper';
import { METADATA_REQUEST_V1_ROUTE } from '../../../plugins/security_solution/server/endpoint/routes/metadata';
import { MetadataQueryStrategyVersions } from '../../../plugins/security_solution/common/endpoint/types';
/**
* The number of host documents in the es archive.
*/
const numberOfHostsInFixture = 3;
export default function ({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const supertest = getService('supertest');
describe('test metadata api v1', () => {
describe(`POST ${METADATA_REQUEST_V1_ROUTE} when index is empty`, () => {
it('metadata api should return empty result when index is empty', async () => {
// the endpoint uses data streams and es archiver does not support deleting them at the moment so we need
// to do it manually
await deleteMetadataStream(getService);
const { body } = await supertest
.post(`${METADATA_REQUEST_V1_ROUTE}`)
.set('kbn-xsrf', 'xxx')
.send()
.expect(200);
expect(body.total).to.eql(0);
expect(body.hosts.length).to.eql(0);
expect(body.request_page_size).to.eql(10);
expect(body.request_page_index).to.eql(0);
expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1);
});
});
describe(`POST ${METADATA_REQUEST_V1_ROUTE} when index is not empty`, () => {
before(
async () =>
await esArchiver.load(
'x-pack/test/functional/es_archives/endpoint/metadata/api_feature',
{ useCreate: true }
)
);
// the endpoint uses data streams and es archiver does not support deleting them at the moment so we need
// to do it manually
after(async () => await deleteMetadataStream(getService));
it('metadata api should return one entry for each host with default paging', async () => {
const { body } = await supertest
.post(`${METADATA_REQUEST_V1_ROUTE}`)
.set('kbn-xsrf', 'xxx')
.send()
.expect(200);
expect(body.total).to.eql(numberOfHostsInFixture);
expect(body.hosts.length).to.eql(numberOfHostsInFixture);
expect(body.request_page_size).to.eql(10);
expect(body.request_page_index).to.eql(0);
expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1);
});
it('metadata api should return page based on paging properties passed.', async () => {
const { body } = await supertest
.post(`${METADATA_REQUEST_V1_ROUTE}`)
.set('kbn-xsrf', 'xxx')
.send({
paging_properties: [
{
page_size: 1,
},
{
page_index: 1,
},
],
})
.expect(200);
expect(body.total).to.eql(numberOfHostsInFixture);
expect(body.hosts.length).to.eql(1);
expect(body.request_page_size).to.eql(1);
expect(body.request_page_index).to.eql(1);
expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1);
});
/* test that when paging properties produces no result, the total should reflect the actual number of metadata
in the index.
*/
it('metadata api should return accurate total metadata if page index produces no result', async () => {
const { body } = await supertest
.post(`${METADATA_REQUEST_V1_ROUTE}`)
.set('kbn-xsrf', 'xxx')
.send({
paging_properties: [
{
page_size: 10,
},
{
page_index: 3,
},
],
})
.expect(200);
expect(body.total).to.eql(numberOfHostsInFixture);
expect(body.hosts.length).to.eql(0);
expect(body.request_page_size).to.eql(10);
expect(body.request_page_index).to.eql(30);
expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1);
});
it('metadata api should return 400 when pagingProperties is below boundaries.', async () => {
const { body } = await supertest
.post(`${METADATA_REQUEST_V1_ROUTE}`)
.set('kbn-xsrf', 'xxx')
.send({
paging_properties: [
{
page_size: 0,
},
{
page_index: 1,
},
],
})
.expect(400);
expect(body.message).to.contain('Value must be equal to or greater than [1]');
});
it('metadata api should return page based on filters passed.', async () => {
const { body } = await supertest
.post(`${METADATA_REQUEST_V1_ROUTE}`)
.set('kbn-xsrf', 'xxx')
.send({
filters: {
kql: 'not host.ip:10.46.229.234',
},
})
.expect(200);
expect(body.total).to.eql(2);
expect(body.hosts.length).to.eql(2);
expect(body.request_page_size).to.eql(10);
expect(body.request_page_index).to.eql(0);
expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1);
});
it('metadata api should return page based on filters and paging passed.', async () => {
const notIncludedIp = '10.46.229.234';
const { body } = await supertest
.post(`${METADATA_REQUEST_V1_ROUTE}`)
.set('kbn-xsrf', 'xxx')
.send({
paging_properties: [
{
page_size: 10,
},
{
page_index: 0,
},
],
filters: {
kql: `not host.ip:${notIncludedIp}`,
},
})
.expect(200);
expect(body.total).to.eql(2);
const resultIps: string[] = [].concat(
...body.hosts.map((hostInfo: Record<string, any>) => hostInfo.metadata.host.ip)
);
expect(resultIps).to.eql([
'10.192.213.130',
'10.70.28.129',
'10.101.149.26',
'2606:a000:ffc0:39:11ef:37b9:3371:578c',
]);
expect(resultIps).not.include.eql(notIncludedIp);
expect(body.hosts.length).to.eql(2);
expect(body.request_page_size).to.eql(10);
expect(body.request_page_index).to.eql(0);
expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1);
});
it('metadata api should return page based on host.os.Ext.variant filter.', async () => {
const variantValue = 'Windows Pro';
const { body } = await supertest
.post(`${METADATA_REQUEST_V1_ROUTE}`)
.set('kbn-xsrf', 'xxx')
.send({
filters: {
kql: `host.os.Ext.variant:${variantValue}`,
},
})
.expect(200);
expect(body.total).to.eql(2);
const resultOsVariantValue: Set<string> = new Set(
body.hosts.map((hostInfo: Record<string, any>) => hostInfo.metadata.host.os.Ext.variant)
);
expect(Array.from(resultOsVariantValue)).to.eql([variantValue]);
expect(body.hosts.length).to.eql(2);
expect(body.request_page_size).to.eql(10);
expect(body.request_page_index).to.eql(0);
expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1);
});
it('metadata api should return the latest event for all the events for an endpoint', async () => {
const targetEndpointIp = '10.46.229.234';
const { body } = await supertest
.post(`${METADATA_REQUEST_V1_ROUTE}`)
.set('kbn-xsrf', 'xxx')
.send({
filters: {
kql: `host.ip:${targetEndpointIp}`,
},
})
.expect(200);
expect(body.total).to.eql(1);
const resultIp: string = body.hosts[0].metadata.host.ip.filter(
(ip: string) => ip === targetEndpointIp
);
expect(resultIp).to.eql([targetEndpointIp]);
expect(body.hosts[0].metadata.event.created).to.eql(1618841405309);
expect(body.hosts.length).to.eql(1);
expect(body.request_page_size).to.eql(10);
expect(body.request_page_index).to.eql(0);
expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1);
});
it('metadata api should return the latest event for all the events where policy status is not success', async () => {
const { body } = await supertest
.post(`${METADATA_REQUEST_V1_ROUTE}`)
.set('kbn-xsrf', 'xxx')
.send({
filters: {
kql: `not Endpoint.policy.applied.status:success`,
},
})
.expect(200);
const statuses: Set<string> = new Set(
body.hosts.map(
(hostInfo: Record<string, any>) => hostInfo.metadata.Endpoint.policy.applied.status
)
);
expect(statuses.size).to.eql(1);
expect(Array.from(statuses)).to.eql(['failure']);
});
it('metadata api should return the endpoint based on the elastic agent id, and status should be unhealthy', async () => {
const targetEndpointId = 'fc0ff548-feba-41b6-8367-65e8790d0eaf';
const targetElasticAgentId = '023fa40c-411d-4188-a941-4147bfadd095';
const { body } = await supertest
.post(`${METADATA_REQUEST_V1_ROUTE}`)
.set('kbn-xsrf', 'xxx')
.send({
filters: {
kql: `elastic.agent.id:${targetElasticAgentId}`,
},
})
.expect(200);
expect(body.total).to.eql(1);
const resultHostId: string = body.hosts[0].metadata.host.id;
const resultElasticAgentId: string = body.hosts[0].metadata.elastic.agent.id;
expect(resultHostId).to.eql(targetEndpointId);
expect(resultElasticAgentId).to.eql(targetElasticAgentId);
expect(body.hosts[0].metadata.event.created).to.eql(1618841405309);
expect(body.hosts[0].host_status).to.eql('unhealthy');
expect(body.hosts.length).to.eql(1);
expect(body.request_page_size).to.eql(10);
expect(body.request_page_index).to.eql(0);
expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1);
});
it('metadata api should return all hosts when filter is empty string', async () => {
const { body } = await supertest
.post(`${METADATA_REQUEST_V1_ROUTE}`)
.set('kbn-xsrf', 'xxx')
.send({
filters: {
kql: '',
},
})
.expect(200);
expect(body.total).to.eql(numberOfHostsInFixture);
expect(body.hosts.length).to.eql(numberOfHostsInFixture);
expect(body.request_page_size).to.eql(10);
expect(body.request_page_index).to.eql(0);
expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1);
});
});
});
}