[Security Solution][Endpoint] Update to Endpoint List and associated supporting API to be space aware (#194312)

## Summary

PR makes the following changes in support of Endpoint management support
for Spaces:


### Fleet

- Adds new method - `getByIds()` - to the server-side Agent service
- Updates some mocks


### Security Solution

- Updates the Endpoint host metadata API for both the list and single
endpoint to be space aware
- Updates the agent status API for Endpoint to be space aware
- Updates endpoint policy response API to be space aware
- New FTR API integration test suite was created
- included changes to some service utilities to enable loading test data
per-space
This commit is contained in:
Paul Tavares 2024-10-17 10:24:38 -04:00 committed by GitHub
parent 8cfa396f46
commit 2a786b88a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 1566 additions and 246 deletions

View file

@ -98,6 +98,7 @@ enabled:
- x-pack/test/security_solution_api_integration/test_suites/edr_workflows/policy_response/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/edr_workflows/spaces/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_endpoint/configs/serverless.endpoint.config.ts
- x-pack/test/security_solution_endpoint/configs/serverless.integrations.config.ts
# serverless config files that run deployment-agnostic tests

View file

@ -85,6 +85,7 @@ enabled:
- x-pack/test/security_solution_api_integration/test_suites/edr_workflows/policy_response/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/edr_workflows/spaces/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_endpoint/configs/endpoint.config.ts
- x-pack/test/security_solution_endpoint/configs/integrations.config.ts
- x-pack/test/api_integration/apis/cloud_security_posture/config.ts

View file

@ -8,9 +8,9 @@
import type { FleetErrorType } from './types';
export class FleetError extends Error {
export class FleetError<TMeta = unknown> extends Error {
attributes?: { type: FleetErrorType };
constructor(message?: string, public readonly meta?: unknown) {
constructor(message?: string, public readonly meta?: TMeta) {
super(message);
this.name = this.constructor.name; // for stack traces
}

View file

@ -95,7 +95,7 @@ export class FleetEncryptedSavedObjectEncryptionKeyRequired extends FleetError {
export class FleetSetupError extends FleetError {}
export class GenerateServiceTokenError extends FleetError {}
export class FleetUnauthorizedError extends FleetError {}
export class FleetNotFoundError extends FleetError {}
export class FleetNotFoundError<TMeta = unknown> extends FleetError<TMeta> {}
export class FleetTooManyRequestsError extends FleetError {}
export class OutputUnauthorizedError extends FleetError {}
@ -105,7 +105,7 @@ export class DownloadSourceError extends FleetError {}
export class DeleteUnenrolledAgentsPreconfiguredError extends FleetError {}
// Not found errors
export class AgentNotFoundError extends FleetNotFoundError {}
export class AgentNotFoundError extends FleetNotFoundError<{ agentId: string }> {}
export class AgentPolicyNotFoundError extends FleetNotFoundError {}
export class AgentActionNotFoundError extends FleetNotFoundError {}
export class DownloadSourceNotFound extends FleetNotFoundError {}
@ -115,7 +115,10 @@ export class SigningServiceNotFoundError extends FleetNotFoundError {}
export class InputNotFoundError extends FleetNotFoundError {}
export class OutputNotFoundError extends FleetNotFoundError {}
export class PackageNotFoundError extends FleetNotFoundError {}
export class PackagePolicyNotFoundError extends FleetNotFoundError {}
export class PackagePolicyNotFoundError extends FleetNotFoundError<{
/** The package policy ID that was not found */
packagePolicyId: string;
}> {}
export class StreamNotFoundError extends FleetNotFoundError {}
export class FleetServerHostUnauthorizedError extends FleetUnauthorizedError {}

View file

@ -15,10 +15,12 @@ const createClientMock = (): jest.Mocked<AgentClient> => ({
getAgentStatusForAgentPolicy: jest.fn(),
listAgents: jest.fn(),
getLatestAgentAvailableVersion: jest.fn(),
getByIds: jest.fn(async (..._) => []),
});
const createServiceMock = (): DeeplyMockedKeys<AgentService> => ({
asInternalUser: createClientMock(),
asInternalScopedUser: jest.fn().mockReturnValue(createClientMock()),
asScoped: jest.fn().mockReturnValue(createClientMock()),
});

View file

@ -185,6 +185,27 @@ describe('AgentService', () => {
() => new AgentServiceImpl(mockEsClient, mockSoClient).asInternalUser
);
});
describe('asInternalScopedUser', () => {
it('should throw error if no space id is passed', () => {
const agentService = new AgentServiceImpl(
elasticsearchServiceMock.createElasticsearchClient(),
savedObjectsClientMock.create()
);
expect(() => agentService.asInternalScopedUser('')).toThrowError(TypeError);
});
{
const mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
const mockSoClient = savedObjectsClientMock.create();
expectApisToCallServicesSuccessfully(
mockEsClient,
() => mockSoClient,
() => new AgentServiceImpl(mockEsClient, mockSoClient).asInternalUser
);
}
});
});
function expectApisToCallServicesSuccessfully(

View file

@ -27,7 +27,7 @@ import { FleetUnauthorizedError } from '../../errors';
import { getCurrentNamespace } from '../spaces/get_current_namespace';
import { getAgentsByKuery, getAgentById } from './crud';
import { getAgentsByKuery, getAgentById, getByIds } from './crud';
import { getAgentStatusById, getAgentStatusForAgentPolicy } from './status';
import { getLatestAvailableAgentVersion } from './versions';
@ -42,6 +42,11 @@ export interface AgentService {
*/
asScoped(req: KibanaRequest): AgentClient;
/**
* Scoped services to a given space
*/
asInternalScopedUser(spaceId: string): AgentClient;
/**
* Only use for server-side usages (eg. telemetry), should not be used for end users unless an explicit authz check is
* done.
@ -60,6 +65,12 @@ export interface AgentClient {
*/
getAgent(agentId: string): Promise<Agent>;
/**
* Get multiple agents by id
* @param agentIds
*/
getByIds(agentIds: string[], options?: { ignoreMissing?: boolean }): Promise<Agent[]>;
/**
* Return the status by the Agent's id
*/
@ -128,6 +139,14 @@ class AgentClientImpl implements AgentClient {
return getAgentById(this.internalEsClient, this.soClient, agentId);
}
public async getByIds(
agentIds: string[],
options?: Partial<{ ignoreMissing: boolean }>
): Promise<Agent[]> {
await this.#runPreflight();
return getByIds(this.internalEsClient, this.soClient, agentIds, options);
}
public async getAgentStatusById(agentId: string) {
await this.#runPreflight();
return getAgentStatusById(this.internalEsClient, this.soClient, agentId);
@ -187,6 +206,21 @@ export class AgentServiceImpl implements AgentService {
);
}
public asInternalScopedUser(spaceId: string): AgentClient {
if (!spaceId) {
throw new TypeError(`spaceId argument is required!`);
}
const soClient = appContextService.getInternalUserSOClientForSpaceId(spaceId);
return new AgentClientImpl(
this.internalEsClient,
soClient,
undefined,
getCurrentNamespace(soClient)
);
}
public get asInternalUser() {
return new AgentClientImpl(this.internalEsClient, this.soClient);
}

View file

@ -9,6 +9,10 @@ import type { ElasticsearchClient } from '@kbn/core/server';
import { elasticsearchServiceMock, savedObjectsClientMock } from '@kbn/core/server/mocks';
import { toElasticsearchQuery } from '@kbn/es-query';
import { isSpaceAwarenessEnabled as _isSpaceAwarenessEnabled } from '../spaces/helpers';
import { AgentNotFoundError } from '../..';
import { AGENTS_INDEX } from '../../constants';
import { createAppContextStartContractMock } from '../../mocks';
import type { Agent } from '../../types';
@ -24,6 +28,7 @@ import {
openPointInTime,
updateAgent,
_joinFilters,
getByIds,
} from './crud';
jest.mock('../audit_logging');
@ -41,6 +46,7 @@ jest.mock('./versions', () => {
jest.mock('../spaces/helpers');
const mockedAuditLoggingService = auditLoggingService as jest.Mocked<typeof auditLoggingService>;
const isSpaceAwarenessEnabledMock = _isSpaceAwarenessEnabled as jest.Mock;
describe('Agents CRUD test', () => {
const soClientMock = savedObjectsClientMock.create();
@ -63,13 +69,22 @@ describe('Agents CRUD test', () => {
appContextService.start(mockContract);
});
function getEsResponse(ids: string[], total: number, status: AgentStatus) {
afterEach(() => {
isSpaceAwarenessEnabledMock.mockReset();
});
function getEsResponse(
ids: string[],
total: number,
status: AgentStatus,
generateSource: (id: string) => Partial<Agent> = () => ({})
) {
return {
hits: {
total,
hits: ids.map((id: string) => ({
_id: id,
_source: {},
_source: generateSource(id),
fields: {
status: [status],
},
@ -513,4 +528,48 @@ describe('Agents CRUD test', () => {
});
});
});
describe(`getByIds()`, () => {
let searchResponse: ReturnType<typeof getEsResponse>;
beforeEach(() => {
searchResponse = getEsResponse(['1', '2'], 2, 'online', (id) => {
return { id, namespaces: ['foo'] };
});
(soClientMock.getCurrentNamespace as jest.Mock).mockReturnValue('foo');
searchMock.mockImplementation(async () => searchResponse);
});
it('should return a list of agents', async () => {
await expect(getByIds(esClientMock, soClientMock, ['1', '2'])).resolves.toEqual([
expect.objectContaining({ id: '1' }),
expect.objectContaining({ id: '2' }),
]);
});
it('should omit agents that are not found if `ignoreMissing` is true', async () => {
searchResponse.hits.hits = [searchResponse.hits.hits[0]];
await expect(
getByIds(esClientMock, soClientMock, ['1', '2'], { ignoreMissing: true })
).resolves.toEqual([expect.objectContaining({ id: '1' })]);
});
it('should error if agent is not found and `ignoreMissing` is false', async () => {
searchResponse.hits.hits = [searchResponse.hits.hits[0]];
await expect(getByIds(esClientMock, soClientMock, ['1', '2'])).rejects.toThrow(
AgentNotFoundError
);
});
it('should error if agent is not part of current space', async () => {
searchResponse.hits.hits[0]._source.namespaces = ['bar'];
isSpaceAwarenessEnabledMock.mockResolvedValue(true);
await expect(getByIds(esClientMock, soClientMock, ['1', '2'])).rejects.toThrow(
AgentNotFoundError
);
});
});
});

View file

@ -415,6 +415,46 @@ export async function getAgentById(
return agentHit;
}
/**
* Get list of agents by `id`. service method performs space awareness checks.
* @param esClient
* @param soClient
* @param agentIds
* @param options
*
* @throws AgentNotFoundError
*/
export const getByIds = async (
esClient: ElasticsearchClient,
soClient: SavedObjectsClientContract,
agentIds: string[],
options?: Partial<{ ignoreMissing: boolean }>
): Promise<Agent[]> => {
const agentsHits = await getAgentsById(esClient, soClient, agentIds);
const currentNamespace = getCurrentNamespace(soClient);
const response: Agent[] = [];
for (const agentHit of agentsHits) {
let throwError = false;
if ('notFound' in agentHit && !options?.ignoreMissing) {
throwError = true;
} else if ((await isAgentInNamespace(agentHit as Agent, currentNamespace)) !== true) {
throwError = true;
}
if (throwError) {
throw new AgentNotFoundError(`Agent ${agentHit.id} not found`, { agentId: agentHit.id });
}
if (!(`notFound` in agentHit)) {
response.push(agentHit);
}
}
return response;
};
async function _filterAgents(
esClient: ElasticsearchClient,
soClient: SavedObjectsClientContract,

View file

@ -771,7 +771,9 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
if (options.ignoreMissing && so.error.statusCode === 404) {
return null;
} else if (so.error.statusCode === 404) {
throw new PackagePolicyNotFoundError(`Package policy ${so.id} not found`);
throw new PackagePolicyNotFoundError(`Package policy ${so.id} not found`, {
packagePolicyId: so.id,
});
} else {
throw new FleetError(so.error.message);
}

View file

@ -11,9 +11,15 @@ import type { DeepPartial } from 'utility-types';
import { merge } from 'lodash';
import { set } from '@kbn/safer-lodash-set';
import { gte } from 'semver';
import type { Agent } from '@kbn/fleet-plugin/common';
import type { EndpointCapabilities } from '../service/response_actions/constants';
import { BaseDataGenerator } from './base_data_generator';
import type { HostMetadataInterface, OSFields, HostInfoInterface } from '../types';
import type {
HostMetadataInterface,
OSFields,
HostInfoInterface,
UnitedAgentMetadataPersistedData,
} from '../types';
import { EndpointStatus, HostPolicyResponseActionStatus, HostStatus } from '../types';
export interface GetCustomEndpointMetadataGeneratorOptions {
@ -226,6 +232,30 @@ export class EndpointMetadataGenerator extends BaseDataGenerator {
return merge(hostInfo, overrides);
}
generateUnitedAgentMetadata(
overrides: DeepPartial<UnitedAgentMetadataPersistedData> = {}
): UnitedAgentMetadataPersistedData {
const endpointMetadata = this.generate();
return merge(
{
agent: {
id: endpointMetadata.agent.id,
},
united: {
endpoint: endpointMetadata,
agent: {
agent: {
id: endpointMetadata.agent.id,
},
policy_id: this.seededUUIDv4(),
} as Agent,
},
} as UnitedAgentMetadataPersistedData,
overrides
);
}
protected randomOsFields(): OSFields {
return this.randomChoice([
EndpointMetadataGenerator.windowsOSFields,

View file

@ -42,7 +42,12 @@ import {
indexFleetEndpointPolicy,
} from './index_fleet_endpoint_policy';
import { metadataCurrentIndexPattern } from '../constants';
import { EndpointDataLoadingError, mergeAndAppendArrays, wrapErrorAndRejectPromise } from './utils';
import {
EndpointDataLoadingError,
fetchActiveSpaceId,
mergeAndAppendArrays,
wrapErrorAndRejectPromise,
} from './utils';
export interface IndexedHostsResponse
extends IndexedFleetAgentResponse,
@ -112,6 +117,7 @@ export const indexEndpointHostDocs = usageTracker.track(
const timeBetweenDocs = 6 * 3600 * 1000; // 6 hours between metadata documents
const timestamp = new Date().getTime();
const kibanaVersion = await fetchKibanaVersion(kbnClient);
const activeSpaceId = await fetchActiveSpaceId(kbnClient);
const response: IndexedHostsResponse = {
hosts: [],
agents: [],
@ -137,7 +143,7 @@ export const indexEndpointHostDocs = usageTracker.track(
for (let j = 0; j < numDocs; j++) {
generator.updateHostData();
generator.updateHostPolicyData();
generator.updateHostPolicyData({ excludeInitialPolicy: true });
hostMetadata = generator.generateHostMetadata(
timestamp - timeBetweenDocs * (numDocs - j - 1),
@ -178,6 +184,7 @@ export const indexEndpointHostDocs = usageTracker.track(
const { agents, fleetAgentsIndex, operations } = buildFleetAgentBulkCreateOperations({
endpoints: [hostMetadata],
agentPolicyId: policyId,
spaceId: activeSpaceId,
kibanaVersion,
});

View file

@ -10,13 +10,12 @@ import type {
DeleteByQueryResponse,
IndexRequest,
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { KbnClient } from '@kbn/test';
import type { FleetServerAgent } from '@kbn/fleet-plugin/common';
import { AGENTS_INDEX } from '@kbn/fleet-plugin/common';
import type { BulkRequest } from '@elastic/elasticsearch/lib/api/types';
import type { DeepPartial } from 'utility-types';
import type { ToolingLog } from '@kbn/tooling-log';
import { usageTracker } from './usage_tracker';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import type { HostMetadata } from '../types';
import { FleetAgentGenerator } from '../data_generators/fleet_agent_generator';
import { createToolingLogger, wrapErrorAndRejectPromise } from './utils';
@ -28,57 +27,12 @@ export interface IndexedFleetAgentResponse {
fleetAgentsIndex: string;
}
/**
* Indexes a Fleet Agent
* (NOTE: ensure that fleet is setup first before calling this loading function)
*
* @param esClient
* @param kbnClient
* @param endpointHost
* @param agentPolicyId
* @param [kibanaVersion]
* @param [fleetAgentGenerator]
*/
export const indexFleetAgentForHost = usageTracker.track(
'indexFleetAgentForHost',
async (
esClient: Client,
kbnClient: KbnClient,
endpointHost: HostMetadata,
agentPolicyId: string,
kibanaVersion: string = '8.0.0',
fleetAgentGenerator: FleetAgentGenerator = defaultFleetAgentGenerator
): Promise<IndexedFleetAgentResponse> => {
const agentDoc = generateFleetAgentEsHitForEndpointHost(
endpointHost,
agentPolicyId,
kibanaVersion,
fleetAgentGenerator
);
await esClient
.index<FleetServerAgent>({
index: agentDoc._index,
id: agentDoc._id,
body: agentDoc._source,
op_type: 'create',
refresh: 'wait_for',
})
.catch(wrapErrorAndRejectPromise);
return {
fleetAgentsIndex: agentDoc._index,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
agents: [agentDoc._source!],
};
}
);
const generateFleetAgentEsHitForEndpointHost = (
endpointHost: HostMetadata,
agentPolicyId: string,
kibanaVersion: string = '8.0.0',
fleetAgentGenerator: FleetAgentGenerator = defaultFleetAgentGenerator
fleetAgentGenerator: FleetAgentGenerator = defaultFleetAgentGenerator,
spaceId: string = DEFAULT_SPACE_ID
) => {
return fleetAgentGenerator.generateEsHit({
_id: endpointHost.agent.id,
@ -102,6 +56,7 @@ const generateFleetAgentEsHitForEndpointHost = (
},
},
policy_id: agentPolicyId,
namespaces: [spaceId],
},
});
};
@ -110,6 +65,7 @@ interface BuildFleetAgentBulkCreateOperationsOptions {
endpoints: HostMetadata[];
agentPolicyId: string;
kibanaVersion?: string;
spaceId?: string;
fleetAgentGenerator?: FleetAgentGenerator;
}
@ -130,6 +86,7 @@ export const buildFleetAgentBulkCreateOperations = ({
agentPolicyId,
kibanaVersion = '8.0.0',
fleetAgentGenerator = defaultFleetAgentGenerator,
spaceId = DEFAULT_SPACE_ID,
}: BuildFleetAgentBulkCreateOperationsOptions): BuildFleetAgentBulkCreateOperationsResponse => {
const response: BuildFleetAgentBulkCreateOperationsResponse = {
operations: [],
@ -142,7 +99,8 @@ export const buildFleetAgentBulkCreateOperations = ({
endpointHost,
agentPolicyId,
kibanaVersion,
fleetAgentGenerator
fleetAgentGenerator,
spaceId
);
response.operations.push(

View file

@ -8,11 +8,13 @@
import type { Client } from '@elastic/elasticsearch';
import { kibanaPackageJson } from '@kbn/repo-info';
import type { KbnClient } from '@kbn/test';
import { v4 as uuidV4 } from 'uuid';
import type {
GetPackagePoliciesResponse,
AgentPolicy,
GetOneAgentPolicyResponse,
CreateAgentPolicyResponse,
NewAgentPolicy,
} from '@kbn/fleet-plugin/common';
import {
AGENT_POLICY_API_ROUTES,
@ -23,11 +25,12 @@ import {
packagePolicyRouteService,
} from '@kbn/fleet-plugin/common';
import type { ToolingLog } from '@kbn/tooling-log';
import { fetchFleetLatestAvailableAgentVersion } from '../utils/fetch_fleet_version';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { indexFleetServerAgent } from './index_fleet_agent';
import { catchAxiosErrorFormatAndThrow } from '../format_axios_error';
import { usageTracker } from './usage_tracker';
import { createToolingLogger, wrapErrorAndRejectPromise } from './utils';
import { createToolingLogger, fetchActiveSpaceId, wrapErrorAndRejectPromise } from './utils';
/**
* Will ensure that at least one fleet server is present in the `.fleet-agents` index. This will
@ -48,17 +51,18 @@ export const enableFleetServerIfNecessary = usageTracker.track(
log: ToolingLog = createToolingLogger(),
version: string = kibanaPackageJson.version
) => {
let agentVersion = version;
const activeSpaceId = await fetchActiveSpaceId(kbnClient);
const agentPolicy = await getOrCreateFleetServerAgentPolicy(kbnClient, activeSpaceId, log);
if (isServerless) {
agentVersion = await fetchFleetLatestAvailableAgentVersion(kbnClient);
}
const agentPolicy = await getOrCreateFleetServerAgentPolicy(kbnClient, log);
if (!isServerless && !(await hasFleetServerAgent(esClient, agentPolicy.id))) {
if (
!isServerless &&
!(await hasFleetServerAgent(esClient, agentPolicy.id, activeSpaceId, log))
) {
log.debug(`Indexing a new fleet server agent`);
const lastCheckin = new Date();
const agentVersion = version;
lastCheckin.setFullYear(lastCheckin.getFullYear() + 1);
const indexedAgent = await indexFleetServerAgent(esClient, log, {
@ -66,9 +70,10 @@ export const enableFleetServerIfNecessary = usageTracker.track(
agent: { version: agentVersion },
last_checkin_status: 'online',
last_checkin: lastCheckin.toISOString(),
namespaces: agentPolicy.space_ids ?? [activeSpaceId],
});
log.verbose(`New fleet server agent indexed:\n${JSON.stringify(indexedAgent)}`);
log.verbose(`New fleet server agent indexed:\n${JSON.stringify(indexedAgent, null, 2)}`);
} else {
log.debug(`Nothing to do. A Fleet Server agent is already registered with Fleet`);
}
@ -77,6 +82,7 @@ export const enableFleetServerIfNecessary = usageTracker.track(
const getOrCreateFleetServerAgentPolicy = async (
kbnClient: KbnClient,
spaceId?: string,
log: ToolingLog = createToolingLogger()
): Promise<AgentPolicy> => {
const packagePolicies = await kbnClient
@ -92,8 +98,10 @@ const getOrCreateFleetServerAgentPolicy = async (
.catch(catchAxiosErrorFormatAndThrow);
if (packagePolicies.data.items[0]) {
log.debug(`Found an existing package policy - fetching associated agent policy`);
log.verbose(JSON.stringify(packagePolicies.data.items[0]));
log.debug(
`Found an existing Fleet Server package policy [${packagePolicies.data.items[0].id}] - fetching associated agent policy`
);
log.verbose(JSON.stringify(packagePolicies.data, null, 2));
return kbnClient
.request<GetOneAgentPolicyResponse>({
@ -103,8 +111,9 @@ const getOrCreateFleetServerAgentPolicy = async (
})
.catch(catchAxiosErrorFormatAndThrow)
.then((response) => {
log.debug(`Returning existing Fleet Server agent policy [${response.data.item.id}]`);
log.verbose(
`Existing agent policy for Fleet Server:\n${JSON.stringify(response.data.item)}`
`Existing agent policy for Fleet Server:\n${JSON.stringify(response.data.item, null, 2)}`
);
return response.data.item;
@ -113,26 +122,33 @@ const getOrCreateFleetServerAgentPolicy = async (
log.debug(`Creating a new fleet server agent policy`);
const policy: NewAgentPolicy = {
name: `Fleet Server policy (${Math.random().toString(32).substring(2)})`,
id: uuidV4(),
description: `Created by CLI Tool via: ${__filename}`,
namespace: spaceId ?? DEFAULT_SPACE_ID,
monitoring_enabled: [],
// This will ensure the Fleet Server integration policy
// is also created and added to the agent policy
has_fleet_server: true,
};
log.verbose(`New policy:\n${JSON.stringify(policy, null, 2)}`);
// create new Fleet Server agent policy
return kbnClient
.request<CreateAgentPolicyResponse>({
method: 'POST',
path: AGENT_POLICY_API_ROUTES.CREATE_PATTERN,
headers: { 'elastic-api-version': '2023-10-31' },
body: {
name: `Fleet Server policy (${Math.random().toString(32).substring(2)})`,
description: `Created by CLI Tool via: ${__filename}`,
namespace: 'default',
monitoring_enabled: [],
// This will ensure the Fleet Server integration policy
// is also created and added to the agent policy
has_fleet_server: true,
},
body: policy,
})
.then((response) => {
log.verbose(
`No fleet server agent policy found. Created a new one:\n${JSON.stringify(
response.data.item
response.data.item,
null,
2
)}`
);
@ -143,8 +159,23 @@ const getOrCreateFleetServerAgentPolicy = async (
const hasFleetServerAgent = async (
esClient: Client,
fleetServerAgentPolicyId: string
fleetServerAgentPolicyId: string,
spaceId?: string,
log: ToolingLog = createToolingLogger()
): Promise<boolean> => {
const query: QueryDslQueryContainer = {
bool: {
filter: [
{
term: {
policy_id: fleetServerAgentPolicyId,
},
},
...(spaceId ? [{ term: { namespaces: spaceId } }] : []),
],
},
};
const searchResponse = await esClient
.search(
{
@ -152,16 +183,19 @@ const hasFleetServerAgent = async (
ignore_unavailable: true,
rest_total_hits_as_int: true,
size: 1,
_source: false,
query: {
match: {
policy_id: fleetServerAgentPolicyId,
},
},
query,
},
{ ignore: [404] }
)
.catch(wrapErrorAndRejectPromise);
log.verbose(
`Search for a fleet server agent with query:\n${JSON.stringify(
query,
null,
2
)}\nreturn:\n ${fleetServerAgentPolicyId}]\n${JSON.stringify(searchResponse, null, 2)}`
);
return Boolean(searchResponse?.hits.total);
};

View file

@ -5,11 +5,14 @@
* 2.0.
*/
import { mergeWith } from 'lodash';
import { memoize, mergeWith } from 'lodash';
import type { ToolingLogTextWriterConfig } from '@kbn/tooling-log';
import { ToolingLog } from '@kbn/tooling-log';
import type { Flags } from '@kbn/dev-cli-runner';
import moment from 'moment/moment';
import type { Space } from '@kbn/spaces-plugin/common';
import type { KbnClient } from '@kbn/test';
import { catchAxiosErrorFormatAndThrow } from '../format_axios_error';
import { EndpointError } from '../errors';
export const RETRYABLE_TRANSIENT_ERRORS: Readonly<Array<string | RegExp>> = [
@ -183,3 +186,13 @@ export const getElapsedTime = (
return `${hours}:${minutes}:${seconds}.${milliseconds}`;
};
export const fetchActiveSpaceId = memoize(async (kbnClient: KbnClient): Promise<string> => {
return kbnClient
.request<Space>({
method: 'GET',
path: `/internal/spaces/_active_space`,
})
.catch(catchAxiosErrorFormatAndThrow)
.then((response) => response.data.id);
});

View file

@ -6,10 +6,11 @@
*/
import { AxiosError } from 'axios';
import { EndpointError } from './errors';
/* eslint-disable @typescript-eslint/no-explicit-any */
export class FormattedAxiosError extends Error {
export class FormattedAxiosError extends EndpointError {
public readonly request: {
method: string;
url: string;
@ -28,7 +29,8 @@ export class FormattedAxiosError extends Error {
super(
`${axiosError.message}${
axiosError?.response?.data ? `: ${JSON.stringify(axiosError?.response?.data)}` : ''
}${url ? `\n(Request: ${method} ${url})` : ''}`
}${url ? `\n(Request: ${method} ${url})` : ''}`,
axiosError
);
this.request = {

View file

@ -400,10 +400,20 @@ export class EndpointDocGenerator extends BaseDataGenerator {
/**
* Updates the current Host common record applied Policy to a different one from the list
* of random choices and gives it a random policy response status.
*
*/
public updateHostPolicyData() {
public updateHostPolicyData({
excludeInitialPolicy = false,
}: Partial<{
/** Excludes the initial policy id (non-existent) that endpoint reports when it first is installed */
excludeInitialPolicy: boolean;
}> = {}) {
const newInfo = this.commonInfo;
newInfo.Endpoint.policy.applied = this.randomChoice(APPLIED_POLICIES);
newInfo.Endpoint.policy.applied = this.randomChoice(
excludeInitialPolicy
? APPLIED_POLICIES.filter(({ id }) => id !== '00000000-0000-0000-0000-000000000000')
: APPLIED_POLICIES
);
newInfo.Endpoint.policy.applied.status = this.randomChoice(POLICY_RESPONSE_STATUSES);
this.commonInfo = newInfo;
}

View file

@ -538,6 +538,7 @@ export interface HostMetadataInterface {
status: EndpointStatus;
policy: {
applied: {
/** The Endpoint integration policy UUID */
id: string;
status: HostPolicyResponseActionStatus;
name: string;

View file

@ -27,8 +27,12 @@ export const ensureSpaceIdExists = async (
return;
}
const alreadyExists = await kbnClient.spaces
.get(spaceId)
const alreadyExists = await kbnClient
.request({
method: 'GET',
path: `/api/spaces/space/${spaceId}`,
headers: { 'elastic-api-version': '2023-10-31' },
})
.then(() => {
log.debug(`Space id [${spaceId}] already exists. Nothing to do.`);
return true;
@ -45,12 +49,20 @@ export const ensureSpaceIdExists = async (
if (!alreadyExists) {
log.info(`Creating space id [${spaceId}]`);
await kbnClient.spaces
.create({
name: spaceId,
id: spaceId,
await kbnClient
.request({
method: 'POST',
path: `/api/spaces/space`,
headers: { 'elastic-api-version': '2023-10-31' },
body: {
name: spaceId,
id: spaceId,
},
})
.catch(catchAxiosErrorFormatAndThrow);
.catch(catchAxiosErrorFormatAndThrow)
.then((response) => {
log.verbose(`space created:\n${JSON.stringify(response.data, null, 2)}`);
});
}
};

View file

@ -253,20 +253,30 @@ export class EndpointAppContextService {
throw new EndpointAppContentServicesNotStartedError();
}
const spaceIdValue = this.experimentalFeatures.endpointManagementSpaceAwarenessEnabled
? spaceId
: DEFAULT_SPACE_ID;
return new EndpointMetadataService(
this.startDependencies.esClient,
this.savedObjects.createInternalScopedSoClient({ readonly: false }),
this.getInternalFleetServices(),
this.savedObjects.createInternalScopedSoClient({ readonly: false, spaceId: spaceIdValue }),
this.getInternalFleetServices(spaceIdValue),
this.createLogger('endpointMetadata')
);
}
public getInternalFleetServices(): EndpointInternalFleetServicesInterface {
/**
* SpaceId should be defined if wanting go get back an inernal client that is scoped to a given space id
* @param spaceId
*/
public getInternalFleetServices(spaceId?: string): EndpointInternalFleetServicesInterface {
if (this.fleetServicesFactory === null) {
throw new EndpointAppContentServicesNotStartedError();
}
return this.fleetServicesFactory.asInternalUser();
return this.fleetServicesFactory.asInternalUser(
this.experimentalFeatures.endpointManagementSpaceAwarenessEnabled ? spaceId : undefined
);
}
public getManifestManager(): ManifestManager | undefined {

View file

@ -50,6 +50,7 @@ import { unsecuredActionsClientMock } from '@kbn/actions-plugin/server/unsecured
import type { PluginStartContract as ActionPluginStartContract } from '@kbn/actions-plugin/server';
import type { Mutable } from 'utility-types';
import type { DeeplyMockedKeys } from '@kbn/utility-types-jest';
import { createSavedObjectsClientFactoryMock } from '../services/saved_objects/saved_objects_client_factory.mocks';
import { EndpointMetadataService } from '../services/metadata';
import { createEndpointFleetServicesFactoryMock } from '../services/fleet/endpoint_fleet_services_factory.mocks';
import type { ProductFeaturesService } from '../../lib/product_features_service';
@ -99,7 +100,8 @@ export const createMockEndpointAppContext = (
export const createMockEndpointAppContextService = (
mockManifestManager?: ManifestManager
): jest.Mocked<EndpointAppContextService> => {
const { esClient, fleetStartServices } = createMockEndpointAppContextServiceStartContract();
const { esClient, fleetStartServices, savedObjectsServiceStart } =
createMockEndpointAppContextServiceStartContract();
const fleetServices = createEndpointFleetServicesFactoryMock({
fleetDependencies: fleetStartServices,
}).service.asInternalUser();
@ -141,6 +143,7 @@ export const createMockEndpointAppContextService = (
getInternalResponseActionsClient: jest.fn(() => {
return responseActionsClientMock.create();
}),
savedObjects: createSavedObjectsClientFactoryMock({ savedObjectsServiceStart }).service,
} as unknown as jest.Mocked<EndpointAppContextService>;
};

View file

@ -109,7 +109,8 @@ describe('Agent Status API route handler', () => {
expect(httpResponseMock.ok).toHaveBeenCalled();
expect(getAgentStatusClientMock).toHaveBeenCalledWith(agentType, {
esClient: (await httpHandlerContextMock.core).elasticsearch.client.asInternalUser,
soClient: (await httpHandlerContextMock.core).savedObjects.client,
soClient:
apiTestSetup.endpointAppContextMock.service.savedObjects.createInternalScopedSoClient(),
connectorActionsClient: (await httpHandlerContextMock.actions).getActionsClient(),
endpointService: apiTestSetup.endpointAppContextMock.service,
});
@ -145,4 +146,38 @@ describe('Agent Status API route handler', () => {
},
});
});
it('should NOT use space ID in creating SO client when feature is disabled', async () => {
await apiTestSetup
.getRegisteredVersionedRoute('get', AGENT_STATUS_ROUTE, '1')
.routeHandler(httpHandlerContextMock, httpRequestMock, httpResponseMock);
expect(httpResponseMock.ok).toHaveBeenCalled();
expect(
apiTestSetup.endpointAppContextMock.service.savedObjects.createInternalScopedSoClient
).toHaveBeenCalledWith({
spaceId: undefined,
});
});
it('should use a scoped SO client when spaces awareness feature is enabled', async () => {
// @ts-expect-error write to readonly property
apiTestSetup.endpointAppContextMock.service.experimentalFeatures.endpointManagementSpaceAwarenessEnabled =
true;
((await httpHandlerContextMock.securitySolution).getSpaceId as jest.Mock).mockReturnValue(
'foo'
);
await apiTestSetup
.getRegisteredVersionedRoute('get', AGENT_STATUS_ROUTE, '1')
.routeHandler(httpHandlerContextMock, httpRequestMock, httpResponseMock);
expect(httpResponseMock.ok).toHaveBeenCalled();
expect(
apiTestSetup.endpointAppContextMock.service.savedObjects.createInternalScopedSoClient
).toHaveBeenCalledWith({
spaceId: 'foo',
});
});
});

View file

@ -78,18 +78,29 @@ export const getAgentStatusRouteHandler = (
);
}
const esClient = (await context.core).elasticsearch.client.asInternalUser;
const soClient = (await context.core).savedObjects.client;
const connectorActionsClient = (await context.actions).getActionsClient();
const agentStatusClient = getAgentStatusClient(agentType, {
esClient,
soClient,
connectorActionsClient,
endpointService: endpointContext.service,
});
const data = await agentStatusClient.getAgentStatuses(agentIds);
try {
const [securitySolutionPlugin, corePlugin, actionsPlugin] = await Promise.all([
context.securitySolution,
context.core,
context.actions,
]);
const esClient = corePlugin.elasticsearch.client.asInternalUser;
const spaceId = endpointContext.service.experimentalFeatures
.endpointManagementSpaceAwarenessEnabled
? securitySolutionPlugin.getSpaceId()
: undefined;
const soClient = endpointContext.service.savedObjects.createInternalScopedSoClient({
spaceId,
});
const connectorActionsClient = actionsPlugin.getActionsClient();
const agentStatusClient = getAgentStatusClient(agentType, {
esClient,
soClient,
connectorActionsClient,
endpointService: endpointContext.service,
});
const data = await agentStatusClient.getAgentStatuses(agentIds);
return response.ok({ body: { data } });
} catch (e) {
return errorHandler(logger, response, e);

View file

@ -9,6 +9,7 @@ import type { TypeOf } from '@kbn/config-schema';
import type { Logger, RequestHandler } from '@kbn/core/server';
import { FLEET_ENDPOINT_PACKAGE } from '@kbn/fleet-plugin/common';
import { stringify } from '../../utils/stringify';
import type {
MetadataListResponse,
EndpointSortableField,
@ -45,7 +46,10 @@ export function getMetadataListRequestHandler(
SecuritySolutionRequestHandlerContext
> {
return async (context, request, response) => {
const endpointMetadataService = endpointAppContext.service.getEndpointMetadataService();
logger.debug(() => `endpoint host metadata list request:\n${stringify(request.query)}`);
const spaceId = (await context.securitySolution).getSpaceId();
const endpointMetadataService = endpointAppContext.service.getEndpointMetadataService(spaceId);
try {
const { data, total } = await endpointMetadataService.getHostMetadataList(request.query);
@ -77,7 +81,8 @@ export const getMetadataRequestHandler = function (
SecuritySolutionRequestHandlerContext
> {
return async (context, request, response) => {
const endpointMetadataService = endpointAppContext.service.getEndpointMetadataService();
const spaceId = (await context.securitySolution).getSpaceId();
const endpointMetadataService = endpointAppContext.service.getEndpointMetadataService(spaceId);
try {
return response.ok({

View file

@ -19,7 +19,11 @@ import {
} from '@kbn/core/server/mocks';
import { createAppContextStartContractMock as fleetCreateAppContextStartContractMock } from '@kbn/fleet-plugin/server/mocks';
import { appContextService as fleetAppContextService } from '@kbn/fleet-plugin/server/services';
import type { HostInfo, MetadataListResponse } from '../../../../common/endpoint/types';
import type {
HostInfo,
MetadataListResponse,
UnitedAgentMetadataPersistedData,
} from '../../../../common/endpoint/types';
import { HostStatus } from '../../../../common/endpoint/types';
import { registerEndpointRoutes } from '.';
import {
@ -62,6 +66,7 @@ import type { TransformGetTransformStatsResponse } from '@elastic/elasticsearch/
import { getEndpointAuthzInitialStateMock } from '../../../../common/endpoint/service/authz/mocks';
import type { VersionedRouteConfig } from '@kbn/core-http-server';
import type { SecuritySolutionPluginRouterMock } from '../../../mocks';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
describe('test endpoint routes', () => {
let routerMock: SecuritySolutionPluginRouterMock;
@ -124,15 +129,9 @@ describe('test endpoint routes', () => {
afterEach(() => endpointAppContextService.stop());
describe('GET list endpoints route', () => {
it('should return expected metadata', async () => {
const mockRequest = httpServerMock.createKibanaRequest({
query: {
page: 0,
pageSize: 10,
hostStatuses: ['updating'],
kuery: 'not host.ip:10.140.73.246',
},
});
let searchListResponse: estypes.SearchResponse<UnitedAgentMetadataPersistedData>;
beforeEach(() => {
mockSavedObjectClient.find.mockResolvedValueOnce({
total: 0,
saved_objects: [],
@ -144,12 +143,25 @@ describe('test endpoint routes', () => {
withoutSpaceExtensions: mockSavedObjectClient,
})
);
searchListResponse = unitedMetadataSearchResponseMock(
new EndpointDocGenerator('seed').generateHostMetadata()
);
mockAgentClient.getAgentStatusById.mockResolvedValue('error');
mockAgentClient.listAgents.mockResolvedValue(noUnenrolledAgent);
mockAgentPolicyService.getByIds = jest.fn().mockResolvedValueOnce([]);
const metadata = new EndpointDocGenerator().generateHostMetadata();
mockScopedClient.asInternalUser.search.mockResponseOnce(searchListResponse);
});
it('should return expected metadata', async () => {
const mockRequest = httpServerMock.createKibanaRequest({
query: {
page: 0,
pageSize: 10,
hostStatuses: ['updating'],
kuery: 'not host.ip:10.140.73.246',
},
});
const esSearchMock = mockScopedClient.asInternalUser.search;
esSearchMock.mockResponseOnce(unitedMetadataSearchResponseMock(metadata));
({ routeHandler, routeConfig } = getRegisteredVersionedRouteMock(
routerMock,
@ -233,7 +245,9 @@ describe('test endpoint routes', () => {
expect(mockResponse.ok).toBeCalled();
const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as MetadataListResponse;
expect(endpointResultList.data.length).toEqual(1);
expect(endpointResultList.data[0].metadata).toEqual(metadata);
expect(endpointResultList.data[0].metadata).toEqual(
searchListResponse.hits.hits[0]._source!.united.endpoint
);
expect(endpointResultList.total).toEqual(1);
expect(endpointResultList.page).toEqual(0);
expect(endpointResultList.pageSize).toEqual(10);
@ -262,6 +276,27 @@ describe('test endpoint routes', () => {
expect(mockResponse.forbidden).toBeCalled();
});
it('should use space id when retrieving Endpoint Metadata service client', async () => {
const mockRequest = httpServerMock.createKibanaRequest();
const mockContext = createRouteHandlerContext(mockScopedClient, mockSavedObjectClient);
(mockContext.securitySolution.getSpaceId as jest.Mock).mockReturnValue('foo');
({ routeHandler, routeConfig } = getRegisteredVersionedRouteMock(
routerMock,
'get',
HOST_METADATA_LIST_ROUTE,
'2023-10-31'
));
const getEndpointMetadataServiceSpy = jest.spyOn(
endpointAppContextService,
'getEndpointMetadataService'
);
await routeHandler(mockContext, mockRequest, mockResponse);
expect(getEndpointMetadataServiceSpy).toHaveBeenCalledWith('foo');
});
});
describe('GET endpoint details route', () => {
@ -497,6 +532,34 @@ describe('test endpoint routes', () => {
expect(mockResponse.forbidden).toBeCalled();
});
it('should retrieve Endpoint Metadata Service client using the space id', async () => {
const response = legacyMetadataSearchResponseMock(
new EndpointDocGenerator().generateHostMetadata()
);
const mockRequest = httpServerMock.createKibanaRequest({
params: { id: response.hits.hits[0]._id },
});
const esSearchMock = mockScopedClient.asInternalUser.search;
mockAgentClient.getAgent.mockResolvedValue(agentGenerator.generate({ status: 'online' }));
esSearchMock.mockResponseOnce(response);
const getEndpointMetadataServiceSpy = jest.spyOn(
endpointAppContextService,
'getEndpointMetadataService'
);
({ routeConfig, routeHandler } = getRegisteredVersionedRouteMock(
routerMock,
'get',
HOST_METADATA_GET_ROUTE,
'2023-10-31'
));
const mockContext = createRouteHandlerContext(mockScopedClient, mockSavedObjectClient);
(mockContext.securitySolution.getSpaceId as jest.Mock).mockReturnValue('foo');
await routeHandler(mockContext, mockRequest, mockResponse);
expect(getEndpointMetadataServiceSpy).toHaveBeenCalledWith('foo');
});
});
describe('GET metadata transform stats route', () => {

View file

@ -27,6 +27,8 @@ import type { Agent } from '@kbn/fleet-plugin/common/types/models';
import type { AgentClient } from '@kbn/fleet-plugin/server/services';
import { get } from 'lodash';
import type { ScopedClusterClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
import type { TypeOf } from '@kbn/config-schema';
import type { GetPolicyResponseSchema } from '../../../../common/api/endpoint';
describe('test policy response handler', () => {
let endpointAppContextService: EndpointAppContextService;
@ -48,11 +50,15 @@ describe('test policy response handler', () => {
it('should return the latest policy response for a host', async () => {
const response = createSearchResponse(new EndpointDocGenerator().generatePolicyResponse());
const hostPolicyResponseHandler = getHostPolicyResponseHandler();
const hostPolicyResponseHandler = getHostPolicyResponseHandler(endpointAppContextService);
mockScopedClient.asInternalUser.search.mockResponseOnce(response);
const mockRequest = httpServerMock.createKibanaRequest({
params: { agentId: 'id' },
const mockRequest = httpServerMock.createKibanaRequest<
never,
TypeOf<typeof GetPolicyResponseSchema.query>,
never
>({
query: { agentId: 'id' },
});
await hostPolicyResponseHandler(
@ -71,12 +77,16 @@ describe('test policy response handler', () => {
});
it('should return not found when there is no response policy for host', async () => {
const hostPolicyResponseHandler = getHostPolicyResponseHandler();
const hostPolicyResponseHandler = getHostPolicyResponseHandler(endpointAppContextService);
mockScopedClient.asInternalUser.search.mockResponseOnce(createSearchResponse());
const mockRequest = httpServerMock.createKibanaRequest({
params: { agentId: 'id' },
const mockRequest = httpServerMock.createKibanaRequest<
never,
TypeOf<typeof GetPolicyResponseSchema.query>,
never
>({
query: { agentId: 'foo' },
});
await hostPolicyResponseHandler(
@ -87,9 +97,34 @@ describe('test policy response handler', () => {
mockResponse
);
expect(mockResponse.notFound).toBeCalled();
const message = mockResponse.notFound.mock.calls[0][0]?.body;
expect(message).toEqual('Policy Response Not Found');
expect(mockResponse.notFound).toHaveBeenCalledWith({
body: expect.objectContaining({
message: 'Policy response for endpoint id [foo] not found',
}),
});
});
it('should retrieve internal fleet services using space id', async () => {
mockScopedClient.asInternalUser.search.mockResponseOnce(createSearchResponse());
const getInternalFleetServicesSpy = jest.spyOn(
endpointAppContextService,
'getInternalFleetServices'
);
const hostPolicyResponseHandler = getHostPolicyResponseHandler(endpointAppContextService);
const mockRequest = httpServerMock.createKibanaRequest<
never,
TypeOf<typeof GetPolicyResponseSchema.query>,
never
>({
query: { agentId: 'foo' },
});
const mockContext = requestContextMock.convertContext(
createRouteHandlerContext(mockScopedClient, mockSavedObjectClient)
);
((await mockContext.securitySolution).getSpaceId as jest.Mock).mockReturnValue('foo');
await hostPolicyResponseHandler(mockContext, mockRequest, mockResponse);
expect(getInternalFleetServicesSpy).toHaveBeenCalledWith('foo');
});
});

View file

@ -7,7 +7,9 @@
import type { RequestHandler } from '@kbn/core/server';
import type { TypeOf } from '@kbn/config-schema';
import { policyIndexPattern } from '../../../../common/endpoint/constants';
import type { SecuritySolutionRequestHandlerContext } from '../../../types';
import type { EndpointAppContextService } from '../../endpoint_app_context_services';
import { errorHandler } from '../error_handler';
import type {
GetPolicyResponseSchema,
GetAgentPolicySummaryRequestSchema,
@ -15,21 +17,37 @@ import type {
import type { EndpointAppContext } from '../../types';
import { getAgentPolicySummary, getPolicyResponseByAgentId } from './service';
import type { GetAgentSummaryResponse } from '../../../../common/endpoint/types';
import { NotFoundError } from '../../errors';
export const getHostPolicyResponseHandler = function (): RequestHandler<
undefined,
export const getHostPolicyResponseHandler = function (
endpointAppContextServices: EndpointAppContextService
): RequestHandler<
never,
TypeOf<typeof GetPolicyResponseSchema.query>,
undefined
never,
SecuritySolutionRequestHandlerContext
> {
const logger = endpointAppContextServices.createLogger('endpointPolicyResponse');
return async (context, request, response) => {
const client = (await context.core).elasticsearch.client;
const doc = await getPolicyResponseByAgentId(policyIndexPattern, request.query.agentId, client);
const spaceId = (await context.securitySolution).getSpaceId();
const esClient = (await context.core).elasticsearch.client.asInternalUser;
const fleetServices = endpointAppContextServices.getInternalFleetServices(spaceId);
if (doc) {
return response.ok({ body: doc });
try {
const agentId = request.query.agentId;
const doc = await getPolicyResponseByAgentId(agentId, esClient, fleetServices);
if (doc) {
return response.ok({ body: doc });
}
logger.debug(`Agent id [${agentId}] has no policy response documents indexed yet`);
throw new NotFoundError(`Policy response for endpoint id [${agentId}] not found`);
} catch (err) {
return errorHandler(logger, response, err);
}
return response.notFound({ body: 'Policy Response Not Found' });
};
};

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import type { IRouter } from '@kbn/core/server';
import {
GetPolicyResponseSchema,
GetAgentPolicySummaryRequestSchema,
@ -17,10 +16,14 @@ import {
BASE_POLICY_RESPONSE_ROUTE,
} from '../../../../common/endpoint/constants';
import { withEndpointAuthz } from '../with_endpoint_authz';
import type { SecuritySolutionPluginRouter } from '../../../types';
export const INITIAL_POLICY_ID = '00000000-0000-0000-0000-000000000000';
export function registerPolicyRoutes(router: IRouter, endpointAppContext: EndpointAppContext) {
export function registerPolicyRoutes(
router: SecuritySolutionPluginRouter,
endpointAppContext: EndpointAppContext
) {
const logger = endpointAppContext.logFactory.get('endpointPolicy');
router.versioned
@ -39,7 +42,7 @@ export function registerPolicyRoutes(router: IRouter, endpointAppContext: Endpoi
withEndpointAuthz(
{ any: ['canReadSecuritySolution', 'canAccessFleet'] },
logger,
getHostPolicyResponseHandler()
getHostPolicyResponseHandler(endpointAppContext.service)
)
);

View file

@ -6,36 +6,91 @@
*/
import { GetPolicyResponseSchema } from '../../../../common/api/endpoint';
import { getESQueryPolicyResponseByAgentID } from './service';
import { getESQueryPolicyResponseByAgentID, getPolicyResponseByAgentId } from './service';
import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks';
import type { ElasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
import type { EndpointInternalFleetServicesInterfaceMocked } from '../../services/fleet/endpoint_fleet_services_factory.mocks';
import { createEndpointFleetServicesFactoryMock } from '../../services/fleet/endpoint_fleet_services_factory.mocks';
import { applyEsClientSearchMock } from '../../mocks/utils.mock';
import { policyIndexPattern } from '../../../../common/endpoint/constants';
import { EndpointPolicyResponseGenerator } from '../../../../common/endpoint/data_generators/endpoint_policy_response_generator';
describe('test policy handlers schema', () => {
it('validate that get policy response query schema', async () => {
expect(
GetPolicyResponseSchema.query.validate({
agentId: 'id',
})
).toBeTruthy();
describe('Policy Response Services', () => {
describe('test policy handlers schema', () => {
it('validate that get policy response query schema', async () => {
expect(
GetPolicyResponseSchema.query.validate({
agentId: 'id',
})
).toBeTruthy();
expect(() => GetPolicyResponseSchema.query.validate({})).toThrowError();
});
});
describe('test policy query', () => {
it('queries for the correct host', async () => {
const agentId = 'f757d3c0-e874-11ea-9ad9-015510b487f4';
const query = getESQueryPolicyResponseByAgentID(agentId, 'anyindex');
expect(query.body?.query?.bool?.filter).toEqual({ term: { 'agent.id': agentId } });
expect(() => GetPolicyResponseSchema.query.validate({})).toThrowError();
});
});
it('filters out initial policy by ID', async () => {
const query = getESQueryPolicyResponseByAgentID(
'f757d3c0-e874-11ea-9ad9-015510b487f4',
'anyindex'
);
expect(query.body?.query?.bool?.must_not).toEqual({
term: {
'Endpoint.policy.applied.id': '00000000-0000-0000-0000-000000000000',
},
describe('test policy query', () => {
it('queries for the correct host', async () => {
const agentId = 'f757d3c0-e874-11ea-9ad9-015510b487f4';
const query = getESQueryPolicyResponseByAgentID(agentId, 'anyindex');
expect(query.body?.query?.bool?.filter).toEqual({ term: { 'agent.id': agentId } });
});
it('filters out initial policy by ID', async () => {
const query = getESQueryPolicyResponseByAgentID(
'f757d3c0-e874-11ea-9ad9-015510b487f4',
'anyindex'
);
expect(query.body?.query?.bool?.must_not).toEqual({
term: {
'Endpoint.policy.applied.id': '00000000-0000-0000-0000-000000000000',
},
});
});
});
describe('getPolicyResponseByAgentId()', () => {
let esClientMock: ElasticsearchClientMock;
let fleetServicesMock: EndpointInternalFleetServicesInterfaceMocked;
beforeEach(() => {
esClientMock = elasticsearchServiceMock.createElasticsearchClient();
fleetServicesMock = createEndpointFleetServicesFactoryMock().service.asInternalUser();
applyEsClientSearchMock({
esClientMock,
index: policyIndexPattern,
response: EndpointPolicyResponseGenerator.toEsSearchResponse([
EndpointPolicyResponseGenerator.toEsSearchHit(
new EndpointPolicyResponseGenerator('seed').generate({ agent: { id: '1-2-3' } })
),
]),
});
});
it('should search using the agent id provided on input', async () => {
await getPolicyResponseByAgentId('1-2-3', esClientMock, fleetServicesMock);
expect(esClientMock.search).toHaveBeenCalledWith(
expect.objectContaining({
body: expect.objectContaining({
query: expect.objectContaining({
bool: expect.objectContaining({
filter: expect.objectContaining({
term: expect.objectContaining({
'agent.id': '1-2-3',
}),
}),
}),
}),
}),
})
);
});
it('should validate that agent id is in current space', async () => {
await getPolicyResponseByAgentId('1-2-3', esClientMock, fleetServicesMock);
expect(fleetServicesMock.ensureInCurrentSpace).toHaveBeenCalledWith({ agentIds: ['1-2-3'] });
});
});
});

View file

@ -5,12 +5,15 @@
* 2.0.
*/
import type { IScopedClusterClient, KibanaRequest } from '@kbn/core/server';
import type { ElasticsearchClient, KibanaRequest } from '@kbn/core/server';
import type { Agent } from '@kbn/fleet-plugin/common/types/models';
import type { ISearchRequestParams } from '@kbn/search-types';
import type { GetHostPolicyResponse, HostPolicyResponse } from '../../../../common/endpoint/types';
import { INITIAL_POLICY_ID } from '.';
import type { EndpointFleetServicesInterface } from '../../services/fleet';
import { policyIndexPattern } from '../../../../common/endpoint/constants';
import { catchAndWrapError } from '../../utils';
import type { EndpointAppContext } from '../../types';
import { INITIAL_POLICY_ID } from '.';
import type { GetHostPolicyResponse, HostPolicyResponse } from '../../../../common/endpoint/types';
export const getESQueryPolicyResponseByAgentID = (
agentID: string,
@ -46,14 +49,17 @@ export const getESQueryPolicyResponseByAgentID = (
};
export async function getPolicyResponseByAgentId(
index: string,
agentID: string,
dataClient: IScopedClusterClient
esClient: ElasticsearchClient,
fleetServices: EndpointFleetServicesInterface
): Promise<GetHostPolicyResponse | undefined> {
const query = getESQueryPolicyResponseByAgentID(agentID, index);
const response = await dataClient.asInternalUser.search<HostPolicyResponse>(query);
const query = getESQueryPolicyResponseByAgentID(agentID, policyIndexPattern);
const response = await esClient.search<HostPolicyResponse>(query).catch(catchAndWrapError);
if (response.hits.hits.length > 0 && response.hits.hits[0]._source != null) {
// Ensure agent is in the current space id. Call to fleet will Error if agent is not in current space
await fleetServices.ensureInCurrentSpace({ agentIds: [agentID] });
return {
policy_response: response.hits.hits[0]._source,
};

View file

@ -0,0 +1,105 @@
/*
* 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 type { AgentStatusClientOptions } from '../lib/base_agent_status_client';
import type { ApplyMetadataMocksResponse } from '../../../metadata/mocks';
import { createEndpointMetadataServiceTestContextMock } from '../../../metadata/mocks';
import { EndpointAgentStatusClient } from '../../..';
import { getPendingActionsSummary as _getPendingActionsSummary } from '../../../actions/pending_actions_summary';
import { createMockEndpointAppContextService } from '../../../../mocks';
import { appContextService as fleetAppContextService } from '@kbn/fleet-plugin/server/services';
import { createAppContextStartContractMock as fleetCreateAppContextStartContractMock } from '@kbn/fleet-plugin/server/mocks';
jest.mock('../../../actions/pending_actions_summary', () => {
const realModule = jest.requireActual('../../../actions/pending_actions_summary');
return {
...realModule,
getPendingActionsSummary: jest.fn(realModule.getPendingActionsSummary),
};
});
const getPendingActionsSummaryMock = _getPendingActionsSummary as jest.Mock;
describe('EndpointAgentStatusClient', () => {
let constructorOptions: AgentStatusClientOptions;
let statusClient: EndpointAgentStatusClient;
let dataMocks: ApplyMetadataMocksResponse;
beforeEach(() => {
const endpointAppContextServiceMock = createMockEndpointAppContextService();
const metadataMocks = createEndpointMetadataServiceTestContextMock();
const soClient = endpointAppContextServiceMock.savedObjects.createInternalScopedSoClient({
readonly: false,
});
dataMocks = metadataMocks.applyMetadataMocks(
metadataMocks.esClient,
metadataMocks.fleetServices
);
(soClient.getCurrentNamespace as jest.Mock).mockReturnValue('foo');
(endpointAppContextServiceMock.getEndpointMetadataService as jest.Mock).mockReturnValue(
metadataMocks.endpointMetadataService
);
constructorOptions = {
endpointService: endpointAppContextServiceMock,
esClient: metadataMocks.esClient,
soClient,
};
statusClient = new EndpointAgentStatusClient(constructorOptions);
// FIXME:PT need to remove the need for this mock. It appears in several test files on our side.
// Its currently needed due to the direct use of Fleet's `buildAgentStatusRuntimeField()` in
// `x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts:239`
(soClient.find as jest.Mock).mockResolvedValue({ saved_objects: [] });
fleetAppContextService.start(
fleetCreateAppContextStartContractMock({}, false, {
withoutSpaceExtensions: soClient,
})
);
});
it('should retrieve endpoint metadata service using space id', async () => {
await statusClient.getAgentStatuses(['one', 'two']);
expect(constructorOptions.endpointService.getEndpointMetadataService).toHaveBeenCalledWith(
'foo'
);
});
it('should retrieve metadata and pending actions for the agents passed on input', async () => {
const metadataClient = constructorOptions.endpointService.getEndpointMetadataService();
const agentIds = ['one', 'two'];
jest.spyOn(metadataClient, 'getHostMetadataList');
await statusClient.getAgentStatuses(agentIds);
expect(metadataClient.getHostMetadataList).toHaveBeenCalledWith(
expect.objectContaining({ kuery: 'agent.id: one or agent.id: two' })
);
expect(getPendingActionsSummaryMock).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
agentIds
);
});
it('should return expected data structure', async () => {
await expect(
statusClient.getAgentStatuses([dataMocks.unitedMetadata.agent.id])
).resolves.toEqual({
'0dc3661d-6e67-46b0-af39-6f12b025fcb0': {
agentId: '0dc3661d-6e67-46b0-af39-6f12b025fcb0',
agentType: 'endpoint',
found: true,
isolated: false,
lastSeen: expect.any(String),
pendingActions: {},
status: 'unhealthy',
},
});
});
});

View file

@ -16,8 +16,11 @@ export class EndpointAgentStatusClient extends AgentStatusClient {
protected readonly agentType: ResponseActionAgentType = 'endpoint';
async getAgentStatuses(agentIds: string[]): Promise<AgentStatusRecords> {
const metadataService = this.options.endpointService.getEndpointMetadataService();
const soClient = this.options.soClient;
const esClient = this.options.esClient;
const metadataService = this.options.endpointService.getEndpointMetadataService(
soClient.getCurrentNamespace()
);
try {
const agentIdsKql = agentIds.map((agentId) => `agent.id: ${agentId}`).join(' or ');
@ -53,7 +56,9 @@ export class EndpointAgentStatusClient extends AgentStatusClient {
}, {});
} catch (err) {
const error = new AgentStatusClientError(
`Failed to fetch endpoint agent statuses for agentIds: [${agentIds}], failed with: ${err.message}`,
`Failed to fetch endpoint agent statuses for agentIds: [${agentIds.join()}], failed with: ${
err.message
}`,
500,
err
);

View file

@ -9,15 +9,19 @@ import type { DeeplyMockedKeys } from '@kbn/utility-types-jest';
import type { FleetStartContract } from '@kbn/fleet-plugin/server';
import { createFleetStartContractMock } from '@kbn/fleet-plugin/server/mocks';
import type { SavedObjectsClientFactory } from '../saved_objects';
import type { EndpointFleetServicesFactoryInterface } from './endpoint_fleet_services_factory';
import type {
EndpointFleetServicesFactoryInterface,
EndpointInternalFleetServicesInterface,
} from './endpoint_fleet_services_factory';
import { EndpointFleetServicesFactory } from './endpoint_fleet_services_factory';
import { createSavedObjectsClientFactoryMock } from '../saved_objects/saved_objects_client_factory.mocks';
interface EndpointFleetServicesFactoryInterfaceMocked
export type EndpointInternalFleetServicesInterfaceMocked =
DeeplyMockedKeys<EndpointInternalFleetServicesInterface>;
export interface EndpointFleetServicesFactoryInterfaceMocked
extends EndpointFleetServicesFactoryInterface {
asInternalUser: () => DeeplyMockedKeys<
ReturnType<EndpointFleetServicesFactoryInterface['asInternalUser']>
>;
asInternalUser: () => EndpointInternalFleetServicesInterfaceMocked;
}
interface CreateEndpointFleetServicesFactoryMockOptions {
@ -36,11 +40,19 @@ export const createEndpointFleetServicesFactoryMock = (
savedObjects = createSavedObjectsClientFactoryMock().service,
} = dependencies;
const serviceFactoryMock = new EndpointFleetServicesFactory(
fleetDependencies,
savedObjects
) as unknown as EndpointFleetServicesFactoryInterfaceMocked;
const fleetInternalServicesMocked = serviceFactoryMock.asInternalUser();
jest.spyOn(fleetInternalServicesMocked, 'ensureInCurrentSpace');
const asInternalUserSpy = jest.spyOn(serviceFactoryMock, 'asInternalUser');
asInternalUserSpy.mockReturnValue(fleetInternalServicesMocked);
return {
service: new EndpointFleetServicesFactory(
fleetDependencies,
savedObjects
) as unknown as EndpointFleetServicesFactoryInterfaceMocked,
service: serviceFactoryMock,
dependencies: { fleetDependencies, savedObjects },
};
};

View file

@ -12,7 +12,14 @@ import type {
PackagePolicyClient,
PackageClient,
} from '@kbn/fleet-plugin/server';
import { AgentNotFoundError } from '@kbn/fleet-plugin/server';
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common';
import type { SavedObjectsClientContract } from '@kbn/core/server';
import {
AgentPolicyNotFoundError,
PackagePolicyNotFoundError,
} from '@kbn/fleet-plugin/server/errors';
import { NotFoundError } from '../../errors';
import type { SavedObjectsClientFactory } from '../saved_objects';
/**
@ -25,14 +32,26 @@ export interface EndpointFleetServicesInterface {
packagePolicy: PackagePolicyClient;
/** The `kuery` that can be used to filter for Endpoint integration policies */
endpointPolicyKuery: string;
/**
* Will check the data provided to ensure it is visible for the current space. Supports
* several types of data (ex. integration policies, agent policies, etc)
*/
ensureInCurrentSpace(options: EnsureInCurrentSpaceOptions): Promise<void>;
}
type EnsureInCurrentSpaceOptions = Partial<{
agentIds: string[];
agentPolicyIds: string[];
integrationPolicyIds: string[];
}>;
export interface EndpointInternalFleetServicesInterface extends EndpointFleetServicesInterface {
savedObjects: SavedObjectsClientFactory;
}
export interface EndpointFleetServicesFactoryInterface {
asInternalUser(): EndpointInternalFleetServicesInterface;
asInternalUser(spaceId?: string): EndpointInternalFleetServicesInterface;
}
/**
@ -44,24 +63,66 @@ export class EndpointFleetServicesFactory implements EndpointFleetServicesFactor
private readonly savedObjects: SavedObjectsClientFactory
) {}
asInternalUser(): EndpointInternalFleetServicesInterface {
asInternalUser(spaceId?: string): EndpointInternalFleetServicesInterface {
const {
agentPolicyService: agentPolicy,
packagePolicyService: packagePolicy,
agentService,
packageService,
} = this.fleetDependencies;
const agent = spaceId
? agentService.asInternalScopedUser(spaceId)
: agentService.asInternalUser;
// Lazily Initialized at the time it is needed
let soClient: SavedObjectsClientContract;
const ensureInCurrentSpace: EndpointFleetServicesInterface['ensureInCurrentSpace'] = async ({
integrationPolicyIds = [],
agentPolicyIds = [],
agentIds = [],
}): Promise<void> => {
if (!soClient) {
soClient = this.savedObjects.createInternalScopedSoClient({ spaceId });
}
const handlePromiseErrors = (err: Error): never => {
// We wrap the error with our own Error class so that the API can property return a 404
if (
err instanceof AgentNotFoundError ||
err instanceof AgentPolicyNotFoundError ||
err instanceof PackagePolicyNotFoundError
) {
throw new NotFoundError(err.message, err);
}
throw err;
};
await Promise.all([
agentIds.length ? agent.getByIds(agentIds).catch(handlePromiseErrors) : null,
agentPolicyIds.length
? agentPolicy.getByIds(soClient, agentPolicyIds).catch(handlePromiseErrors)
: null,
integrationPolicyIds.length
? packagePolicy.getByIDs(soClient, integrationPolicyIds).catch(handlePromiseErrors)
: null,
]);
};
return {
agent: agentService.asInternalUser,
agent,
agentPolicy,
packages: packageService.asInternalUser,
packagePolicy,
endpointPolicyKuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: "endpoint"`,
savedObjects: this.savedObjects,
endpointPolicyKuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: "endpoint"`,
ensureInCurrentSpace,
};
}
}

View file

@ -76,6 +76,18 @@ describe('EndpointMetadataService', () => {
const response = await metadataService.findHostMetadataForFleetAgents(fleetAgentIds);
expect(response).toEqual([endpointMetadataDoc]);
});
it('should validate agent is visible in current space', async () => {
const data = testMockedContext.applyMetadataMocks(
testMockedContext.esClient,
testMockedContext.fleetServices
);
await metadataService.findHostMetadataForFleetAgents([data.unitedMetadata.agent.id]);
expect(testMockedContext.fleetServices.ensureInCurrentSpace).toHaveBeenCalledWith({
agentIds: [data.unitedMetadata.agent.id],
});
});
});
describe('#getHostMetadataList', () => {
@ -219,4 +231,32 @@ describe('EndpointMetadataService', () => {
expect(endpointPackagePolicies).toEqual(expected);
});
});
describe('#getHostMetadata()', () => {
it('should validate agent is visible in current space', async () => {
const data = testMockedContext.applyMetadataMocks(
testMockedContext.esClient,
testMockedContext.fleetServices
);
await metadataService.getHostMetadata(data.unitedMetadata.agent.id);
expect(testMockedContext.fleetServices.ensureInCurrentSpace).toHaveBeenCalledWith({
agentIds: [data.unitedMetadata.agent.id],
});
});
});
describe('#getMetadataForEndpoints()', () => {
it('should validate agent is visible in current space', async () => {
const data = testMockedContext.applyMetadataMocks(
testMockedContext.esClient,
testMockedContext.fleetServices
);
await metadataService.getMetadataForEndpoints([data.unitedMetadata.agent.id]);
expect(testMockedContext.fleetServices.ensureInCurrentSpace).toHaveBeenCalledWith({
agentIds: [data.unitedMetadata.agent.id],
});
});
});
});

View file

@ -65,6 +65,28 @@ export class EndpointMetadataService {
private readonly logger?: Logger
) {}
/**
* Validates that the data retrieved is valid for the current user space. We do this
* by just querying fleet to ensure the policy is visible in the current space
* (the space is determined from the `soClient`)
*
* @protected
*/
protected async ensureDataValidForSpace(data: SearchResponse<HostMetadata>): Promise<void> {
const agentIds = (data?.hits?.hits || [])
.map((hit) => hit._source?.agent.id ?? '')
.filter((id) => !!id);
if (agentIds.length > 0) {
this.logger?.debug(
`Checking to see if the following agent ids are valid for current space:\n${agentIds.join(
'\n'
)}`
);
await this.fleetServices.ensureInCurrentSpace({ agentIds });
}
}
/**
* Retrieve a single endpoint host metadata. Note that the return endpoint document, if found,
* could be associated with a Fleet Agent that is no longer active. If wanting to ensure the
@ -77,6 +99,9 @@ export class EndpointMetadataService {
async getHostMetadata(endpointId: string): Promise<HostMetadata> {
const query = getESQueryHostMetadataByID(endpointId);
const queryResult = await this.esClient.search<HostMetadata>(query).catch(catchAndWrapError);
await this.ensureDataValidForSpace(queryResult);
const endpointMetadata = queryResponseToHostResult(queryResult).result;
if (endpointMetadata) {
@ -100,6 +125,8 @@ export class EndpointMetadataService {
.search<HostMetadata>(query, { ignore: [404] })
.catch(catchAndWrapError);
await this.ensureDataValidForSpace(searchResult);
return queryResponseToHostListResult(searchResult).resultList;
}
@ -335,6 +362,9 @@ export class EndpointMetadataService {
unitedMetadataQueryResponse = await this.esClient.search<UnitedAgentMetadataPersistedData>(
unitedIndexQuery
);
// FYI: we don't need to run the ES search response through `this.ensureDataValidForSpace()` because
// the query (`unitedIndexQuery`) above already included a filter with all of the valid policy ids
// for the current space - thus data is already coped to the space
} catch (error) {
const errorType = error?.meta?.body?.error?.type ?? '';
if (errorType === 'index_not_found_exception') {
@ -389,7 +419,6 @@ export class EndpointMetadataService {
const agentPolicy = agentPoliciesMap[_agent.policy_id!];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const endpointPolicy = endpointPoliciesMap[_agent.policy_id!];
const runtimeFields: Partial<typeof _agent> = {
status: doc?.fields?.status?.[0],
last_checkin: doc?.fields?.last_checkin?.[0],
@ -415,10 +444,10 @@ export class EndpointMetadataService {
async getMetadataForEndpoints(endpointIDs: string[]): Promise<HostMetadata[]> {
const query = getESQueryHostMetadataByIDs(endpointIDs);
const { body } = await this.esClient.search<HostMetadata>(query, {
meta: true,
});
const hosts = queryResponseToHostListResult(body);
return hosts.resultList;
const searchResult = await this.esClient.search<HostMetadata>(query).catch(catchAndWrapError);
await this.ensureDataValidForSpace(searchResult);
return queryResponseToHostListResult(searchResult).resultList;
}
}

View file

@ -9,11 +9,25 @@ import type { SavedObjectsServiceStart } from '@kbn/core/server';
import { coreMock, type ElasticsearchClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import type { createPackagePolicyServiceMock } from '@kbn/fleet-plugin/server/mocks';
import type { AgentPolicyServiceInterface, AgentService } from '@kbn/fleet-plugin/server';
import type { Agent, GetAgentPoliciesResponseItem } from '@kbn/fleet-plugin/common';
import type {
PolicyData,
UnitedAgentMetadataPersistedData,
} from '../../../../common/endpoint/types';
import { FleetAgentPolicyGenerator } from '../../../../common/endpoint/data_generators/fleet_agent_policy_generator';
import { FleetAgentGenerator } from '../../../../common/endpoint/data_generators/fleet_agent_generator';
import { FleetPackagePolicyGenerator } from '../../../../common/endpoint/data_generators/fleet_package_policy_generator';
import { applyEsClientSearchMock } from '../../mocks/utils.mock';
import type { EndpointInternalFleetServicesInterfaceMocked } from '../fleet/endpoint_fleet_services_factory.mocks';
import { createEndpointFleetServicesFactoryMock } from '../fleet/endpoint_fleet_services_factory.mocks';
import { createMockEndpointAppContextServiceStartContract } from '../../mocks';
import { EndpointMetadataService } from './endpoint_metadata_service';
import type { EndpointInternalFleetServicesInterface } from '../fleet/endpoint_fleet_services_factory';
import { SavedObjectsClientFactory } from '../saved_objects';
import {
METADATA_UNITED_INDEX,
metadataCurrentIndexPattern,
} from '../../../../common/endpoint/constants';
import { EndpointMetadataGenerator } from '../../../../common/endpoint/data_generators/endpoint_metadata_generator';
/**
* Endpoint Metadata Service test context. Includes an instance of `EndpointMetadataService` along with the
@ -25,9 +39,10 @@ export interface EndpointMetadataServiceTestContextMock {
agentPolicyService: jest.Mocked<AgentPolicyServiceInterface>;
packagePolicyService: ReturnType<typeof createPackagePolicyServiceMock>;
endpointMetadataService: EndpointMetadataService;
fleetServices: EndpointInternalFleetServicesInterface;
fleetServices: EndpointInternalFleetServicesInterfaceMocked;
logger: ReturnType<ReturnType<typeof loggingSystemMock.create>['get']>;
esClient: ElasticsearchClientMock;
applyMetadataMocks: typeof applyMetadataMocks;
}
export const createEndpointMetadataServiceTestContextMock =
@ -64,12 +79,111 @@ export const createEndpointMetadataServiceTestContextMock =
agentService: {
asInternalUser: fleetServices.agent,
asScoped: jest.fn().mockReturnValue(fleetServices.agent),
asInternalScopedUser: jest.fn().mockReturnValue(fleetServices.agent),
},
agentPolicyService: fleetServices.agentPolicy,
packagePolicyService: fleetServices.packagePolicy,
logger,
endpointMetadataService,
fleetServices,
applyMetadataMocks,
esClient: esClient as ElasticsearchClientMock,
};
};
export interface ApplyMetadataMocksResponse {
unitedMetadata: UnitedAgentMetadataPersistedData;
integrationPolicies: PolicyData[];
agentPolicies: GetAgentPoliciesResponseItem[];
agents: Agent[];
}
/**
* Apply mocks to the various services used to retrieve metadata via the EndpointMetadataService.
* Returns the data that is used in the mocks, thus allowing manipulation of it before running the
* test.
* @param esClientMock
* @param fleetServices
*/
export const applyMetadataMocks = (
esClientMock: ElasticsearchClientMock,
fleetServices: EndpointInternalFleetServicesInterfaceMocked
): ApplyMetadataMocksResponse => {
const metadataGenerator = new EndpointMetadataGenerator('seed');
const fleetIntegrationPolicyGenerator = new FleetPackagePolicyGenerator('seed');
const fleetAgentGenerator = new FleetAgentGenerator('seed');
const fleetAgentPolicyGenerator = new FleetAgentPolicyGenerator('seed');
const unitedMetadata = metadataGenerator.generateUnitedAgentMetadata();
const integrationPolicies = [
fleetIntegrationPolicyGenerator.generateEndpointPackagePolicy({
id: unitedMetadata.united.endpoint.Endpoint.policy.applied.id,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
policy_ids: [unitedMetadata.united.agent.policy_id!],
}),
];
const agentPolicies = [
fleetAgentPolicyGenerator.generate({ id: unitedMetadata.united.agent.policy_id }),
];
const agents = [
fleetAgentGenerator.generate({
id: unitedMetadata.agent.id,
policy_id: agentPolicies[0].id,
}),
];
applyEsClientSearchMock({
esClientMock,
index: METADATA_UNITED_INDEX,
response: metadataGenerator.toEsSearchResponse([
metadataGenerator.toEsSearchHit(unitedMetadata, METADATA_UNITED_INDEX),
]),
});
applyEsClientSearchMock({
esClientMock,
index: metadataCurrentIndexPattern,
response: metadataGenerator.toEsSearchResponse([
metadataGenerator.toEsSearchHit(unitedMetadata.united.endpoint, metadataCurrentIndexPattern),
]),
});
fleetServices.packagePolicy.list.mockImplementation(async (_, { page = 1 }) => {
// FYI: need to implement returning an empty list of items after page 1 due to how
// `getAllEndpointPackagePolicies()` is currently looping through all policies
// See `x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/endpoint_package_policies.ts`
return {
items: page === 1 ? integrationPolicies : [],
page: 1,
total: 1,
perPage: 20,
};
});
fleetServices.packagePolicy.get.mockImplementation(async () => {
return integrationPolicies[0];
});
fleetServices.agentPolicy.getByIds.mockImplementation(async () => {
return agentPolicies;
});
fleetServices.agentPolicy.get.mockImplementation(async () => {
return agentPolicies[0];
});
fleetServices.agent.getByIds.mockImplementation(async () => {
return agents;
});
fleetServices.agent.getAgent.mockImplementation(async () => {
return agents[0];
});
return {
unitedMetadata,
integrationPolicies,
agentPolicies,
agents,
};
};

View file

@ -10,11 +10,10 @@ import TestAgent from 'supertest/lib/agent';
import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows';
export default function ({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const endpointDataStreamHelpers = getService('endpointDataStreamHelpers');
const utils = getService('securitySolutionUtils');
const endpointTestresources = getService('endpointTestResources');
describe('@ess @serverless Endpoint policy api', function () {
describe('@ess @serverless Endpoint policy response api', function () {
let adminSupertest: TestAgent;
before(async () => {
@ -22,19 +21,24 @@ export default function ({ getService }: FtrProviderContext) {
});
describe('GET /api/endpoint/policy_response', () => {
before(
async () =>
await esArchiver.load('x-pack/test/functional/es_archives/endpoint/policy', {
useCreate: true,
})
);
let mockData: Awaited<ReturnType<typeof endpointTestresources.loadEndpointData>>;
before(async () => {
mockData = await endpointTestresources.loadEndpointData();
});
// 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 endpointDataStreamHelpers.deletePolicyStream(getService));
after(async () => {
if (mockData) {
await endpointTestresources.unloadEndpointData(mockData);
// @ts-expect-error
mockData = undefined;
}
});
it('should return one policy response for an id', async () => {
const expectedAgentId = 'a10ac658-a3bc-4ac6-944a-68d9bd1c5a5e';
const expectedAgentId = mockData.hosts[0].agent.id;
const { body } = await adminSupertest
.get(`/api/endpoint/policy_response?agentId=${expectedAgentId}`)
.send()
@ -50,7 +54,7 @@ export default function ({ getService }: FtrProviderContext) {
.send()
.expect(404);
expect(body.message).to.contain('Policy Response Not Found');
expect(body.message).to.contain('Policy response for endpoint id [bad_id] not found');
});
});
});

View file

@ -0,0 +1,48 @@
/*
* 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 { FtrConfigProviderContext } from '@kbn/test';
import type { ExperimentalFeatures as SecuritySolutionExperimentalFeatures } from '@kbn/security-solution-plugin/common';
import type { ExperimentalFeatures as FleetExperimentalFeatures } from '@kbn/fleet-plugin/common/experimental_features';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const functionalConfig = await readConfigFile(
require.resolve('../../../../../config/ess/config.base.edr_workflows.trial')
);
const securitySolutionEnableExperimental: Array<keyof SecuritySolutionExperimentalFeatures> = [
'endpointManagementSpaceAwarenessEnabled',
];
const fleetEnableExperimental: Array<keyof FleetExperimentalFeatures> = ['useSpaceAwareness'];
return {
...functionalConfig.getAll(),
testFiles: [require.resolve('..')],
junit: {
reportName: 'EDR Workflows - Space Awareness Integration Tests - ESS Env - Trial License',
},
kbnTestServer: {
...functionalConfig.get('kbnTestServer'),
serverArgs: [
...functionalConfig.get('kbnTestServer.serverArgs').filter(
// Exclude Fleet and Security solution experimental features
// properties since we are overriding them here
(arg: string) =>
!arg.includes('xpack.fleet.enableExperimental') &&
!arg.includes('xpack.securitySolution.enableExperimental')
),
// FLEET: set any experimental feature flags for testing
`--xpack.fleet.enableExperimental=${JSON.stringify(fleetEnableExperimental)}`,
// SECURITY SOLUTION: set any experimental feature flags for testing
`--xpack.securitySolution.enableExperimental=${JSON.stringify(
securitySolutionEnableExperimental
)}`,
],
},
};
}

View file

@ -0,0 +1,51 @@
/*
* 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 { FtrConfigProviderContext } from '@kbn/test';
import type { ExperimentalFeatures as SecuritySolutionExperimentalFeatures } from '@kbn/security-solution-plugin/common';
import type { ExperimentalFeatures as FleetExperimentalFeatures } from '@kbn/fleet-plugin/common/experimental_features';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const functionalConfig = await readConfigFile(
require.resolve('../../../../../config/serverless/config.base.edr_workflows')
);
const securitySolutionEnableExperimental: Array<keyof SecuritySolutionExperimentalFeatures> = [
'endpointManagementSpaceAwarenessEnabled',
];
const fleetEnableExperimental: Array<keyof FleetExperimentalFeatures> = ['useSpaceAwareness'];
return {
...functionalConfig.getAll(),
testFiles: [require.resolve('..')],
junit: {
reportName: 'EDR Workflows - Space Awareness Integration Tests - Serverless Env - Complete',
},
kbnTestServer: {
...functionalConfig.get('kbnTestServer'),
serverArgs: [
...functionalConfig.get('kbnTestServer.serverArgs').filter(
// Exclude Fleet and Security solution experimental features
// properties since we are overriding them here
(arg: string) =>
!arg.includes('xpack.fleet.enableExperimental') &&
!arg.includes('xpack.securitySolution.enableExperimental')
),
// FLEET: set any experimental feature flags for testing
`--xpack.fleet.enableExperimental=${JSON.stringify(fleetEnableExperimental)}`,
// SECURITY SOLUTION: set any experimental feature flags for testing
`--xpack.securitySolution.enableExperimental=${JSON.stringify(
securitySolutionEnableExperimental
)}`,
// Enable spaces UI capabilities
'--xpack.spaces.maxSpaces=100',
],
},
};
}

View file

@ -0,0 +1,61 @@
/*
* 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 { getRegistryUrl as getRegistryUrlFromIngest } from '@kbn/fleet-plugin/server';
import { isServerlessKibanaFlavor } from '@kbn/security-solution-plugin/common/endpoint/utils/kibana_status';
import { enableFleetSpaceAwareness } from '@kbn/security-solution-plugin/scripts/endpoint/common/fleet_services';
import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows';
import { ROLE } from '../../../../config/services/security_solution_edr_workflows_roles_users';
export default function endpointAPIIntegrationTests(providerContext: FtrProviderContext) {
const { loadTestFile, getService } = providerContext;
describe('Endpoint plugin spaces support', function () {
const ingestManager = getService('ingestManager');
const rolesUsersProvider = getService('rolesUsersProvider');
const kbnClient = getService('kibanaServer');
const log = getService('log');
const endpointRegistryHelpers = getService('endpointRegistryHelpers');
const roles = Object.values(ROLE);
before(async () => {
if (!endpointRegistryHelpers.isRegistryEnabled()) {
log.warning('These tests are being run with an external package registry');
}
const registryUrl =
endpointRegistryHelpers.getRegistryUrlFromTestEnv() ?? getRegistryUrlFromIngest();
log.info(`Package registry URL for tests: ${registryUrl}`);
try {
await ingestManager.setup();
} catch (err) {
log.warning(`Error setting up ingestManager: ${err}`);
}
if (!(await isServerlessKibanaFlavor(kbnClient))) {
// create role/user
for (const role of roles) {
await rolesUsersProvider.createRole({ predefinedRole: role });
await rolesUsersProvider.createUser({ name: role, roles: [role] });
}
}
// Enable fleet space awareness
log.info('Enabling Fleet space awareness');
await enableFleetSpaceAwareness(kbnClient);
});
after(async () => {
if (!(await isServerlessKibanaFlavor(kbnClient))) {
// delete role/user
await rolesUsersProvider.deleteUsers(roles);
await rolesUsersProvider.deleteRoles(roles);
}
});
loadTestFile(require.resolve('./space_awareness'));
});
}

View file

@ -0,0 +1,190 @@
/*
* 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 TestAgent from 'supertest/lib/agent';
import { ensureSpaceIdExists } from '@kbn/security-solution-plugin/scripts/endpoint/common/spaces';
import { addSpaceIdToPath } from '@kbn/spaces-plugin/common';
import expect from '@kbn/expect';
import {
AGENT_STATUS_ROUTE,
BASE_POLICY_RESPONSE_ROUTE,
HOST_METADATA_GET_ROUTE,
HOST_METADATA_LIST_ROUTE,
} from '@kbn/security-solution-plugin/common/endpoint/constants';
import { createSupertestErrorLogger } from '../../utils';
import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows';
export default function ({ getService }: FtrProviderContext) {
const utils = getService('securitySolutionUtils');
const endpointTestresources = getService('endpointTestResources');
const kbnServer = getService('kibanaServer');
const log = getService('log');
describe('@ess @serverless Endpoint management space awareness support', function () {
let adminSupertest: TestAgent;
let dataSpaceA: Awaited<ReturnType<typeof endpointTestresources.loadEndpointData>>;
let dataSpaceB: Awaited<ReturnType<typeof endpointTestresources.loadEndpointData>>;
before(async () => {
adminSupertest = await utils.createSuperTest();
await Promise.all([
ensureSpaceIdExists(kbnServer, 'space_a', { log }),
ensureSpaceIdExists(kbnServer, 'space_b', { log }),
]);
dataSpaceA = await endpointTestresources.loadEndpointData({
spaceId: 'space_a',
generatorSeed: Math.random().toString(32),
});
dataSpaceB = await endpointTestresources.loadEndpointData({
spaceId: 'space_b',
generatorSeed: Math.random().toString(32),
});
log.verbose(
`mocked data loaded:\nSPACE A:\n${JSON.stringify(
dataSpaceA,
null,
2
)}\nSPACE B:\n${JSON.stringify(dataSpaceB, null, 2)}`
);
});
// 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 () => {
if (dataSpaceA) {
await dataSpaceA.unloadEndpointData();
// @ts-expect-error
dataSpaceA = undefined;
}
if (dataSpaceB) {
await dataSpaceB.unloadEndpointData();
// @ts-expect-error
dataSpaceB = undefined;
}
});
describe(`Policy Response API: ${BASE_POLICY_RESPONSE_ROUTE}`, () => {
it('should return policy response in space', async () => {
const { body } = await adminSupertest
.get(
addSpaceIdToPath(
'/',
dataSpaceA.spaceId,
`/api/endpoint/policy_response?agentId=${dataSpaceA.hosts[0].agent.id}`
)
)
.on('error', createSupertestErrorLogger(log))
.send()
.expect(200);
expect(body.policy_response.agent.id).to.eql(dataSpaceA.hosts[0].agent.id);
});
it('should return not found for a host policy response not in current space', async () => {
await adminSupertest
.get(
addSpaceIdToPath(
'/',
dataSpaceA.spaceId,
`/api/endpoint/policy_response?agentId=${dataSpaceB.hosts[0].agent.id}`
)
)
.on('error', createSupertestErrorLogger(log).ignoreCodes([404]))
.send()
.expect(404);
});
});
describe(`Host Metadata List API: ${HOST_METADATA_LIST_ROUTE}`, () => {
it('should retrieve list with only metadata for hosts in current space', async () => {
const { body } = await adminSupertest
.get(addSpaceIdToPath('/', dataSpaceA.spaceId, HOST_METADATA_LIST_ROUTE))
.on('error', createSupertestErrorLogger(log))
.send()
.expect(200);
expect(body.total).to.eql(1);
expect(body.data[0].metadata.agent.id).to.eql(dataSpaceA.hosts[0].agent.id);
});
it('should not return host data from other spaces when using kuery value', async () => {
const { body } = await adminSupertest
.get(addSpaceIdToPath('/', dataSpaceA.spaceId, HOST_METADATA_LIST_ROUTE))
.on('error', createSupertestErrorLogger(log))
.query({
kuery: `united.endpoint.agent.id: "${dataSpaceB.hosts[0].agent.id}"`,
})
.send()
.expect(200);
expect(body.total).to.eql(0);
});
});
describe(`Host Details Metadata API: ${HOST_METADATA_GET_ROUTE}`, () => {
it('should retrieve metadata details for agent id in space', async () => {
await adminSupertest
.get(
addSpaceIdToPath(
'/',
dataSpaceA.spaceId,
HOST_METADATA_GET_ROUTE.replace('{id}', dataSpaceA.hosts[0].agent.id)
)
)
.on('error', createSupertestErrorLogger(log))
.send()
.expect(200);
});
it('should NOT return metadata details for agent id that is not in current space', async () => {
await adminSupertest
.get(
addSpaceIdToPath(
'/',
dataSpaceA.spaceId,
HOST_METADATA_GET_ROUTE.replace('{id}', dataSpaceB.hosts[0].agent.id)
)
)
.on('error', createSupertestErrorLogger(log).ignoreCodes([404]))
.send()
.expect(404);
});
});
describe(`Agent Status API: ${AGENT_STATUS_ROUTE}`, () => {
it('should return status for an agent in current space', async () => {
const { body } = await adminSupertest
.get(addSpaceIdToPath('/', dataSpaceA.spaceId, AGENT_STATUS_ROUTE))
.query({ agentIds: [dataSpaceA.hosts[0].agent.id] })
.set('elastic-api-version', '1')
.set('x-elastic-internal-origin', 'kibana')
.on('error', createSupertestErrorLogger(log))
.send()
.expect(200);
expect(body.data[dataSpaceA.hosts[0].agent.id].found).to.eql(true);
});
it('should NOT return status for an agent that is not in current space', async () => {
const { body } = await adminSupertest
.get(addSpaceIdToPath('/', dataSpaceA.spaceId, AGENT_STATUS_ROUTE))
.query({ agentIds: [dataSpaceB.hosts[0].agent.id] })
.set('elastic-api-version', '1')
.set('x-elastic-internal-origin', 'kibana')
.on('error', createSupertestErrorLogger(log))
.send()
.expect(200);
expect(body.data[dataSpaceB.hosts[0].agent.id].found).to.eql(false);
});
});
});
}

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export * from './supertest_error_logger';

View file

@ -0,0 +1,73 @@
/*
* 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 SuperTest from 'supertest';
import { ToolingLog } from '@kbn/tooling-log';
export interface LogErrorDetailsInterface {
(this: SuperTest.Test, err: Error & { response?: any }): SuperTest.Test;
ignoreCodes: (
codes: number[]
) => (this: SuperTest.Test, err: Error & { response?: SuperTest.Response }) => SuperTest.Test;
}
/**
* Creates a logger that can be used with `supertest` to log details around errors
*
* @param log
*
* @example
* const errorLogger = createSupertestErrorLogger(log);
*
* supertestWithoutAuth
* .post(`some/url`)
* .on('error', errorLogger) //<< Add logger to `error` event
* .send({})
*
* // Ignore 404
* supertestWithoutAuth
* .post(`some/url`)
* .on('error', errorLogger.ignoreCodes([404]) //<< Add logger to `error` event and ignore 404
* .send({})
*/
export const createSupertestErrorLogger = (log: ToolingLog): LogErrorDetailsInterface => {
/**
* Utility for use with `supertest` that logs errors with details returned by the API
* @param err
*/
const logErrorDetails: LogErrorDetailsInterface = function (err) {
if (err.response && (err.response.body || err.response.text)) {
let outputData =
'RESPONSE:\n' + err.response.body
? JSON.stringify(err.response.body, null, 2)
: err.response.text;
if (err.response.request) {
const { url = '', method = '', _data = '' } = err.response.request;
outputData += `\nREQUEST:
${method} ${url}
${JSON.stringify(_data, null, 2)}
`;
}
log.error(outputData);
}
return this ?? err;
};
logErrorDetails.ignoreCodes = (codes) => {
return function (err) {
if (err.response && err.response.status && !codes.includes(err.response.status)) {
return logErrorDetails.call(this, err);
}
return this;
};
};
return logErrorDetails;
};

View file

@ -50,5 +50,6 @@
"@kbn/search-types",
"@kbn/security-plugin",
"@kbn/ftr-common-functional-ui-services",
"@kbn/spaces-plugin",
]
}

View file

@ -21,6 +21,7 @@ import {
} from '@kbn/security-solution-plugin/common/endpoint/constants';
import {
deleteIndexedHostsAndAlerts,
DeleteIndexedHostsAndAlertsResponse,
IndexedHostsAndAlertsResponse,
indexHostsAndAlerts,
} from '@kbn/security-solution-plugin/common/endpoint/index_data';
@ -40,11 +41,17 @@ import seedrandom from 'seedrandom';
import { fetchFleetLatestAvailableAgentVersion } from '@kbn/security-solution-plugin/common/endpoint/utils/fetch_fleet_version';
import { KbnClient } from '@kbn/test';
import { isServerlessKibanaFlavor } from '@kbn/security-solution-plugin/common/endpoint/utils/kibana_status';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { createKbnClient } from '@kbn/security-solution-plugin/scripts/endpoint/common/stack_services';
import { FtrService } from '../../functional/ftr_provider_context';
export type IndexedHostsAndAlertsResponseExtended = IndexedHostsAndAlertsResponse & {
unloadEndpointData(): Promise<DeleteIndexedHostsAndAlertsResponse>;
spaceId: string;
};
// Document Generator override that uses a custom Endpoint Metadata generator and sets the
// `agent.version` to the current version
const createDocGeneratorClass = async (kbnClient: KbnClient, isServerless: boolean) => {
let version = kibanaPackageJson.version;
if (isServerless) {
@ -74,6 +81,26 @@ export class EndpointTestResources extends FtrService {
private readonly supertest = this.ctx.getService('supertest');
private readonly log = this.ctx.getService('log');
public getScopedKbnClient(spaceId: string = DEFAULT_SPACE_ID): KbnClient {
if (!spaceId || spaceId === DEFAULT_SPACE_ID) {
return this.kbnClient;
}
const kbnClientOptions: Parameters<typeof createKbnClient>[0] = {
url: this.kbnClient.resolveUrl('/'),
username: this.config.get('servers.elasticsearch.username'),
password: this.config.get('servers.elasticsearch.password'),
spaceId,
};
this.log.info(`creating new KbnClient with:\n${JSON.stringify(kbnClientOptions, null, 2)}`);
// Was not included above in order to keep the output of the log.info() above clean in the output
kbnClientOptions.log = this.log;
return createKbnClient(kbnClientOptions);
}
async stopTransform(transformId: string) {
const stopRequest = {
transform_id: `${transformId}*`,
@ -120,8 +147,9 @@ export class EndpointTestResources extends FtrService {
waitUntilTransformed: boolean;
waitTimeout: number;
customIndexFn: () => Promise<IndexedHostsAndAlertsResponse>;
spaceId: string;
}> = {}
): Promise<IndexedHostsAndAlertsResponse> {
): Promise<IndexedHostsAndAlertsResponseExtended> {
const {
numHosts = 1,
numHostDocs = 1,
@ -131,12 +159,16 @@ export class EndpointTestResources extends FtrService {
waitUntilTransformed = true,
waitTimeout = 120000,
customIndexFn,
spaceId = DEFAULT_SPACE_ID,
} = options;
const kbnClient = this.getScopedKbnClient(spaceId);
let currentTransformName = metadataTransformPrefix;
let unitedTransformName = METADATA_UNITED_TRANSFORM;
if (waitUntilTransformed && customIndexFn) {
const endpointPackage = await getEndpointPackageInfo(this.kbnClient);
const endpointPackage = await getEndpointPackageInfo(kbnClient);
const isV2 = isEndpointPackageV2(endpointPackage.version);
if (isV2) {
@ -152,18 +184,15 @@ export class EndpointTestResources extends FtrService {
await this.stopTransform(unitedTransformName);
}
const isServerless = await isServerlessKibanaFlavor(this.kbnClient);
const CurrentKibanaVersionDocGenerator = await createDocGeneratorClass(
this.kbnClient,
isServerless
);
const isServerless = await isServerlessKibanaFlavor(kbnClient);
const CurrentKibanaVersionDocGenerator = await createDocGeneratorClass(kbnClient, isServerless);
// load data into the system
const indexedData = customIndexFn
? await customIndexFn()
: await indexHostsAndAlerts(
this.esClient as Client,
this.kbnClient,
kbnClient,
generatorSeed,
numHosts,
numHostDocs,
@ -194,15 +223,29 @@ export class EndpointTestResources extends FtrService {
await this.waitForUnitedEndpoints(agentIds, waitTimeout);
}
return indexedData;
return {
...indexedData,
spaceId,
unloadEndpointData: (): Promise<DeleteIndexedHostsAndAlertsResponse> => {
return this.unloadEndpointData(indexedData, { spaceId });
},
};
}
/**
* Deletes the loaded data created via `loadEndpointData()`
* @param indexedData
* @param options
*/
async unloadEndpointData(indexedData: IndexedHostsAndAlertsResponse) {
return deleteIndexedHostsAndAlerts(this.esClient as Client, this.kbnClient, indexedData);
async unloadEndpointData(
indexedData: IndexedHostsAndAlertsResponse,
{ spaceId = DEFAULT_SPACE_ID }: { spaceId?: string } = {}
): Promise<DeleteIndexedHostsAndAlertsResponse> {
return deleteIndexedHostsAndAlerts(
this.esClient as Client,
this.getScopedKbnClient(spaceId),
indexedData
);
}
private async waitForIndex(
@ -315,10 +358,10 @@ export class EndpointTestResources extends FtrService {
* installs (or upgrades) the Endpoint Fleet package
* (NOTE: ensure that fleet is setup first before calling this function)
*/
async installOrUpgradeEndpointFleetPackage(): ReturnType<
typeof installOrUpgradeEndpointFleetPackage
> {
return installOrUpgradeEndpointFleetPackage(this.kbnClient, this.log);
async installOrUpgradeEndpointFleetPackage(
spaceId: string = DEFAULT_SPACE_ID
): ReturnType<typeof installOrUpgradeEndpointFleetPackage> {
return installOrUpgradeEndpointFleetPackage(this.getScopedKbnClient(spaceId), this.log);
}
/**
@ -383,8 +426,8 @@ export class EndpointTestResources extends FtrService {
return response;
}
async isEndpointPackageV2(): Promise<boolean> {
const endpointPackage = await getEndpointPackageInfo(this.kbnClient);
async isEndpointPackageV2(spaceId: string = DEFAULT_SPACE_ID): Promise<boolean> {
const endpointPackage = await getEndpointPackageInfo(this.getScopedKbnClient(spaceId));
return isEndpointPackageV2(endpointPackage.version);
}
}

View file

@ -28,5 +28,6 @@
"@kbn/test",
"@kbn/test-subj-selector",
"@kbn/ftr-common-functional-services",
"@kbn/spaces-plugin",
]
}