mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
8cfa396f46
commit
2a786b88a1
48 changed files with 1566 additions and 246 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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()),
|
||||
});
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -538,6 +538,7 @@ export interface HostMetadataInterface {
|
|||
status: EndpointStatus;
|
||||
policy: {
|
||||
applied: {
|
||||
/** The Endpoint integration policy UUID */
|
||||
id: string;
|
||||
status: HostPolicyResponseActionStatus;
|
||||
name: string;
|
||||
|
|
|
@ -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)}`);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>;
|
||||
};
|
||||
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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' });
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
@ -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'] });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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 },
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
Binary file not shown.
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
)}`,
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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',
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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';
|
|
@ -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;
|
||||
};
|
|
@ -50,5 +50,6 @@
|
|||
"@kbn/search-types",
|
||||
"@kbn/security-plugin",
|
||||
"@kbn/ftr-common-functional-ui-services",
|
||||
"@kbn/spaces-plugin",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,5 +28,6 @@
|
|||
"@kbn/test",
|
||||
"@kbn/test-subj-selector",
|
||||
"@kbn/ftr-common-functional-services",
|
||||
"@kbn/spaces-plugin",
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue