[Security Solution][Roles] Add Read-Only Endpoint indexes to the (Detections) role creation scripts + FTR improvements (#107086)
* Add needed indexes to the role scripts * Moved/renamed detection engine roles and user utility to `x-pack/test/common/security_solution` * removed duplicate code in rule_registry and instead exported same methods from `common/services/security_solution` * new endpoint FTR service that includes methods for loading and unloading data (uses existing data indexer methods) * Transforms: Added additional methods to the FTR Test service
This commit is contained in:
parent
2230c032c6
commit
a3119a5541
|
@ -7,10 +7,19 @@
|
||||||
|
|
||||||
export const eventsIndexPattern = 'logs-endpoint.events.*';
|
export const eventsIndexPattern = 'logs-endpoint.events.*';
|
||||||
export const alertsIndexPattern = 'logs-endpoint.alerts-*';
|
export const alertsIndexPattern = 'logs-endpoint.alerts-*';
|
||||||
|
|
||||||
|
/** index pattern for the data source index (data stream) that the Endpoint streams documents to */
|
||||||
export const metadataIndexPattern = 'metrics-endpoint.metadata-*';
|
export const metadataIndexPattern = 'metrics-endpoint.metadata-*';
|
||||||
|
|
||||||
|
/** index that the metadata transform writes to (destination) and that is used by endpoint APIs */
|
||||||
export const metadataCurrentIndexPattern = 'metrics-endpoint.metadata_current_*';
|
export const metadataCurrentIndexPattern = 'metrics-endpoint.metadata_current_*';
|
||||||
|
|
||||||
|
/** The metadata Transform Name prefix with NO (package) version) */
|
||||||
export const metadataTransformPrefix = 'endpoint.metadata_current-default';
|
export const metadataTransformPrefix = 'endpoint.metadata_current-default';
|
||||||
|
|
||||||
|
/** The metadata Transform Name prefix with NO namespace and NO (package) version) */
|
||||||
export const metadataTransformPattern = 'endpoint.metadata_current-*';
|
export const metadataTransformPattern = 'endpoint.metadata_current-*';
|
||||||
|
|
||||||
export const policyIndexPattern = 'metrics-endpoint.policy-*';
|
export const policyIndexPattern = 'metrics-endpoint.policy-*';
|
||||||
export const telemetryIndexPattern = 'metrics-endpoint.telemetry-*';
|
export const telemetryIndexPattern = 'metrics-endpoint.telemetry-*';
|
||||||
export const LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG = 'endpoint:limited-concurrency';
|
export const LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG = 'endpoint:limited-concurrency';
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
* 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 { Client } from '@elastic/elasticsearch';
|
||||||
|
import { EndpointDocGenerator, Event, TreeOptions } from '../generate_data';
|
||||||
|
import { firstNonNullValue } from '../models/ecs_safety_helpers';
|
||||||
|
|
||||||
|
function delay(ms: number) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indexes Alerts/Events into elasticsarch
|
||||||
|
*
|
||||||
|
* @param client
|
||||||
|
* @param eventIndex
|
||||||
|
* @param alertIndex
|
||||||
|
* @param generator
|
||||||
|
* @param numAlerts
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
export async function indexAlerts({
|
||||||
|
client,
|
||||||
|
eventIndex,
|
||||||
|
alertIndex,
|
||||||
|
generator,
|
||||||
|
numAlerts,
|
||||||
|
options = {},
|
||||||
|
}: {
|
||||||
|
client: Client;
|
||||||
|
eventIndex: string;
|
||||||
|
alertIndex: string;
|
||||||
|
generator: EndpointDocGenerator;
|
||||||
|
numAlerts: number;
|
||||||
|
options: TreeOptions;
|
||||||
|
}) {
|
||||||
|
const alertGenerator = generator.alertsGenerator(numAlerts, options);
|
||||||
|
let result = alertGenerator.next();
|
||||||
|
while (!result.done) {
|
||||||
|
let k = 0;
|
||||||
|
const resolverDocs: Event[] = [];
|
||||||
|
while (k < 1000 && !result.done) {
|
||||||
|
resolverDocs.push(result.value);
|
||||||
|
result = alertGenerator.next();
|
||||||
|
k++;
|
||||||
|
}
|
||||||
|
const body = resolverDocs.reduce(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(array: Array<Record<string, any>>, doc) => {
|
||||||
|
let index = eventIndex;
|
||||||
|
if (firstNonNullValue(doc.event?.kind) === 'alert') {
|
||||||
|
index = alertIndex;
|
||||||
|
}
|
||||||
|
array.push({ create: { _index: index } }, doc);
|
||||||
|
return array;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
await client.bulk({ body, refresh: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.indices.refresh({
|
||||||
|
index: eventIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Unclear why the documents are not showing up after the call to refresh.
|
||||||
|
// Waiting 5 seconds allows the indices to refresh automatically and
|
||||||
|
// the documents become available in API/integration tests.
|
||||||
|
await delay(5000);
|
||||||
|
}
|
|
@ -0,0 +1,310 @@
|
||||||
|
/*
|
||||||
|
* 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 { Client } from '@elastic/elasticsearch';
|
||||||
|
import { cloneDeep, merge } from 'lodash';
|
||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
import { KbnClient } from '@kbn/test';
|
||||||
|
import { DeleteByQueryResponse } from '@elastic/elasticsearch/api/types';
|
||||||
|
import { Agent, CreatePackagePolicyResponse, GetPackagesResponse } from '../../../../fleet/common';
|
||||||
|
import { EndpointDocGenerator } from '../generate_data';
|
||||||
|
import { HostMetadata, HostPolicyResponse } from '../types';
|
||||||
|
import {
|
||||||
|
deleteIndexedFleetAgents,
|
||||||
|
DeleteIndexedFleetAgentsResponse,
|
||||||
|
IndexedFleetAgentResponse,
|
||||||
|
indexFleetAgentForHost,
|
||||||
|
} from './index_fleet_agent';
|
||||||
|
import {
|
||||||
|
deleteIndexedFleetActions,
|
||||||
|
DeleteIndexedFleetActionsResponse,
|
||||||
|
IndexedFleetActionsForHostResponse,
|
||||||
|
indexFleetActionsForHost,
|
||||||
|
} from './index_fleet_actions';
|
||||||
|
import {
|
||||||
|
deleteIndexedFleetEndpointPolicies,
|
||||||
|
DeleteIndexedFleetEndpointPoliciesResponse,
|
||||||
|
IndexedFleetEndpointPolicyResponse,
|
||||||
|
indexFleetEndpointPolicy,
|
||||||
|
} from './index_fleet_endpoint_policy';
|
||||||
|
import { metadataCurrentIndexPattern } from '../constants';
|
||||||
|
import { EndpointDataLoadingError, wrapErrorAndRejectPromise } from './utils';
|
||||||
|
|
||||||
|
export interface IndexedHostsResponse
|
||||||
|
extends IndexedFleetAgentResponse,
|
||||||
|
IndexedFleetActionsForHostResponse,
|
||||||
|
IndexedFleetEndpointPolicyResponse {
|
||||||
|
/**
|
||||||
|
* The documents (1 or more) that were generated for the (single) endpoint host.
|
||||||
|
* If consuming this data and wanting only the last one created, just access the
|
||||||
|
* last item in the array
|
||||||
|
*/
|
||||||
|
hosts: HostMetadata[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of Endpoint Policy Response documents (1 or more) that was created.
|
||||||
|
*/
|
||||||
|
policyResponses: HostPolicyResponse[];
|
||||||
|
|
||||||
|
metadataIndex: string;
|
||||||
|
policyResponseIndex: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indexes the requested number of documents for the endpoint host metadata currently being output by the generator.
|
||||||
|
* Endpoint Host metadata documents are added to an index that is set as "append only", thus one Endpoint host could
|
||||||
|
* have multiple documents in that index.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param numDocs
|
||||||
|
* @param client
|
||||||
|
* @param kbnClient
|
||||||
|
* @param realPolicies
|
||||||
|
* @param epmEndpointPackage
|
||||||
|
* @param metadataIndex
|
||||||
|
* @param policyResponseIndex
|
||||||
|
* @param enrollFleet
|
||||||
|
* @param generator
|
||||||
|
*/
|
||||||
|
export async function indexEndpointHostDocs({
|
||||||
|
numDocs,
|
||||||
|
client,
|
||||||
|
kbnClient,
|
||||||
|
realPolicies,
|
||||||
|
epmEndpointPackage,
|
||||||
|
metadataIndex,
|
||||||
|
policyResponseIndex,
|
||||||
|
enrollFleet,
|
||||||
|
generator,
|
||||||
|
}: {
|
||||||
|
numDocs: number;
|
||||||
|
client: Client;
|
||||||
|
kbnClient: KbnClient;
|
||||||
|
realPolicies: Record<string, CreatePackagePolicyResponse['item']>;
|
||||||
|
epmEndpointPackage: GetPackagesResponse['response'][0];
|
||||||
|
metadataIndex: string;
|
||||||
|
policyResponseIndex: string;
|
||||||
|
enrollFleet: boolean;
|
||||||
|
generator: EndpointDocGenerator;
|
||||||
|
}): Promise<IndexedHostsResponse> {
|
||||||
|
const timeBetweenDocs = 6 * 3600 * 1000; // 6 hours between metadata documents
|
||||||
|
const timestamp = new Date().getTime();
|
||||||
|
const kibanaVersion = await fetchKibanaVersion(kbnClient);
|
||||||
|
const response: IndexedHostsResponse = {
|
||||||
|
hosts: [],
|
||||||
|
agents: [],
|
||||||
|
policyResponses: [],
|
||||||
|
metadataIndex,
|
||||||
|
policyResponseIndex,
|
||||||
|
fleetAgentsIndex: '',
|
||||||
|
actionResponses: [],
|
||||||
|
responsesIndex: '',
|
||||||
|
actions: [],
|
||||||
|
actionsIndex: '',
|
||||||
|
integrationPolicies: [],
|
||||||
|
agentPolicies: [],
|
||||||
|
};
|
||||||
|
let hostMetadata: HostMetadata;
|
||||||
|
let wasAgentEnrolled = false;
|
||||||
|
let enrolledAgent: undefined | Agent;
|
||||||
|
|
||||||
|
for (let j = 0; j < numDocs; j++) {
|
||||||
|
generator.updateHostData();
|
||||||
|
generator.updateHostPolicyData();
|
||||||
|
|
||||||
|
hostMetadata = generator.generateHostMetadata(
|
||||||
|
timestamp - timeBetweenDocs * (numDocs - j - 1),
|
||||||
|
EndpointDocGenerator.createDataStreamFromIndex(metadataIndex)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (enrollFleet) {
|
||||||
|
const { id: appliedPolicyId, name: appliedPolicyName } = hostMetadata.Endpoint.policy.applied;
|
||||||
|
|
||||||
|
// If we don't yet have a "real" policy record, then create it now in ingest (package config)
|
||||||
|
if (!realPolicies[appliedPolicyId]) {
|
||||||
|
const createdPolicies = await indexFleetEndpointPolicy(
|
||||||
|
kbnClient,
|
||||||
|
appliedPolicyName,
|
||||||
|
epmEndpointPackage.version
|
||||||
|
);
|
||||||
|
|
||||||
|
merge(response, createdPolicies);
|
||||||
|
|
||||||
|
// eslint-disable-next-line require-atomic-updates
|
||||||
|
realPolicies[appliedPolicyId] = createdPolicies.integrationPolicies[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we did not yet enroll an agent for this Host, do it now that we have good policy id
|
||||||
|
if (!wasAgentEnrolled) {
|
||||||
|
wasAgentEnrolled = true;
|
||||||
|
|
||||||
|
const indexedAgentResponse = await indexFleetAgentForHost(
|
||||||
|
client,
|
||||||
|
kbnClient,
|
||||||
|
hostMetadata!,
|
||||||
|
realPolicies[appliedPolicyId].policy_id,
|
||||||
|
kibanaVersion
|
||||||
|
);
|
||||||
|
|
||||||
|
enrolledAgent = indexedAgentResponse.agents[0];
|
||||||
|
merge(response, indexedAgentResponse);
|
||||||
|
}
|
||||||
|
// Update the Host metadata record with the ID of the "real" policy along with the enrolled agent id
|
||||||
|
hostMetadata = {
|
||||||
|
...hostMetadata,
|
||||||
|
elastic: {
|
||||||
|
...hostMetadata.elastic,
|
||||||
|
agent: {
|
||||||
|
...hostMetadata.elastic.agent,
|
||||||
|
id: enrolledAgent?.id ?? hostMetadata.elastic.agent.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Endpoint: {
|
||||||
|
...hostMetadata.Endpoint,
|
||||||
|
policy: {
|
||||||
|
...hostMetadata.Endpoint.policy,
|
||||||
|
applied: {
|
||||||
|
...hostMetadata.Endpoint.policy.applied,
|
||||||
|
id: realPolicies[appliedPolicyId].id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create some actions for this Host
|
||||||
|
await indexFleetActionsForHost(client, hostMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
await client
|
||||||
|
.index({
|
||||||
|
index: metadataIndex,
|
||||||
|
body: hostMetadata,
|
||||||
|
op_type: 'create',
|
||||||
|
})
|
||||||
|
.catch(wrapErrorAndRejectPromise);
|
||||||
|
|
||||||
|
const hostPolicyResponse = generator.generatePolicyResponse({
|
||||||
|
ts: timestamp - timeBetweenDocs * (numDocs - j - 1),
|
||||||
|
policyDataStream: EndpointDocGenerator.createDataStreamFromIndex(policyResponseIndex),
|
||||||
|
});
|
||||||
|
|
||||||
|
await client
|
||||||
|
.index({
|
||||||
|
index: policyResponseIndex,
|
||||||
|
body: hostPolicyResponse,
|
||||||
|
op_type: 'create',
|
||||||
|
})
|
||||||
|
.catch(wrapErrorAndRejectPromise);
|
||||||
|
|
||||||
|
// Clone the hostMetadata and policyResponse document to ensure that no shared state
|
||||||
|
// (as a result of using the generator) is returned across docs.
|
||||||
|
response.hosts.push(cloneDeep(hostMetadata));
|
||||||
|
response.policyResponses.push(cloneDeep(hostPolicyResponse));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchKibanaVersion = async (kbnClient: KbnClient) => {
|
||||||
|
const version = ((await kbnClient.request({
|
||||||
|
path: '/api/status',
|
||||||
|
method: 'GET',
|
||||||
|
})) as AxiosResponse).data.version.number;
|
||||||
|
|
||||||
|
if (!version) {
|
||||||
|
throw new EndpointDataLoadingError('failed to get kibana version via `/api/status` api');
|
||||||
|
}
|
||||||
|
|
||||||
|
return version;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DeleteIndexedEndpointHostsResponse
|
||||||
|
extends DeleteIndexedFleetAgentsResponse,
|
||||||
|
DeleteIndexedFleetActionsResponse,
|
||||||
|
DeleteIndexedFleetEndpointPoliciesResponse {
|
||||||
|
hosts: DeleteByQueryResponse | undefined;
|
||||||
|
policyResponses: DeleteByQueryResponse | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteIndexedEndpointHosts = async (
|
||||||
|
esClient: Client,
|
||||||
|
kbnClient: KbnClient,
|
||||||
|
indexedData: IndexedHostsResponse
|
||||||
|
): Promise<DeleteIndexedEndpointHostsResponse> => {
|
||||||
|
const response: DeleteIndexedEndpointHostsResponse = {
|
||||||
|
hosts: undefined,
|
||||||
|
policyResponses: undefined,
|
||||||
|
agents: undefined,
|
||||||
|
responses: undefined,
|
||||||
|
actions: undefined,
|
||||||
|
integrationPolicies: undefined,
|
||||||
|
agentPolicies: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (indexedData.hosts.length) {
|
||||||
|
const body = {
|
||||||
|
query: {
|
||||||
|
bool: {
|
||||||
|
filter: [{ terms: { 'agent.id': indexedData.hosts.map((host) => host.agent.id) } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
response.hosts = (
|
||||||
|
await esClient
|
||||||
|
.deleteByQuery({
|
||||||
|
index: indexedData.metadataIndex,
|
||||||
|
wait_for_completion: true,
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
.catch(wrapErrorAndRejectPromise)
|
||||||
|
).body;
|
||||||
|
|
||||||
|
// Delete from the transform destination index
|
||||||
|
await esClient
|
||||||
|
.deleteByQuery({
|
||||||
|
index: metadataCurrentIndexPattern,
|
||||||
|
wait_for_completion: true,
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
.catch(wrapErrorAndRejectPromise);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indexedData.policyResponses.length) {
|
||||||
|
response.policyResponses = (
|
||||||
|
await esClient
|
||||||
|
.deleteByQuery({
|
||||||
|
index: indexedData.policyResponseIndex,
|
||||||
|
wait_for_completion: true,
|
||||||
|
body: {
|
||||||
|
query: {
|
||||||
|
bool: {
|
||||||
|
filter: [
|
||||||
|
{
|
||||||
|
terms: {
|
||||||
|
'agent.id': indexedData.policyResponses.map(
|
||||||
|
(policyResponse) => policyResponse.agent.id
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch(wrapErrorAndRejectPromise)
|
||||||
|
).body;
|
||||||
|
}
|
||||||
|
|
||||||
|
merge(response, await deleteIndexedFleetAgents(esClient, indexedData));
|
||||||
|
merge(response, await deleteIndexedFleetActions(esClient, indexedData));
|
||||||
|
merge(response, await deleteIndexedFleetEndpointPolicies(kbnClient, indexedData));
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
|
@ -0,0 +1,218 @@
|
||||||
|
/*
|
||||||
|
* 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 { Client } from '@elastic/elasticsearch';
|
||||||
|
import { DeleteByQueryResponse } from '@elastic/elasticsearch/api/types';
|
||||||
|
import { EndpointAction, EndpointActionResponse, HostMetadata } from '../types';
|
||||||
|
import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '../../../../fleet/common';
|
||||||
|
import { FleetActionGenerator } from '../data_generators/fleet_action_generator';
|
||||||
|
import { wrapErrorAndRejectPromise } from './utils';
|
||||||
|
|
||||||
|
const defaultFleetActionGenerator = new FleetActionGenerator();
|
||||||
|
|
||||||
|
export interface IndexedFleetActionsForHostResponse {
|
||||||
|
actions: EndpointAction[];
|
||||||
|
actionResponses: EndpointActionResponse[];
|
||||||
|
actionsIndex: string;
|
||||||
|
responsesIndex: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indexes a randome number of Endpoint (via Fleet) Actions for a given host
|
||||||
|
* (NOTE: ensure that fleet is setup first before calling this loading function)
|
||||||
|
*
|
||||||
|
* @param esClient
|
||||||
|
* @param endpointHost
|
||||||
|
* @param [fleetActionGenerator]
|
||||||
|
*/
|
||||||
|
export const indexFleetActionsForHost = async (
|
||||||
|
esClient: Client,
|
||||||
|
endpointHost: HostMetadata,
|
||||||
|
fleetActionGenerator: FleetActionGenerator = defaultFleetActionGenerator
|
||||||
|
): Promise<IndexedFleetActionsForHostResponse> => {
|
||||||
|
const ES_INDEX_OPTIONS = { headers: { 'X-elastic-product-origin': 'fleet' } };
|
||||||
|
const agentId = endpointHost.elastic.agent.id;
|
||||||
|
const total = fleetActionGenerator.randomN(5);
|
||||||
|
const response: IndexedFleetActionsForHostResponse = {
|
||||||
|
actions: [],
|
||||||
|
actionResponses: [],
|
||||||
|
actionsIndex: AGENT_ACTIONS_INDEX,
|
||||||
|
responsesIndex: AGENT_ACTIONS_RESULTS_INDEX,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < total; i++) {
|
||||||
|
// create an action
|
||||||
|
const action = fleetActionGenerator.generate({
|
||||||
|
data: { comment: 'data generator: this host is bad' },
|
||||||
|
});
|
||||||
|
|
||||||
|
action.agents = [agentId];
|
||||||
|
|
||||||
|
esClient
|
||||||
|
.index(
|
||||||
|
{
|
||||||
|
index: AGENT_ACTIONS_INDEX,
|
||||||
|
body: action,
|
||||||
|
},
|
||||||
|
ES_INDEX_OPTIONS
|
||||||
|
)
|
||||||
|
.catch(wrapErrorAndRejectPromise);
|
||||||
|
|
||||||
|
// Create an action response for the above
|
||||||
|
const actionResponse = fleetActionGenerator.generateResponse({
|
||||||
|
action_id: action.action_id,
|
||||||
|
agent_id: agentId,
|
||||||
|
action_data: action.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
esClient
|
||||||
|
.index(
|
||||||
|
{
|
||||||
|
index: AGENT_ACTIONS_RESULTS_INDEX,
|
||||||
|
body: actionResponse,
|
||||||
|
},
|
||||||
|
ES_INDEX_OPTIONS
|
||||||
|
)
|
||||||
|
.catch(wrapErrorAndRejectPromise);
|
||||||
|
|
||||||
|
response.actions.push(action);
|
||||||
|
response.actionResponses.push(actionResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add edge cases (maybe)
|
||||||
|
if (fleetActionGenerator.randomFloat() < 0.3) {
|
||||||
|
const randomFloat = fleetActionGenerator.randomFloat();
|
||||||
|
|
||||||
|
// 60% of the time just add either an Isolate -OR- an UnIsolate action
|
||||||
|
if (randomFloat < 0.6) {
|
||||||
|
let action: EndpointAction;
|
||||||
|
|
||||||
|
if (randomFloat < 0.3) {
|
||||||
|
// add a pending isolation
|
||||||
|
action = fleetActionGenerator.generateIsolateAction({
|
||||||
|
'@timestamp': new Date().toISOString(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// add a pending UN-isolation
|
||||||
|
action = fleetActionGenerator.generateUnIsolateAction({
|
||||||
|
'@timestamp': new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
action.agents = [agentId];
|
||||||
|
|
||||||
|
await esClient
|
||||||
|
.index(
|
||||||
|
{
|
||||||
|
index: AGENT_ACTIONS_INDEX,
|
||||||
|
body: action,
|
||||||
|
},
|
||||||
|
ES_INDEX_OPTIONS
|
||||||
|
)
|
||||||
|
.catch(wrapErrorAndRejectPromise);
|
||||||
|
|
||||||
|
response.actions.push(action);
|
||||||
|
} else {
|
||||||
|
// Else (40% of the time) add a pending isolate AND pending un-isolate
|
||||||
|
const action1 = fleetActionGenerator.generateIsolateAction({
|
||||||
|
'@timestamp': new Date().toISOString(),
|
||||||
|
});
|
||||||
|
const action2 = fleetActionGenerator.generateUnIsolateAction({
|
||||||
|
'@timestamp': new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
action1.agents = [agentId];
|
||||||
|
action2.agents = [agentId];
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
esClient
|
||||||
|
.index(
|
||||||
|
{
|
||||||
|
index: AGENT_ACTIONS_INDEX,
|
||||||
|
body: action1,
|
||||||
|
},
|
||||||
|
ES_INDEX_OPTIONS
|
||||||
|
)
|
||||||
|
.catch(wrapErrorAndRejectPromise),
|
||||||
|
esClient
|
||||||
|
.index(
|
||||||
|
{
|
||||||
|
index: AGENT_ACTIONS_INDEX,
|
||||||
|
body: action2,
|
||||||
|
},
|
||||||
|
ES_INDEX_OPTIONS
|
||||||
|
)
|
||||||
|
.catch(wrapErrorAndRejectPromise),
|
||||||
|
]);
|
||||||
|
|
||||||
|
response.actions.push(action1, action2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DeleteIndexedFleetActionsResponse {
|
||||||
|
actions: DeleteByQueryResponse | undefined;
|
||||||
|
responses: DeleteByQueryResponse | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteIndexedFleetActions = async (
|
||||||
|
esClient: Client,
|
||||||
|
indexedData: IndexedFleetActionsForHostResponse
|
||||||
|
): Promise<DeleteIndexedFleetActionsResponse> => {
|
||||||
|
const response: DeleteIndexedFleetActionsResponse = {
|
||||||
|
actions: undefined,
|
||||||
|
responses: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (indexedData.actions.length) {
|
||||||
|
response.actions = (
|
||||||
|
await esClient
|
||||||
|
.deleteByQuery({
|
||||||
|
index: `${indexedData.actionsIndex}-*`,
|
||||||
|
wait_for_completion: true,
|
||||||
|
body: {
|
||||||
|
query: {
|
||||||
|
bool: {
|
||||||
|
filter: [
|
||||||
|
{ terms: { action_id: indexedData.actions.map((action) => action.action_id) } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch(wrapErrorAndRejectPromise)
|
||||||
|
).body;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indexedData.actionResponses) {
|
||||||
|
response.responses = (
|
||||||
|
await esClient
|
||||||
|
.deleteByQuery({
|
||||||
|
index: `${indexedData.responsesIndex}-*`,
|
||||||
|
wait_for_completion: true,
|
||||||
|
body: {
|
||||||
|
query: {
|
||||||
|
bool: {
|
||||||
|
filter: [
|
||||||
|
{
|
||||||
|
terms: {
|
||||||
|
action_id: indexedData.actionResponses.map((action) => action.action_id),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch(wrapErrorAndRejectPromise)
|
||||||
|
).body;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
|
@ -0,0 +1,133 @@
|
||||||
|
/*
|
||||||
|
* 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 { Client } from '@elastic/elasticsearch';
|
||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
import { DeleteByQueryResponse } from '@elastic/elasticsearch/api/types';
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
import { KbnClient } from '@kbn/test';
|
||||||
|
import { HostMetadata } from '../types';
|
||||||
|
import {
|
||||||
|
Agent,
|
||||||
|
AGENT_API_ROUTES,
|
||||||
|
FleetServerAgent,
|
||||||
|
GetOneAgentResponse,
|
||||||
|
} from '../../../../fleet/common';
|
||||||
|
import { FleetAgentGenerator } from '../data_generators/fleet_agent_generator';
|
||||||
|
import { wrapErrorAndRejectPromise } from './utils';
|
||||||
|
|
||||||
|
const defaultFleetAgentGenerator = new FleetAgentGenerator();
|
||||||
|
|
||||||
|
export interface IndexedFleetAgentResponse {
|
||||||
|
agents: Agent[];
|
||||||
|
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 = async (
|
||||||
|
esClient: Client,
|
||||||
|
kbnClient: KbnClient,
|
||||||
|
endpointHost: HostMetadata,
|
||||||
|
agentPolicyId: string,
|
||||||
|
kibanaVersion: string = '8.0.0',
|
||||||
|
fleetAgentGenerator: FleetAgentGenerator = defaultFleetAgentGenerator
|
||||||
|
): Promise<IndexedFleetAgentResponse> => {
|
||||||
|
const agentDoc = fleetAgentGenerator.generateEsHit({
|
||||||
|
_source: {
|
||||||
|
local_metadata: {
|
||||||
|
elastic: {
|
||||||
|
agent: {
|
||||||
|
version: kibanaVersion,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
host: {
|
||||||
|
...endpointHost.host,
|
||||||
|
},
|
||||||
|
os: {
|
||||||
|
...endpointHost.host.os,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
policy_id: agentPolicyId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const createdFleetAgent = await esClient
|
||||||
|
.index<FleetServerAgent>({
|
||||||
|
index: agentDoc._index,
|
||||||
|
id: agentDoc._id,
|
||||||
|
body: agentDoc._source!,
|
||||||
|
op_type: 'create',
|
||||||
|
})
|
||||||
|
.catch(wrapErrorAndRejectPromise);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fleetAgentsIndex: agentDoc._index,
|
||||||
|
agents: [
|
||||||
|
await fetchFleetAgent(kbnClient, createdFleetAgent.body._id).catch(wrapErrorAndRejectPromise),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchFleetAgent = async (kbnClient: KbnClient, agentId: string): Promise<Agent> => {
|
||||||
|
return ((await kbnClient
|
||||||
|
.request({
|
||||||
|
path: AGENT_API_ROUTES.INFO_PATTERN.replace('{agentId}', agentId),
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
.catch(wrapErrorAndRejectPromise)) as AxiosResponse<GetOneAgentResponse>).data.item;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DeleteIndexedFleetAgentsResponse {
|
||||||
|
agents: DeleteByQueryResponse | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteIndexedFleetAgents = async (
|
||||||
|
esClient: Client,
|
||||||
|
indexedData: IndexedFleetAgentResponse
|
||||||
|
): Promise<DeleteIndexedFleetAgentsResponse> => {
|
||||||
|
const response: DeleteIndexedFleetAgentsResponse = {
|
||||||
|
agents: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (indexedData.agents.length) {
|
||||||
|
response.agents = (
|
||||||
|
await esClient
|
||||||
|
.deleteByQuery({
|
||||||
|
index: `${indexedData.fleetAgentsIndex}-*`,
|
||||||
|
wait_for_completion: true,
|
||||||
|
body: {
|
||||||
|
query: {
|
||||||
|
bool: {
|
||||||
|
filter: [
|
||||||
|
{
|
||||||
|
terms: {
|
||||||
|
'local_metadata.elastic.agent.id': indexedData.agents.map(
|
||||||
|
(agent) => agent.local_metadata.elastic.agent.id
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch(wrapErrorAndRejectPromise)
|
||||||
|
).body;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
|
@ -0,0 +1,159 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
import { KbnClient } from '@kbn/test';
|
||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
import {
|
||||||
|
AGENT_POLICY_API_ROUTES,
|
||||||
|
AgentPolicy,
|
||||||
|
CreateAgentPolicyRequest,
|
||||||
|
CreateAgentPolicyResponse,
|
||||||
|
CreatePackagePolicyRequest,
|
||||||
|
CreatePackagePolicyResponse,
|
||||||
|
DeleteAgentPolicyResponse,
|
||||||
|
DeletePackagePoliciesResponse,
|
||||||
|
PACKAGE_POLICY_API_ROUTES,
|
||||||
|
} from '../../../../fleet/common';
|
||||||
|
import { PolicyData } from '../types';
|
||||||
|
import { policyFactory as policyConfigFactory } from '../models/policy_config';
|
||||||
|
import { wrapErrorAndRejectPromise } from './utils';
|
||||||
|
|
||||||
|
export interface IndexedFleetEndpointPolicyResponse {
|
||||||
|
integrationPolicies: PolicyData[];
|
||||||
|
agentPolicies: AgentPolicy[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an endpoint Integration Policy (and associated Agent Policy) via Fleet
|
||||||
|
* (NOTE: ensure that fleet is setup first before calling this loading function)
|
||||||
|
*/
|
||||||
|
export const indexFleetEndpointPolicy = async (
|
||||||
|
kbnClient: KbnClient,
|
||||||
|
policyName: string,
|
||||||
|
endpointPackageVersion: string = '8.0.0'
|
||||||
|
): Promise<IndexedFleetEndpointPolicyResponse> => {
|
||||||
|
const response: IndexedFleetEndpointPolicyResponse = {
|
||||||
|
integrationPolicies: [],
|
||||||
|
agentPolicies: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create Agent Policy first
|
||||||
|
const newAgentPolicyData: CreateAgentPolicyRequest['body'] = {
|
||||||
|
name: `Policy for ${policyName} (${Math.random().toString(36).substr(2, 5)})`,
|
||||||
|
description: `Policy created with endpoint data generator (${policyName})`,
|
||||||
|
namespace: 'default',
|
||||||
|
};
|
||||||
|
|
||||||
|
let agentPolicy: AxiosResponse<CreateAgentPolicyResponse>;
|
||||||
|
|
||||||
|
try {
|
||||||
|
agentPolicy = (await kbnClient
|
||||||
|
.request({
|
||||||
|
path: AGENT_POLICY_API_ROUTES.CREATE_PATTERN,
|
||||||
|
method: 'POST',
|
||||||
|
body: newAgentPolicyData,
|
||||||
|
})
|
||||||
|
.catch(wrapErrorAndRejectPromise)) as AxiosResponse<CreateAgentPolicyResponse>;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`create fleet agent policy failed ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.agentPolicies.push(agentPolicy.data.item);
|
||||||
|
|
||||||
|
// Create integration (package) policy
|
||||||
|
const newPackagePolicyData: CreatePackagePolicyRequest['body'] = {
|
||||||
|
name: policyName,
|
||||||
|
description: 'Protect the worlds data',
|
||||||
|
policy_id: agentPolicy.data.item.id,
|
||||||
|
enabled: true,
|
||||||
|
output_id: '',
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
type: 'endpoint',
|
||||||
|
enabled: true,
|
||||||
|
streams: [],
|
||||||
|
config: {
|
||||||
|
policy: {
|
||||||
|
value: policyConfigFactory(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
namespace: 'default',
|
||||||
|
package: {
|
||||||
|
name: 'endpoint',
|
||||||
|
title: 'endpoint',
|
||||||
|
version: endpointPackageVersion,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const packagePolicy = (await kbnClient
|
||||||
|
.request({
|
||||||
|
path: PACKAGE_POLICY_API_ROUTES.CREATE_PATTERN,
|
||||||
|
method: 'POST',
|
||||||
|
body: newPackagePolicyData,
|
||||||
|
})
|
||||||
|
.catch(wrapErrorAndRejectPromise)) as AxiosResponse<CreatePackagePolicyResponse>;
|
||||||
|
|
||||||
|
response.integrationPolicies.push(packagePolicy.data.item as PolicyData);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DeleteIndexedFleetEndpointPoliciesResponse {
|
||||||
|
integrationPolicies: DeletePackagePoliciesResponse | undefined;
|
||||||
|
agentPolicies: DeleteAgentPolicyResponse[] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete indexed Fleet Endpoint integration policies along with their respective Agent Policies.
|
||||||
|
* Prior to calling this function, ensure that no agents are associated with the Agent Policy.
|
||||||
|
* (NOTE: ensure that fleet is setup first before calling this loading function)
|
||||||
|
* @param kbnClient
|
||||||
|
* @param indexData
|
||||||
|
*/
|
||||||
|
export const deleteIndexedFleetEndpointPolicies = async (
|
||||||
|
kbnClient: KbnClient,
|
||||||
|
indexData: IndexedFleetEndpointPolicyResponse
|
||||||
|
): Promise<DeleteIndexedFleetEndpointPoliciesResponse> => {
|
||||||
|
const response: DeleteIndexedFleetEndpointPoliciesResponse = {
|
||||||
|
integrationPolicies: undefined,
|
||||||
|
agentPolicies: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (indexData.integrationPolicies.length) {
|
||||||
|
response.integrationPolicies = ((await kbnClient
|
||||||
|
.request({
|
||||||
|
path: PACKAGE_POLICY_API_ROUTES.DELETE_PATTERN,
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
packagePolicyIds: indexData.integrationPolicies.map((policy) => policy.id),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch(wrapErrorAndRejectPromise)) as AxiosResponse<DeletePackagePoliciesResponse>).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indexData.agentPolicies.length) {
|
||||||
|
response.agentPolicies = [];
|
||||||
|
|
||||||
|
for (const agentPolicy of indexData.agentPolicies) {
|
||||||
|
response.agentPolicies.push(
|
||||||
|
((await kbnClient
|
||||||
|
.request({
|
||||||
|
path: AGENT_POLICY_API_ROUTES.DELETE_PATTERN,
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
agentPolicyId: agentPolicy.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch(wrapErrorAndRejectPromise)) as AxiosResponse<DeleteAgentPolicyResponse>).data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* 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 { Client } from '@elastic/elasticsearch';
|
||||||
|
import { FLEET_SERVER_SERVERS_INDEX } from '../../../../fleet/common';
|
||||||
|
import { wrapErrorAndRejectPromise } from './utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will ensure that at least one fleet server is present in the `.fleet-servers` index. This will
|
||||||
|
* enable the `Agent` section of kibana Fleet to be displayed
|
||||||
|
*
|
||||||
|
* @param esClient
|
||||||
|
* @param version
|
||||||
|
*/
|
||||||
|
export const enableFleetServerIfNecessary = async (esClient: Client, version: string = '8.0.0') => {
|
||||||
|
const res = await esClient.search<{}, {}>({
|
||||||
|
index: FLEET_SERVER_SERVERS_INDEX,
|
||||||
|
ignore_unavailable: true,
|
||||||
|
rest_total_hits_as_int: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.body.hits.total > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a Fake fleet-server in this kibana instance
|
||||||
|
await esClient
|
||||||
|
.index({
|
||||||
|
index: FLEET_SERVER_SERVERS_INDEX,
|
||||||
|
body: {
|
||||||
|
agent: {
|
||||||
|
id: '12988155-475c-430d-ac89-84dc84b67cd1',
|
||||||
|
version,
|
||||||
|
},
|
||||||
|
host: {
|
||||||
|
architecture: 'linux',
|
||||||
|
id: 'c3e5f4f690b4a3ff23e54900701a9513',
|
||||||
|
ip: ['127.0.0.1', '::1', '10.201.0.213', 'fe80::4001:aff:fec9:d5'],
|
||||||
|
name: 'endpoint-data-generator',
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
id: '12988155-475c-430d-ac89-84dc84b67cd1',
|
||||||
|
version,
|
||||||
|
},
|
||||||
|
'@timestamp': '2021-05-12T18:42:52.009482058Z',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch(wrapErrorAndRejectPromise);
|
||||||
|
};
|
|
@ -0,0 +1,117 @@
|
||||||
|
/*
|
||||||
|
* 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 { AxiosResponse } from 'axios';
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
import { KbnClient } from '@kbn/test';
|
||||||
|
import {
|
||||||
|
AGENTS_SETUP_API_ROUTES,
|
||||||
|
BulkInstallPackageInfo,
|
||||||
|
BulkInstallPackagesResponse,
|
||||||
|
EPM_API_ROUTES,
|
||||||
|
IBulkInstallPackageHTTPError,
|
||||||
|
PostFleetSetupResponse,
|
||||||
|
SETUP_API_ROUTE,
|
||||||
|
} from '../../../../fleet/common';
|
||||||
|
import { EndpointDataLoadingError, wrapErrorAndRejectPromise } from './utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls the fleet setup APIs and then installs the latest Endpoint package
|
||||||
|
* @param kbnClient
|
||||||
|
*/
|
||||||
|
export const setupFleetForEndpoint = async (kbnClient: KbnClient) => {
|
||||||
|
// We try to use the kbnClient **private** logger, bug if unable to access it, then just use console
|
||||||
|
// @ts-ignore
|
||||||
|
const log = kbnClient.log ? kbnClient.log : console;
|
||||||
|
|
||||||
|
// Setup Fleet
|
||||||
|
try {
|
||||||
|
const setupResponse = (await kbnClient
|
||||||
|
.request({
|
||||||
|
path: SETUP_API_ROUTE,
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
.catch(wrapErrorAndRejectPromise)) as AxiosResponse<PostFleetSetupResponse>;
|
||||||
|
|
||||||
|
if (!setupResponse.data.isInitialized) {
|
||||||
|
log.error(setupResponse.data);
|
||||||
|
throw new Error('Initializing the ingest manager failed, existing');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup Agents
|
||||||
|
try {
|
||||||
|
const setupResponse = (await kbnClient
|
||||||
|
.request({
|
||||||
|
path: AGENTS_SETUP_API_ROUTES.CREATE_PATTERN,
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
.catch(wrapErrorAndRejectPromise)) as AxiosResponse<PostFleetSetupResponse>;
|
||||||
|
|
||||||
|
if (!setupResponse.data.isInitialized) {
|
||||||
|
log.error(setupResponse.data);
|
||||||
|
throw new Error('Initializing Fleet failed, existing');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install/upgrade the endpoint package
|
||||||
|
try {
|
||||||
|
await installOrUpgradeEndpointFleetPackage(kbnClient);
|
||||||
|
} catch (error) {
|
||||||
|
log.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installs the Endpoint package (or upgrades it) in Fleet to the latest available in the registry
|
||||||
|
*
|
||||||
|
* @param kbnClient
|
||||||
|
*/
|
||||||
|
export const installOrUpgradeEndpointFleetPackage = async (kbnClient: KbnClient): Promise<void> => {
|
||||||
|
const installEndpointPackageResp = (await kbnClient
|
||||||
|
.request({
|
||||||
|
path: EPM_API_ROUTES.BULK_INSTALL_PATTERN,
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
packages: ['endpoint'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch(wrapErrorAndRejectPromise)) as AxiosResponse<BulkInstallPackagesResponse>;
|
||||||
|
|
||||||
|
const bulkResp = installEndpointPackageResp.data.response;
|
||||||
|
|
||||||
|
if (bulkResp.length <= 0) {
|
||||||
|
throw new EndpointDataLoadingError(
|
||||||
|
'Installing the Endpoint package failed, response was empty, existing',
|
||||||
|
bulkResp
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFleetBulkInstallError(bulkResp[0])) {
|
||||||
|
if (bulkResp[0].error instanceof Error) {
|
||||||
|
throw new EndpointDataLoadingError(
|
||||||
|
`Installing the Endpoint package failed: ${bulkResp[0].error.message}, exiting`,
|
||||||
|
bulkResp
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new EndpointDataLoadingError(bulkResp[0].error, bulkResp);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function isFleetBulkInstallError(
|
||||||
|
installResponse: BulkInstallPackageInfo | IBulkInstallPackageHTTPError
|
||||||
|
): installResponse is IBulkInstallPackageHTTPError {
|
||||||
|
return 'error' in installResponse && installResponse.error !== undefined;
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* 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 class EndpointDataLoadingError extends Error {
|
||||||
|
constructor(message: string, public meta?: unknown) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const wrapErrorIfNeeded = (error: Error): EndpointDataLoadingError =>
|
||||||
|
error instanceof EndpointDataLoadingError
|
||||||
|
? error
|
||||||
|
: new EndpointDataLoadingError(error.message, error);
|
||||||
|
|
||||||
|
// Use it in Promise's `.catch()` as `.catch(wrapErrorAndRejectPromise)`
|
||||||
|
export const wrapErrorAndRejectPromise = (error: Error) => Promise.reject(wrapErrorIfNeeded(error));
|
|
@ -5,39 +5,49 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Client, estypes } from '@elastic/elasticsearch';
|
import { Client } from '@elastic/elasticsearch';
|
||||||
import seedrandom from 'seedrandom';
|
import seedrandom from 'seedrandom';
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
import { KbnClient } from '@kbn/test';
|
import { KbnClient } from '@kbn/test';
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import { EndpointDocGenerator, Event, TreeOptions } from './generate_data';
|
import { merge } from 'lodash';
|
||||||
import { firstNonNullValue } from './models/ecs_safety_helpers';
|
import { EndpointDocGenerator, TreeOptions } from './generate_data';
|
||||||
import {
|
import {
|
||||||
AGENT_ACTIONS_INDEX,
|
|
||||||
AGENT_ACTIONS_RESULTS_INDEX,
|
|
||||||
AGENT_POLICY_API_ROUTES,
|
|
||||||
CreateAgentPolicyRequest,
|
|
||||||
CreateAgentPolicyResponse,
|
|
||||||
CreatePackagePolicyRequest,
|
|
||||||
CreatePackagePolicyResponse,
|
CreatePackagePolicyResponse,
|
||||||
EPM_API_ROUTES,
|
EPM_API_ROUTES,
|
||||||
FLEET_SERVER_SERVERS_INDEX,
|
|
||||||
FleetServerAgent,
|
|
||||||
GetPackagesResponse,
|
GetPackagesResponse,
|
||||||
PACKAGE_POLICY_API_ROUTES,
|
|
||||||
} from '../../../fleet/common';
|
} from '../../../fleet/common';
|
||||||
import { policyFactory as policyConfigFactory } from './models/policy_config';
|
import {
|
||||||
import { EndpointAction, HostMetadata } from './types';
|
deleteIndexedEndpointHosts,
|
||||||
import { KbnClientWithApiKeySupport } from '../../scripts/endpoint/kbn_client_with_api_key_support';
|
DeleteIndexedEndpointHostsResponse,
|
||||||
import { FleetAgentGenerator } from './data_generators/fleet_agent_generator';
|
IndexedHostsResponse,
|
||||||
import { FleetActionGenerator } from './data_generators/fleet_action_generator';
|
indexEndpointHostDocs,
|
||||||
|
} from './data_loaders/index_endpoint_hosts';
|
||||||
|
import { enableFleetServerIfNecessary } from './data_loaders/index_fleet_server';
|
||||||
|
import { indexAlerts } from './data_loaders/index_alerts';
|
||||||
|
import { setupFleetForEndpoint } from './data_loaders/setup_fleet_for_endpoint';
|
||||||
|
|
||||||
const fleetAgentGenerator = new FleetAgentGenerator();
|
export type IndexedHostsAndAlertsResponse = IndexedHostsResponse;
|
||||||
const fleetActionGenerator = new FleetActionGenerator();
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indexes Endpoint Hosts (with optional Fleet counterparts) along with alerts
|
||||||
|
*
|
||||||
|
* @param client
|
||||||
|
* @param kbnClient
|
||||||
|
* @param seed
|
||||||
|
* @param numHosts
|
||||||
|
* @param numDocs
|
||||||
|
* @param metadataIndex
|
||||||
|
* @param policyResponseIndex
|
||||||
|
* @param eventIndex
|
||||||
|
* @param alertIndex
|
||||||
|
* @param alertsPerHost
|
||||||
|
* @param fleet
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
export async function indexHostsAndAlerts(
|
export async function indexHostsAndAlerts(
|
||||||
client: Client,
|
client: Client,
|
||||||
kbnClient: KbnClientWithApiKeySupport,
|
kbnClient: KbnClient,
|
||||||
seed: string,
|
seed: string,
|
||||||
numHosts: number,
|
numHosts: number,
|
||||||
numDocs: number,
|
numDocs: number,
|
||||||
|
@ -48,9 +58,26 @@ export async function indexHostsAndAlerts(
|
||||||
alertsPerHost: number,
|
alertsPerHost: number,
|
||||||
fleet: boolean,
|
fleet: boolean,
|
||||||
options: TreeOptions = {}
|
options: TreeOptions = {}
|
||||||
) {
|
): Promise<IndexedHostsAndAlertsResponse> {
|
||||||
const random = seedrandom(seed);
|
const random = seedrandom(seed);
|
||||||
const epmEndpointPackage = await getEndpointPackageInfo(kbnClient);
|
const epmEndpointPackage = await getEndpointPackageInfo(kbnClient);
|
||||||
|
const response: IndexedHostsAndAlertsResponse = {
|
||||||
|
hosts: [],
|
||||||
|
policyResponses: [],
|
||||||
|
agents: [],
|
||||||
|
fleetAgentsIndex: '',
|
||||||
|
metadataIndex,
|
||||||
|
policyResponseIndex,
|
||||||
|
actionResponses: [],
|
||||||
|
responsesIndex: '',
|
||||||
|
actions: [],
|
||||||
|
actionsIndex: '',
|
||||||
|
integrationPolicies: [],
|
||||||
|
agentPolicies: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ensure fleet is setup and endpint package installed
|
||||||
|
await setupFleetForEndpoint(kbnClient);
|
||||||
|
|
||||||
// If `fleet` integration is true, then ensure a (fake) fleet-server is connected
|
// If `fleet` integration is true, then ensure a (fake) fleet-server is connected
|
||||||
if (fleet) {
|
if (fleet) {
|
||||||
|
@ -62,7 +89,7 @@ export async function indexHostsAndAlerts(
|
||||||
|
|
||||||
for (let i = 0; i < numHosts; i++) {
|
for (let i = 0; i < numHosts; i++) {
|
||||||
const generator = new EndpointDocGenerator(random);
|
const generator = new EndpointDocGenerator(random);
|
||||||
await indexHostDocs({
|
const indexedHosts = await indexEndpointHostDocs({
|
||||||
numDocs,
|
numDocs,
|
||||||
client,
|
client,
|
||||||
kbnClient,
|
kbnClient,
|
||||||
|
@ -73,6 +100,9 @@ export async function indexHostsAndAlerts(
|
||||||
enrollFleet: fleet,
|
enrollFleet: fleet,
|
||||||
generator,
|
generator,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
merge(response, indexedHosts);
|
||||||
|
|
||||||
await indexAlerts({
|
await indexAlerts({
|
||||||
client,
|
client,
|
||||||
eventIndex,
|
eventIndex,
|
||||||
|
@ -83,220 +113,9 @@ export async function indexHostsAndAlerts(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.indices.refresh({
|
return response;
|
||||||
index: eventIndex,
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Unclear why the documents are not showing up after the call to refresh.
|
|
||||||
// Waiting 5 seconds allows the indices to refresh automatically and
|
|
||||||
// the documents become available in API/integration tests.
|
|
||||||
await delay(5000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function delay(ms: number) {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function indexHostDocs({
|
|
||||||
numDocs,
|
|
||||||
client,
|
|
||||||
kbnClient,
|
|
||||||
realPolicies,
|
|
||||||
epmEndpointPackage,
|
|
||||||
metadataIndex,
|
|
||||||
policyResponseIndex,
|
|
||||||
enrollFleet,
|
|
||||||
generator,
|
|
||||||
}: {
|
|
||||||
numDocs: number;
|
|
||||||
client: Client;
|
|
||||||
kbnClient: KbnClientWithApiKeySupport;
|
|
||||||
realPolicies: Record<string, CreatePackagePolicyResponse['item']>;
|
|
||||||
epmEndpointPackage: GetPackagesResponse['response'][0];
|
|
||||||
metadataIndex: string;
|
|
||||||
policyResponseIndex: string;
|
|
||||||
enrollFleet: boolean;
|
|
||||||
generator: EndpointDocGenerator;
|
|
||||||
}) {
|
|
||||||
const timeBetweenDocs = 6 * 3600 * 1000; // 6 hours between metadata documents
|
|
||||||
const timestamp = new Date().getTime();
|
|
||||||
const kibanaVersion = await fetchKibanaVersion(kbnClient);
|
|
||||||
let hostMetadata: HostMetadata;
|
|
||||||
let wasAgentEnrolled = false;
|
|
||||||
let enrolledAgent: undefined | estypes.SearchHit<FleetServerAgent>;
|
|
||||||
|
|
||||||
for (let j = 0; j < numDocs; j++) {
|
|
||||||
generator.updateHostData();
|
|
||||||
generator.updateHostPolicyData();
|
|
||||||
|
|
||||||
hostMetadata = generator.generateHostMetadata(
|
|
||||||
timestamp - timeBetweenDocs * (numDocs - j - 1),
|
|
||||||
EndpointDocGenerator.createDataStreamFromIndex(metadataIndex)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (enrollFleet) {
|
|
||||||
const { id: appliedPolicyId, name: appliedPolicyName } = hostMetadata.Endpoint.policy.applied;
|
|
||||||
|
|
||||||
// If we don't yet have a "real" policy record, then create it now in ingest (package config)
|
|
||||||
if (!realPolicies[appliedPolicyId]) {
|
|
||||||
// eslint-disable-next-line require-atomic-updates
|
|
||||||
realPolicies[appliedPolicyId] = await createPolicy(
|
|
||||||
kbnClient,
|
|
||||||
appliedPolicyName,
|
|
||||||
epmEndpointPackage.version
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we did not yet enroll an agent for this Host, do it now that we have good policy id
|
|
||||||
if (!wasAgentEnrolled) {
|
|
||||||
wasAgentEnrolled = true;
|
|
||||||
|
|
||||||
enrolledAgent = await indexFleetAgentForHost(
|
|
||||||
client,
|
|
||||||
hostMetadata!,
|
|
||||||
realPolicies[appliedPolicyId].policy_id,
|
|
||||||
kibanaVersion
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Update the Host metadata record with the ID of the "real" policy along with the enrolled agent id
|
|
||||||
hostMetadata = {
|
|
||||||
...hostMetadata,
|
|
||||||
elastic: {
|
|
||||||
...hostMetadata.elastic,
|
|
||||||
agent: {
|
|
||||||
...hostMetadata.elastic.agent,
|
|
||||||
id: enrolledAgent?._id ?? hostMetadata.elastic.agent.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Endpoint: {
|
|
||||||
...hostMetadata.Endpoint,
|
|
||||||
policy: {
|
|
||||||
...hostMetadata.Endpoint.policy,
|
|
||||||
applied: {
|
|
||||||
...hostMetadata.Endpoint.policy.applied,
|
|
||||||
id: realPolicies[appliedPolicyId].id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create some actions for this Host
|
|
||||||
await indexFleetActionsForHost(client, hostMetadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
await client.index({
|
|
||||||
index: metadataIndex,
|
|
||||||
body: hostMetadata,
|
|
||||||
op_type: 'create',
|
|
||||||
});
|
|
||||||
await client.index({
|
|
||||||
index: policyResponseIndex,
|
|
||||||
body: generator.generatePolicyResponse({
|
|
||||||
ts: timestamp - timeBetweenDocs * (numDocs - j - 1),
|
|
||||||
policyDataStream: EndpointDocGenerator.createDataStreamFromIndex(policyResponseIndex),
|
|
||||||
}),
|
|
||||||
op_type: 'create',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function indexAlerts({
|
|
||||||
client,
|
|
||||||
eventIndex,
|
|
||||||
alertIndex,
|
|
||||||
generator,
|
|
||||||
numAlerts,
|
|
||||||
options = {},
|
|
||||||
}: {
|
|
||||||
client: Client;
|
|
||||||
eventIndex: string;
|
|
||||||
alertIndex: string;
|
|
||||||
generator: EndpointDocGenerator;
|
|
||||||
numAlerts: number;
|
|
||||||
options: TreeOptions;
|
|
||||||
}) {
|
|
||||||
const alertGenerator = generator.alertsGenerator(numAlerts, options);
|
|
||||||
let result = alertGenerator.next();
|
|
||||||
while (!result.done) {
|
|
||||||
let k = 0;
|
|
||||||
const resolverDocs: Event[] = [];
|
|
||||||
while (k < 1000 && !result.done) {
|
|
||||||
resolverDocs.push(result.value);
|
|
||||||
result = alertGenerator.next();
|
|
||||||
k++;
|
|
||||||
}
|
|
||||||
const body = resolverDocs.reduce(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
(array: Array<Record<string, any>>, doc) => {
|
|
||||||
let index = eventIndex;
|
|
||||||
if (firstNonNullValue(doc.event?.kind) === 'alert') {
|
|
||||||
index = alertIndex;
|
|
||||||
}
|
|
||||||
array.push({ create: { _index: index } }, doc);
|
|
||||||
return array;
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
await client.bulk({ body, refresh: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const createPolicy = async (
|
|
||||||
kbnClient: KbnClient,
|
|
||||||
policyName: string,
|
|
||||||
endpointPackageVersion: string
|
|
||||||
): Promise<CreatePackagePolicyResponse['item']> => {
|
|
||||||
// Create Agent Policy first
|
|
||||||
const newAgentPolicyData: CreateAgentPolicyRequest['body'] = {
|
|
||||||
name: `Policy for ${policyName} (${Math.random().toString(36).substr(2, 5)})`,
|
|
||||||
description: `Policy created with endpoint data generator (${policyName})`,
|
|
||||||
namespace: 'default',
|
|
||||||
};
|
|
||||||
let agentPolicy;
|
|
||||||
try {
|
|
||||||
agentPolicy = (await kbnClient.request({
|
|
||||||
path: AGENT_POLICY_API_ROUTES.CREATE_PATTERN,
|
|
||||||
method: 'POST',
|
|
||||||
body: newAgentPolicyData,
|
|
||||||
})) as AxiosResponse<CreateAgentPolicyResponse>;
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`create policy ${error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create Package Configuration
|
|
||||||
const newPackagePolicyData: CreatePackagePolicyRequest['body'] = {
|
|
||||||
name: policyName,
|
|
||||||
description: 'Protect the worlds data',
|
|
||||||
policy_id: agentPolicy.data.item.id,
|
|
||||||
enabled: true,
|
|
||||||
output_id: '',
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
type: 'endpoint',
|
|
||||||
enabled: true,
|
|
||||||
streams: [],
|
|
||||||
config: {
|
|
||||||
policy: {
|
|
||||||
value: policyConfigFactory(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
namespace: 'default',
|
|
||||||
package: {
|
|
||||||
name: 'endpoint',
|
|
||||||
title: 'endpoint',
|
|
||||||
version: endpointPackageVersion,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const packagePolicy = (await kbnClient.request({
|
|
||||||
path: PACKAGE_POLICY_API_ROUTES.CREATE_PATTERN,
|
|
||||||
method: 'POST',
|
|
||||||
body: newPackagePolicyData,
|
|
||||||
})) as AxiosResponse<CreatePackagePolicyResponse>;
|
|
||||||
return packagePolicy.data.item;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getEndpointPackageInfo = async (
|
const getEndpointPackageInfo = async (
|
||||||
kbnClient: KbnClient
|
kbnClient: KbnClient
|
||||||
): Promise<GetPackagesResponse['response'][0]> => {
|
): Promise<GetPackagesResponse['response'][0]> => {
|
||||||
|
@ -314,194 +133,14 @@ const getEndpointPackageInfo = async (
|
||||||
return endpointPackage;
|
return endpointPackage;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchKibanaVersion = async (kbnClient: KbnClientWithApiKeySupport) => {
|
export type DeleteIndexedHostsAndAlertsResponse = DeleteIndexedEndpointHostsResponse;
|
||||||
const version = ((await kbnClient.request({
|
|
||||||
path: '/api/status',
|
|
||||||
method: 'GET',
|
|
||||||
})) as AxiosResponse).data.version.number;
|
|
||||||
|
|
||||||
if (!version) {
|
export const deleteIndexedHostsAndAlerts = async (
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('failed to retrieve kibana version');
|
|
||||||
return '8.0.0';
|
|
||||||
}
|
|
||||||
|
|
||||||
return version;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Will ensure that at least one fleet server is present in the `.fleet-servers` index. This will
|
|
||||||
* enable the `Agent` section of kibana Fleet to be displayed
|
|
||||||
*
|
|
||||||
* @param esClient
|
|
||||||
* @param version
|
|
||||||
*/
|
|
||||||
const enableFleetServerIfNecessary = async (esClient: Client, version: string = '8.0.0') => {
|
|
||||||
const res = await esClient.search<{}, {}>({
|
|
||||||
index: FLEET_SERVER_SERVERS_INDEX,
|
|
||||||
ignore_unavailable: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// @ts-expect-error value is number | TotalHits
|
|
||||||
if (res.body.hits.total.value > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a Fake fleet-server in this kibana instance
|
|
||||||
await esClient.index({
|
|
||||||
index: FLEET_SERVER_SERVERS_INDEX,
|
|
||||||
body: {
|
|
||||||
agent: {
|
|
||||||
id: '12988155-475c-430d-ac89-84dc84b67cd1',
|
|
||||||
version: '',
|
|
||||||
},
|
|
||||||
host: {
|
|
||||||
architecture: 'linux',
|
|
||||||
id: 'c3e5f4f690b4a3ff23e54900701a9513',
|
|
||||||
ip: ['127.0.0.1', '::1', '10.201.0.213', 'fe80::4001:aff:fec9:d5'],
|
|
||||||
name: 'endpoint-data-generator',
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
id: '12988155-475c-430d-ac89-84dc84b67cd1',
|
|
||||||
version: '8.0.0-SNAPSHOT',
|
|
||||||
},
|
|
||||||
'@timestamp': '2021-05-12T18:42:52.009482058Z',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const indexFleetAgentForHost = async (
|
|
||||||
esClient: Client,
|
esClient: Client,
|
||||||
endpointHost: HostMetadata,
|
kbnClient: KbnClient,
|
||||||
agentPolicyId: string,
|
indexedData: IndexedHostsAndAlertsResponse
|
||||||
kibanaVersion: string = '8.0.0'
|
): Promise<DeleteIndexedHostsAndAlertsResponse> => {
|
||||||
): Promise<estypes.SearchHit<FleetServerAgent>> => {
|
return {
|
||||||
const agentDoc = fleetAgentGenerator.generateEsHit({
|
...(await deleteIndexedEndpointHosts(esClient, kbnClient, indexedData)),
|
||||||
_source: {
|
};
|
||||||
local_metadata: {
|
|
||||||
elastic: {
|
|
||||||
agent: {
|
|
||||||
version: kibanaVersion,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
host: {
|
|
||||||
...endpointHost.host,
|
|
||||||
},
|
|
||||||
os: {
|
|
||||||
...endpointHost.host.os,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
policy_id: agentPolicyId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await esClient.index<FleetServerAgent>({
|
|
||||||
index: agentDoc._index,
|
|
||||||
id: agentDoc._id,
|
|
||||||
body: agentDoc._source!,
|
|
||||||
op_type: 'create',
|
|
||||||
});
|
|
||||||
|
|
||||||
return agentDoc;
|
|
||||||
};
|
|
||||||
|
|
||||||
const indexFleetActionsForHost = async (
|
|
||||||
esClient: Client,
|
|
||||||
endpointHost: HostMetadata
|
|
||||||
): Promise<void> => {
|
|
||||||
const ES_INDEX_OPTIONS = { headers: { 'X-elastic-product-origin': 'fleet' } };
|
|
||||||
const agentId = endpointHost.elastic.agent.id;
|
|
||||||
const total = fleetActionGenerator.randomN(5);
|
|
||||||
|
|
||||||
for (let i = 0; i < total; i++) {
|
|
||||||
// create an action
|
|
||||||
const action = fleetActionGenerator.generate({
|
|
||||||
data: { comment: 'data generator: this host is bad' },
|
|
||||||
});
|
|
||||||
|
|
||||||
action.agents = [agentId];
|
|
||||||
|
|
||||||
esClient.index(
|
|
||||||
{
|
|
||||||
index: AGENT_ACTIONS_INDEX,
|
|
||||||
body: action,
|
|
||||||
},
|
|
||||||
ES_INDEX_OPTIONS
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create an action response for the above
|
|
||||||
const actionResponse = fleetActionGenerator.generateResponse({
|
|
||||||
action_id: action.action_id,
|
|
||||||
agent_id: agentId,
|
|
||||||
action_data: action.data,
|
|
||||||
});
|
|
||||||
|
|
||||||
esClient.index(
|
|
||||||
{
|
|
||||||
index: AGENT_ACTIONS_RESULTS_INDEX,
|
|
||||||
body: actionResponse,
|
|
||||||
},
|
|
||||||
ES_INDEX_OPTIONS
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add edge cases (maybe)
|
|
||||||
if (fleetActionGenerator.randomFloat() < 0.3) {
|
|
||||||
const randomFloat = fleetActionGenerator.randomFloat();
|
|
||||||
|
|
||||||
// 60% of the time just add either an Isolate -OR- an UnIsolate action
|
|
||||||
if (randomFloat < 0.6) {
|
|
||||||
let action: EndpointAction;
|
|
||||||
|
|
||||||
if (randomFloat < 0.3) {
|
|
||||||
// add a pending isolation
|
|
||||||
action = fleetActionGenerator.generateIsolateAction({
|
|
||||||
'@timestamp': new Date().toISOString(),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// add a pending UN-isolation
|
|
||||||
action = fleetActionGenerator.generateUnIsolateAction({
|
|
||||||
'@timestamp': new Date().toISOString(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
action.agents = [agentId];
|
|
||||||
|
|
||||||
await esClient.index(
|
|
||||||
{
|
|
||||||
index: AGENT_ACTIONS_INDEX,
|
|
||||||
body: action,
|
|
||||||
},
|
|
||||||
ES_INDEX_OPTIONS
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Else (40% of the time) add a pending isolate AND pending un-isolate
|
|
||||||
const action1 = fleetActionGenerator.generateIsolateAction({
|
|
||||||
'@timestamp': new Date().toISOString(),
|
|
||||||
});
|
|
||||||
const action2 = fleetActionGenerator.generateUnIsolateAction({
|
|
||||||
'@timestamp': new Date().toISOString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
action1.agents = [agentId];
|
|
||||||
action2.agents = [agentId];
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
esClient.index(
|
|
||||||
{
|
|
||||||
index: AGENT_ACTIONS_INDEX,
|
|
||||||
body: action1,
|
|
||||||
},
|
|
||||||
ES_INDEX_OPTIONS
|
|
||||||
),
|
|
||||||
esClient.index(
|
|
||||||
{
|
|
||||||
index: AGENT_ACTIONS_INDEX,
|
|
||||||
body: action2,
|
|
||||||
},
|
|
||||||
ES_INDEX_OPTIONS
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -227,7 +227,10 @@ const DetectionEnginePageComponent = () => {
|
||||||
<SiemSearchBar id="global" indexPattern={indexPattern} />
|
<SiemSearchBar id="global" indexPattern={indexPattern} />
|
||||||
</FiltersGlobal>
|
</FiltersGlobal>
|
||||||
|
|
||||||
<SecuritySolutionPageWrapper noPadding={globalFullScreen}>
|
<SecuritySolutionPageWrapper
|
||||||
|
noPadding={globalFullScreen}
|
||||||
|
data-test-subj="detectionsAlertsPage"
|
||||||
|
>
|
||||||
<Display show={!globalFullScreen}>
|
<Display show={!globalFullScreen}>
|
||||||
<DetectionEngineHeaderPage
|
<DetectionEngineHeaderPage
|
||||||
subtitle={
|
subtitle={
|
||||||
|
|
|
@ -126,7 +126,10 @@ const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDeta
|
||||||
<SiemSearchBar indexPattern={indexPattern} id="global" />
|
<SiemSearchBar indexPattern={indexPattern} id="global" />
|
||||||
</FiltersGlobal>
|
</FiltersGlobal>
|
||||||
|
|
||||||
<SecuritySolutionPageWrapper noPadding={globalFullScreen}>
|
<SecuritySolutionPageWrapper
|
||||||
|
noPadding={globalFullScreen}
|
||||||
|
data-test-subj="hostDetailsPage"
|
||||||
|
>
|
||||||
<Display show={!globalFullScreen}>
|
<Display show={!globalFullScreen}>
|
||||||
<HeaderPage
|
<HeaderPage
|
||||||
border
|
border
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { URL } from 'url';
|
|
||||||
|
|
||||||
import { KbnClient, KbnClientOptions } from '@kbn/test';
|
|
||||||
import fetch, { RequestInit } from 'node-fetch';
|
|
||||||
|
|
||||||
export class KbnClientWithApiKeySupport extends KbnClient {
|
|
||||||
private kibanaUrlNoAuth: URL;
|
|
||||||
|
|
||||||
constructor(options: KbnClientOptions) {
|
|
||||||
super(options);
|
|
||||||
|
|
||||||
// strip auth from url
|
|
||||||
const url = new URL(this.resolveUrl('/'));
|
|
||||||
url.username = '';
|
|
||||||
url.password = '';
|
|
||||||
|
|
||||||
this.kibanaUrlNoAuth = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The fleet api to enroll and agent requires an api key when you make
|
|
||||||
* the request, however KbnClient currently does not support sending
|
|
||||||
* an api key with the request. This function allows you to send an
|
|
||||||
* api key with a request.
|
|
||||||
*/
|
|
||||||
requestWithApiKey(path: string, init?: RequestInit) {
|
|
||||||
return fetch(new URL(path, this.kibanaUrlNoAuth), init);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -12,21 +12,8 @@ import { Client, ClientOptions } from '@elastic/elasticsearch';
|
||||||
import { ResponseError } from '@elastic/elasticsearch/lib/errors';
|
import { ResponseError } from '@elastic/elasticsearch/lib/errors';
|
||||||
import { ToolingLog, CA_CERT_PATH } from '@kbn/dev-utils';
|
import { ToolingLog, CA_CERT_PATH } from '@kbn/dev-utils';
|
||||||
import { KbnClient } from '@kbn/test';
|
import { KbnClient } from '@kbn/test';
|
||||||
import { AxiosResponse } from 'axios';
|
|
||||||
import { indexHostsAndAlerts } from '../../common/endpoint/index_data';
|
import { indexHostsAndAlerts } from '../../common/endpoint/index_data';
|
||||||
import { ANCESTRY_LIMIT, EndpointDocGenerator } from '../../common/endpoint/generate_data';
|
import { ANCESTRY_LIMIT, EndpointDocGenerator } from '../../common/endpoint/generate_data';
|
||||||
import {
|
|
||||||
AGENTS_SETUP_API_ROUTES,
|
|
||||||
EPM_API_ROUTES,
|
|
||||||
SETUP_API_ROUTE,
|
|
||||||
} from '../../../fleet/common/constants';
|
|
||||||
import {
|
|
||||||
BulkInstallPackageInfo,
|
|
||||||
BulkInstallPackagesResponse,
|
|
||||||
IBulkInstallPackageHTTPError,
|
|
||||||
PostFleetSetupResponse,
|
|
||||||
} from '../../../fleet/common/types/rest_spec';
|
|
||||||
import { KbnClientWithApiKeySupport } from './kbn_client_with_api_key_support';
|
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|
||||||
|
@ -50,75 +37,6 @@ async function deleteIndices(indices: string[], client: Client) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFleetBulkInstallError(
|
|
||||||
installResponse: BulkInstallPackageInfo | IBulkInstallPackageHTTPError
|
|
||||||
): installResponse is IBulkInstallPackageHTTPError {
|
|
||||||
return 'error' in installResponse && installResponse.error !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function doIngestSetup(kbnClient: KbnClient) {
|
|
||||||
// Setup Ingest
|
|
||||||
try {
|
|
||||||
const setupResponse = (await kbnClient.request({
|
|
||||||
path: SETUP_API_ROUTE,
|
|
||||||
method: 'POST',
|
|
||||||
})) as AxiosResponse<PostFleetSetupResponse>;
|
|
||||||
|
|
||||||
if (!setupResponse.data.isInitialized) {
|
|
||||||
console.error(setupResponse.data);
|
|
||||||
throw new Error('Initializing the ingest manager failed, existing');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup Fleet
|
|
||||||
try {
|
|
||||||
const setupResponse = (await kbnClient.request({
|
|
||||||
path: AGENTS_SETUP_API_ROUTES.CREATE_PATTERN,
|
|
||||||
method: 'POST',
|
|
||||||
})) as AxiosResponse<PostFleetSetupResponse>;
|
|
||||||
|
|
||||||
if (!setupResponse.data.isInitialized) {
|
|
||||||
console.error(setupResponse.data);
|
|
||||||
throw new Error('Initializing Fleet failed, existing');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install/upgrade the endpoint package
|
|
||||||
try {
|
|
||||||
const installEndpointPackageResp = (await kbnClient.request({
|
|
||||||
path: EPM_API_ROUTES.BULK_INSTALL_PATTERN,
|
|
||||||
method: 'POST',
|
|
||||||
body: {
|
|
||||||
packages: ['endpoint'],
|
|
||||||
},
|
|
||||||
})) as AxiosResponse<BulkInstallPackagesResponse>;
|
|
||||||
|
|
||||||
const bulkResp = installEndpointPackageResp.data.response;
|
|
||||||
if (bulkResp.length <= 0) {
|
|
||||||
throw new Error('Installing the Endpoint package failed, response was empty, existing');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFleetBulkInstallError(bulkResp[0])) {
|
|
||||||
if (bulkResp[0].error instanceof Error) {
|
|
||||||
throw new Error(
|
|
||||||
`Installing the Endpoint package failed: ${bulkResp[0].error.message}, exiting`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(bulkResp[0].error);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const argv = yargs.help().options({
|
const argv = yargs.help().options({
|
||||||
seed: {
|
seed: {
|
||||||
|
@ -255,14 +173,14 @@ async function main() {
|
||||||
},
|
},
|
||||||
}).argv;
|
}).argv;
|
||||||
let ca: Buffer;
|
let ca: Buffer;
|
||||||
let kbnClient: KbnClientWithApiKeySupport;
|
let kbnClient: KbnClient;
|
||||||
let clientOptions: ClientOptions;
|
let clientOptions: ClientOptions;
|
||||||
|
|
||||||
if (argv.ssl) {
|
if (argv.ssl) {
|
||||||
ca = fs.readFileSync(CA_CERT_PATH);
|
ca = fs.readFileSync(CA_CERT_PATH);
|
||||||
const url = argv.kibana.replace('http:', 'https:');
|
const url = argv.kibana.replace('http:', 'https:');
|
||||||
const node = argv.node.replace('http:', 'https:');
|
const node = argv.node.replace('http:', 'https:');
|
||||||
kbnClient = new KbnClientWithApiKeySupport({
|
kbnClient = new KbnClient({
|
||||||
log: new ToolingLog({
|
log: new ToolingLog({
|
||||||
level: 'info',
|
level: 'info',
|
||||||
writeTo: process.stdout,
|
writeTo: process.stdout,
|
||||||
|
@ -272,7 +190,7 @@ async function main() {
|
||||||
});
|
});
|
||||||
clientOptions = { node, ssl: { ca: [ca] } };
|
clientOptions = { node, ssl: { ca: [ca] } };
|
||||||
} else {
|
} else {
|
||||||
kbnClient = new KbnClientWithApiKeySupport({
|
kbnClient = new KbnClient({
|
||||||
log: new ToolingLog({
|
log: new ToolingLog({
|
||||||
level: 'info',
|
level: 'info',
|
||||||
writeTo: process.stdout,
|
writeTo: process.stdout,
|
||||||
|
@ -283,13 +201,6 @@ async function main() {
|
||||||
}
|
}
|
||||||
const client = new Client(clientOptions);
|
const client = new Client(clientOptions);
|
||||||
|
|
||||||
try {
|
|
||||||
await doIngestSetup(kbnClient);
|
|
||||||
} catch (error) {
|
|
||||||
// eslint-disable-next-line no-process-exit
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (argv.delete) {
|
if (argv.delete) {
|
||||||
await deleteIndices(
|
await deleteIndices(
|
||||||
[argv.eventIndex, argv.metadataIndex, argv.policyIndex, argv.alertIndex],
|
[argv.eventIndex, argv.metadataIndex, argv.policyIndex, argv.alertIndex],
|
||||||
|
|
|
@ -26,7 +26,12 @@ import {
|
||||||
} from './errors';
|
} from './errors';
|
||||||
import { getESQueryHostMetadataByID } from '../../routes/metadata/query_builders';
|
import { getESQueryHostMetadataByID } from '../../routes/metadata/query_builders';
|
||||||
import { queryResponseToHostResult } from '../../routes/metadata/support/query_strategies';
|
import { queryResponseToHostResult } from '../../routes/metadata/support/query_strategies';
|
||||||
import { DEFAULT_ENDPOINT_HOST_STATUS, fleetAgentStatusToEndpointHostStatus } from '../../utils';
|
import {
|
||||||
|
catchAndWrapError,
|
||||||
|
DEFAULT_ENDPOINT_HOST_STATUS,
|
||||||
|
fleetAgentStatusToEndpointHostStatus,
|
||||||
|
wrapErrorIfNeeded,
|
||||||
|
} from '../../utils';
|
||||||
import { EndpointError } from '../../errors';
|
import { EndpointError } from '../../errors';
|
||||||
import { createInternalReadonlySoClient } from '../../utils/create_internal_readonly_so_client';
|
import { createInternalReadonlySoClient } from '../../utils/create_internal_readonly_so_client';
|
||||||
|
|
||||||
|
@ -34,15 +39,6 @@ type AgentPolicyWithPackagePolicies = Omit<AgentPolicy, 'package_policies'> & {
|
||||||
package_policies: PackagePolicy[];
|
package_policies: PackagePolicy[];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Will wrap the given Error with `EndpointError`, which will
|
|
||||||
// help getting a good picture of where in our code the error originated from.
|
|
||||||
const wrapErrorIfNeeded = (error: Error): EndpointError =>
|
|
||||||
error instanceof EndpointError ? error : new EndpointError(error.message, error);
|
|
||||||
|
|
||||||
// used as the callback to `Promise#catch()` to ensure errors
|
|
||||||
// (especially those from kibana/elasticsearch clients) are wrapped
|
|
||||||
const catchAndWrapError = <E extends Error>(error: E) => Promise.reject(wrapErrorIfNeeded(error));
|
|
||||||
|
|
||||||
export class EndpointMetadataService {
|
export class EndpointMetadataService {
|
||||||
/**
|
/**
|
||||||
* For internal use only by the `this.DANGEROUS_INTERNAL_SO_CLIENT`
|
* For internal use only by the `this.DANGEROUS_INTERNAL_SO_CLIENT`
|
||||||
|
|
|
@ -6,3 +6,4 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from './fleet_agent_status_to_endpoint_host_status';
|
export * from './fleet_agent_status_to_endpoint_host_status';
|
||||||
|
export * from './wrap_errors';
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* 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 { EndpointError } from '../errors';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will wrap the given Error with `EndpointError`, which will help getting a good picture of where in
|
||||||
|
* our code the error originated (better stack trace).
|
||||||
|
*/
|
||||||
|
export const wrapErrorIfNeeded = (error: Error): EndpointError =>
|
||||||
|
error instanceof EndpointError ? error : new EndpointError(error.message, error);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* used as the callback to `Promise#catch()` to ensure errors
|
||||||
|
* (especially those from kibana/elasticsearch clients) are wrapped
|
||||||
|
*
|
||||||
|
* @param error
|
||||||
|
*/
|
||||||
|
export const catchAndWrapError = <E extends Error>(error: E) =>
|
||||||
|
Promise.reject(wrapErrorIfNeeded(error));
|
|
@ -47,5 +47,5 @@ export const config: PluginConfigDescriptor<ConfigType> = {
|
||||||
|
|
||||||
export { ConfigType, Plugin, PluginSetup, PluginStart };
|
export { ConfigType, Plugin, PluginSetup, PluginStart };
|
||||||
export { AppClient };
|
export { AppClient };
|
||||||
|
|
||||||
export type { AppRequestContext } from './types';
|
export type { AppRequestContext } from './types';
|
||||||
|
export { EndpointError } from './endpoint/errors';
|
||||||
|
|
|
@ -17,6 +17,14 @@
|
||||||
"winlogbeat-*"
|
"winlogbeat-*"
|
||||||
],
|
],
|
||||||
"privileges": ["manage", "write", "read"]
|
"privileges": ["manage", "write", "read"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"metrics-endpoint.metadata_current_*",
|
||||||
|
".fleet-agents*",
|
||||||
|
".fleet-actions*"
|
||||||
|
],
|
||||||
|
"privileges": ["read"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -22,6 +22,14 @@
|
||||||
{
|
{
|
||||||
"names": [".lists*", ".items*"],
|
"names": [".lists*", ".items*"],
|
||||||
"privileges": ["read", "write"]
|
"privileges": ["read", "write"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"metrics-endpoint.metadata_current_*",
|
||||||
|
".fleet-agents*",
|
||||||
|
".fleet-actions*"
|
||||||
|
],
|
||||||
|
"privileges": ["read"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,7 +15,10 @@
|
||||||
"filebeat-*",
|
"filebeat-*",
|
||||||
"logs-*",
|
"logs-*",
|
||||||
"packetbeat-*",
|
"packetbeat-*",
|
||||||
"winlogbeat-*"
|
"winlogbeat-*",
|
||||||
|
"metrics-endpoint.metadata_current_*",
|
||||||
|
".fleet-agents*",
|
||||||
|
".fleet-actions*"
|
||||||
],
|
],
|
||||||
"privileges": ["all"]
|
"privileges": ["all"]
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,7 +6,10 @@
|
||||||
"names" : [
|
"names" : [
|
||||||
".siem-signals*",
|
".siem-signals*",
|
||||||
".lists*",
|
".lists*",
|
||||||
".items*"
|
".items*",
|
||||||
|
"metrics-endpoint.metadata_current_*",
|
||||||
|
".fleet-agents*",
|
||||||
|
".fleet-actions*"
|
||||||
],
|
],
|
||||||
"privileges" : ["read"]
|
"privileges" : ["read"]
|
||||||
},
|
},
|
||||||
|
|
|
@ -20,6 +20,14 @@
|
||||||
{
|
{
|
||||||
"names": [".siem-signals-*"],
|
"names": [".siem-signals-*"],
|
||||||
"privileges": ["read", "write", "maintenance", "view_index_metadata"]
|
"privileges": ["read", "write", "maintenance", "view_index_metadata"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"metrics-endpoint.metadata_current_*",
|
||||||
|
".fleet-agents*",
|
||||||
|
".fleet-actions*"
|
||||||
|
],
|
||||||
|
"privileges": ["read"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -20,6 +20,14 @@
|
||||||
{
|
{
|
||||||
"names": [".siem-signals-*"],
|
"names": [".siem-signals-*"],
|
||||||
"privileges": ["read", "write", "manage"]
|
"privileges": ["read", "write", "manage"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"metrics-endpoint.metadata_current_*",
|
||||||
|
".fleet-agents*",
|
||||||
|
".fleet-actions*"
|
||||||
|
],
|
||||||
|
"privileges": ["read"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -12,7 +12,10 @@
|
||||||
"filebeat-*",
|
"filebeat-*",
|
||||||
"logs-*",
|
"logs-*",
|
||||||
"packetbeat-*",
|
"packetbeat-*",
|
||||||
"winlogbeat-*"
|
"winlogbeat-*",
|
||||||
|
"metrics-endpoint.metadata_current_*",
|
||||||
|
".fleet-agents*",
|
||||||
|
".fleet-actions*"
|
||||||
],
|
],
|
||||||
"privileges": ["read"]
|
"privileges": ["read"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,10 @@
|
||||||
"filebeat-*",
|
"filebeat-*",
|
||||||
"logs-*",
|
"logs-*",
|
||||||
"packetbeat-*",
|
"packetbeat-*",
|
||||||
"winlogbeat-*"
|
"winlogbeat-*",
|
||||||
|
"metrics-endpoint.metadata_current_*",
|
||||||
|
".fleet-agents*",
|
||||||
|
".fleet-actions*"
|
||||||
],
|
],
|
||||||
"privileges": ["read"]
|
"privileges": ["read"]
|
||||||
}
|
}
|
||||||
|
|
8
x-pack/test/common/services/security_solution/index.ts
Normal file
8
x-pack/test/common/services/security_solution/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './roles_users_utils';
|
|
@ -5,8 +5,8 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { assertUnreachable } from '../../../../plugins/security_solution/common/utility_types';
|
import { assertUnreachable } from '../../../../plugins/security_solution/common';
|
||||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||||
import {
|
import {
|
||||||
t1AnalystUser,
|
t1AnalystUser,
|
||||||
t2AnalystUser,
|
t2AnalystUser,
|
||||||
|
@ -28,6 +28,13 @@ import {
|
||||||
|
|
||||||
import { ROLES } from '../../../../plugins/security_solution/common/test';
|
import { ROLES } from '../../../../plugins/security_solution/common/test';
|
||||||
|
|
||||||
|
export { ROLES };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* creates a security solution centric role and a user (both having the same name)
|
||||||
|
* @param getService
|
||||||
|
* @param role
|
||||||
|
*/
|
||||||
export const createUserAndRole = async (
|
export const createUserAndRole = async (
|
||||||
getService: FtrProviderContext['getService'],
|
getService: FtrProviderContext['getService'],
|
||||||
role: ROLES
|
role: ROLES
|
|
@ -53,7 +53,7 @@ import {
|
||||||
getThresholdRuleForSignalTesting,
|
getThresholdRuleForSignalTesting,
|
||||||
} from '../../utils';
|
} from '../../utils';
|
||||||
import { ROLES } from '../../../../plugins/security_solution/common/test';
|
import { ROLES } from '../../../../plugins/security_solution/common/test';
|
||||||
import { createUserAndRole, deleteUserAndRole } from '../roles_users_utils';
|
import { createUserAndRole, deleteUserAndRole } from '../../../common/services/security_solution';
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default ({ getService }: FtrProviderContext) => {
|
export default ({ getService }: FtrProviderContext) => {
|
||||||
|
|
|
@ -14,7 +14,7 @@ import {
|
||||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||||
import { deleteSignalsIndex } from '../../utils';
|
import { deleteSignalsIndex } from '../../utils';
|
||||||
import { ROLES } from '../../../../plugins/security_solution/common/test';
|
import { ROLES } from '../../../../plugins/security_solution/common/test';
|
||||||
import { createUserAndRole, deleteUserAndRole } from '../roles_users_utils';
|
import { createUserAndRole, deleteUserAndRole } from '../../../common/services/security_solution';
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default ({ getService }: FtrProviderContext) => {
|
export default ({ getService }: FtrProviderContext) => {
|
||||||
|
|
|
@ -32,7 +32,7 @@ import {
|
||||||
getRuleForSignalTestingWithTimestampOverride,
|
getRuleForSignalTestingWithTimestampOverride,
|
||||||
} from '../../utils';
|
} from '../../utils';
|
||||||
import { ROLES } from '../../../../plugins/security_solution/common/test';
|
import { ROLES } from '../../../../plugins/security_solution/common/test';
|
||||||
import { createUserAndRole, deleteUserAndRole } from '../roles_users_utils';
|
import { createUserAndRole, deleteUserAndRole } from '../../../common/services/security_solution';
|
||||||
import { RuleStatusResponse } from '../../../../plugins/security_solution/server/lib/detection_engine/rules/types';
|
import { RuleStatusResponse } from '../../../../plugins/security_solution/server/lib/detection_engine/rules/types';
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
|
|
@ -22,7 +22,7 @@ import {
|
||||||
getIndexNameFromLoad,
|
getIndexNameFromLoad,
|
||||||
waitForIndexToPopulate,
|
waitForIndexToPopulate,
|
||||||
} from '../../utils';
|
} from '../../utils';
|
||||||
import { createUserAndRole, deleteUserAndRole } from '../roles_users_utils';
|
import { createUserAndRole, deleteUserAndRole } from '../../../common/services/security_solution';
|
||||||
|
|
||||||
interface CreateResponse {
|
interface CreateResponse {
|
||||||
index: string;
|
index: string;
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {
|
||||||
import { ROLES } from '../../../../plugins/security_solution/common/test';
|
import { ROLES } from '../../../../plugins/security_solution/common/test';
|
||||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||||
import { createSignalsIndex, deleteSignalsIndex, getIndexNameFromLoad, waitFor } from '../../utils';
|
import { createSignalsIndex, deleteSignalsIndex, getIndexNameFromLoad, waitFor } from '../../utils';
|
||||||
import { createUserAndRole } from '../roles_users_utils';
|
import { createUserAndRole } from '../../../common/services/security_solution';
|
||||||
|
|
||||||
interface CreateResponse {
|
interface CreateResponse {
|
||||||
index: string;
|
index: string;
|
||||||
|
|
|
@ -21,7 +21,7 @@ import {
|
||||||
getIndexNameFromLoad,
|
getIndexNameFromLoad,
|
||||||
waitFor,
|
waitFor,
|
||||||
} from '../../utils';
|
} from '../../utils';
|
||||||
import { createUserAndRole, deleteUserAndRole } from '../roles_users_utils';
|
import { createUserAndRole, deleteUserAndRole } from '../../../common/services/security_solution';
|
||||||
|
|
||||||
interface StatusResponse {
|
interface StatusResponse {
|
||||||
index: string;
|
index: string;
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { DETECTION_ENGINE_SIGNALS_MIGRATION_STATUS_URL } from '../../../../plugi
|
||||||
import { ROLES } from '../../../../plugins/security_solution/common/test';
|
import { ROLES } from '../../../../plugins/security_solution/common/test';
|
||||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||||
import { createSignalsIndex, deleteSignalsIndex, getIndexNameFromLoad } from '../../utils';
|
import { createSignalsIndex, deleteSignalsIndex, getIndexNameFromLoad } from '../../utils';
|
||||||
import { createUserAndRole, deleteUserAndRole } from '../roles_users_utils';
|
import { createUserAndRole, deleteUserAndRole } from '../../../common/services/security_solution';
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default ({ getService }: FtrProviderContext): void => {
|
export default ({ getService }: FtrProviderContext): void => {
|
||||||
|
|
|
@ -27,7 +27,7 @@ import {
|
||||||
waitForRuleSuccessOrStatus,
|
waitForRuleSuccessOrStatus,
|
||||||
getRuleForSignalTesting,
|
getRuleForSignalTesting,
|
||||||
} from '../../utils';
|
} from '../../utils';
|
||||||
import { createUserAndRole, deleteUserAndRole } from '../roles_users_utils';
|
import { createUserAndRole, deleteUserAndRole } from '../../../common/services/security_solution';
|
||||||
import { ROLES } from '../../../../plugins/security_solution/common/test';
|
import { ROLES } from '../../../../plugins/security_solution/common/test';
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { DETECTION_ENGINE_PRIVILEGES_URL } from '../../../../plugins/security_so
|
||||||
|
|
||||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||||
import { ROLES } from '../../../../plugins/security_solution/common/test';
|
import { ROLES } from '../../../../plugins/security_solution/common/test';
|
||||||
import { createUserAndRole, deleteUserAndRole } from '../roles_users_utils';
|
import { createUserAndRole, deleteUserAndRole } from '../../../common/services/security_solution';
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default ({ getService }: FtrProviderContext) => {
|
export default ({ getService }: FtrProviderContext) => {
|
||||||
|
|
|
@ -12,6 +12,9 @@ import { TransformState, TRANSFORM_STATE } from '../../../../plugins/transform/c
|
||||||
import type { TransformStats } from '../../../../plugins/transform/common/types/transform_stats';
|
import type { TransformStats } from '../../../../plugins/transform/common/types/transform_stats';
|
||||||
|
|
||||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||||
|
import { GetTransformsResponseSchema } from '../../../../plugins/transform/common/api_schemas/transforms';
|
||||||
|
import { PostTransformsUpdateRequestSchema } from '../../../../plugins/transform/common/api_schemas/update_transforms';
|
||||||
|
import { TransformPivotConfig } from '../../../../plugins/transform/common/types/transform';
|
||||||
|
|
||||||
export async function asyncForEach(array: any[], callback: Function) {
|
export async function asyncForEach(array: any[], callback: Function) {
|
||||||
for (let index = 0; index < array.length; index++) {
|
for (let index = 0; index < array.length; index++) {
|
||||||
|
@ -174,10 +177,28 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getTransformList(size: number = 10): Promise<GetTransformsResponseSchema> {
|
||||||
|
return (await esSupertest
|
||||||
|
.get(`/_transform`)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => response.body)) as GetTransformsResponseSchema;
|
||||||
|
},
|
||||||
|
|
||||||
async getTransform(transformId: string, expectedCode = 200) {
|
async getTransform(transformId: string, expectedCode = 200) {
|
||||||
return await esSupertest.get(`/_transform/${transformId}`).expect(expectedCode);
|
return await esSupertest.get(`/_transform/${transformId}`).expect(expectedCode);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async updateTransform(
|
||||||
|
transformId: string,
|
||||||
|
updates: Partial<PostTransformsUpdateRequestSchema>
|
||||||
|
): Promise<TransformPivotConfig> {
|
||||||
|
return await esSupertest
|
||||||
|
.post(`/_transform/${transformId}/_update`)
|
||||||
|
.send(updates)
|
||||||
|
.expect(200)
|
||||||
|
.then((response: { body: TransformPivotConfig }) => response.body);
|
||||||
|
},
|
||||||
|
|
||||||
async createTransform(transformId: string, transformConfig: PutTransformsRequestSchema) {
|
async createTransform(transformId: string, transformConfig: PutTransformsRequestSchema) {
|
||||||
log.debug(`Creating transform with id '${transformId}'...`);
|
log.debug(`Creating transform with id '${transformId}'...`);
|
||||||
await esSupertest.put(`/_transform/${transformId}`).send(transformConfig).expect(200);
|
await esSupertest.put(`/_transform/${transformId}`).send(transformConfig).expect(200);
|
||||||
|
|
|
@ -5,120 +5,4 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { assertUnreachable } from '../../../../plugins/security_solution/common/utility_types';
|
export * from '../../../common/services/security_solution/roles_users_utils';
|
||||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
|
||||||
import {
|
|
||||||
t1AnalystUser,
|
|
||||||
t2AnalystUser,
|
|
||||||
hunterUser,
|
|
||||||
ruleAuthorUser,
|
|
||||||
socManagerUser,
|
|
||||||
platformEngineerUser,
|
|
||||||
detectionsAdminUser,
|
|
||||||
readerUser,
|
|
||||||
t1AnalystRole,
|
|
||||||
t2AnalystRole,
|
|
||||||
hunterRole,
|
|
||||||
ruleAuthorRole,
|
|
||||||
socManagerRole,
|
|
||||||
platformEngineerRole,
|
|
||||||
detectionsAdminRole,
|
|
||||||
readerRole,
|
|
||||||
} from '../../../../plugins/security_solution/server/lib/detection_engine/scripts/roles_users';
|
|
||||||
|
|
||||||
import { ROLES } from '../../../../plugins/security_solution/common/test';
|
|
||||||
|
|
||||||
export const createUserAndRole = async (
|
|
||||||
getService: FtrProviderContext['getService'],
|
|
||||||
role: ROLES
|
|
||||||
): Promise<void> => {
|
|
||||||
switch (role) {
|
|
||||||
case ROLES.detections_admin:
|
|
||||||
return postRoleAndUser(
|
|
||||||
ROLES.detections_admin,
|
|
||||||
detectionsAdminRole,
|
|
||||||
detectionsAdminUser,
|
|
||||||
getService
|
|
||||||
);
|
|
||||||
case ROLES.t1_analyst:
|
|
||||||
return postRoleAndUser(ROLES.t1_analyst, t1AnalystRole, t1AnalystUser, getService);
|
|
||||||
case ROLES.t2_analyst:
|
|
||||||
return postRoleAndUser(ROLES.t2_analyst, t2AnalystRole, t2AnalystUser, getService);
|
|
||||||
case ROLES.hunter:
|
|
||||||
return postRoleAndUser(ROLES.hunter, hunterRole, hunterUser, getService);
|
|
||||||
case ROLES.rule_author:
|
|
||||||
return postRoleAndUser(ROLES.rule_author, ruleAuthorRole, ruleAuthorUser, getService);
|
|
||||||
case ROLES.soc_manager:
|
|
||||||
return postRoleAndUser(ROLES.soc_manager, socManagerRole, socManagerUser, getService);
|
|
||||||
case ROLES.platform_engineer:
|
|
||||||
return postRoleAndUser(
|
|
||||||
ROLES.platform_engineer,
|
|
||||||
platformEngineerRole,
|
|
||||||
platformEngineerUser,
|
|
||||||
getService
|
|
||||||
);
|
|
||||||
case ROLES.reader:
|
|
||||||
return postRoleAndUser(ROLES.reader, readerRole, readerUser, getService);
|
|
||||||
default:
|
|
||||||
return assertUnreachable(role);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a roleName and security service this will delete the roleName
|
|
||||||
* and user
|
|
||||||
* @param roleName The user and role to delete with the same name
|
|
||||||
* @param securityService The security service
|
|
||||||
*/
|
|
||||||
export const deleteUserAndRole = async (
|
|
||||||
getService: FtrProviderContext['getService'],
|
|
||||||
roleName: ROLES
|
|
||||||
): Promise<void> => {
|
|
||||||
const securityService = getService('security');
|
|
||||||
await securityService.user.delete(roleName);
|
|
||||||
await securityService.role.delete(roleName);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface UserInterface {
|
|
||||||
password: string;
|
|
||||||
roles: string[];
|
|
||||||
full_name: string;
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RoleInterface {
|
|
||||||
elasticsearch: {
|
|
||||||
cluster: string[];
|
|
||||||
indices: Array<{
|
|
||||||
names: string[];
|
|
||||||
privileges: string[];
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
kibana: Array<{
|
|
||||||
feature: {
|
|
||||||
ml: string[];
|
|
||||||
siem: string[];
|
|
||||||
actions: string[];
|
|
||||||
builtInAlerts: string[];
|
|
||||||
};
|
|
||||||
spaces: string[];
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const postRoleAndUser = async (
|
|
||||||
roleName: string,
|
|
||||||
role: RoleInterface,
|
|
||||||
user: UserInterface,
|
|
||||||
getService: FtrProviderContext['getService']
|
|
||||||
): Promise<void> => {
|
|
||||||
const securityService = getService('security');
|
|
||||||
await securityService.role.create(roleName, {
|
|
||||||
kibana: role.kibana,
|
|
||||||
elasticsearch: role.elasticsearch,
|
|
||||||
});
|
|
||||||
await securityService.user.create(roleName, {
|
|
||||||
password: 'changeme',
|
|
||||||
full_name: user.full_name,
|
|
||||||
roles: user.roles,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import expect from '@kbn/expect';
|
||||||
|
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||||
|
import {
|
||||||
|
createUserAndRole,
|
||||||
|
deleteUserAndRole,
|
||||||
|
ROLES,
|
||||||
|
} from '../../../common/services/security_solution';
|
||||||
|
import { IndexedHostsAndAlertsResponse } from '../../../../plugins/security_solution/common/endpoint/index_data';
|
||||||
|
|
||||||
|
export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||||
|
const PageObjects = getPageObjects(['security', 'endpoint', 'detections', 'hosts']);
|
||||||
|
const testSubjects = getService('testSubjects');
|
||||||
|
const endpointTestResources = getService('endpointTestResources');
|
||||||
|
const policyTestResources = getService('policyTestResources');
|
||||||
|
|
||||||
|
describe('Endpoint permissions:', () => {
|
||||||
|
let indexedData: IndexedHostsAndAlertsResponse;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
// todo: way to force an endpoint to be created in isolated mode so we can check that state in the UI
|
||||||
|
const endpointPackage = await policyTestResources.getEndpointPackage();
|
||||||
|
await endpointTestResources.setMetadataTransformFrequency('1s', endpointPackage.version);
|
||||||
|
indexedData = await endpointTestResources.loadEndpointData();
|
||||||
|
|
||||||
|
// Force a logout so that we start from the login page
|
||||||
|
await PageObjects.security.forceLogout();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await endpointTestResources.unloadEndpointData(indexedData);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run the same set of tests against all of the Security Solution roles
|
||||||
|
for (const role of Object.keys(ROLES) as Array<keyof typeof ROLES>) {
|
||||||
|
describe(`when running with user/role [${role}]`, () => {
|
||||||
|
before(async () => {
|
||||||
|
// create role/user
|
||||||
|
await createUserAndRole(getService, ROLES[role]);
|
||||||
|
|
||||||
|
// log back in with new uer
|
||||||
|
await PageObjects.security.login(role, 'changeme');
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
// Log the user back out
|
||||||
|
await PageObjects.security.forceLogout();
|
||||||
|
|
||||||
|
// delete role/user
|
||||||
|
await deleteUserAndRole(getService, ROLES[role]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT allow access to endpoint management pages', async () => {
|
||||||
|
await PageObjects.endpoint.navigateToEndpointList();
|
||||||
|
await testSubjects.existOrFail('noIngestPermissions');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display endpoint data on Host Details', async () => {
|
||||||
|
const endpoint = indexedData.hosts[0];
|
||||||
|
await PageObjects.hosts.navigateToHostDetails(endpoint.host.name);
|
||||||
|
const endpointSummary = await PageObjects.hosts.hostDetailsEndpointOverviewData();
|
||||||
|
|
||||||
|
expect(endpointSummary['Endpoint integration policy']).to.be(
|
||||||
|
endpoint.Endpoint.policy.applied.name
|
||||||
|
);
|
||||||
|
expect(endpointSummary['Endpoint version']).to.be(endpoint.agent.version);
|
||||||
|
|
||||||
|
// The values for these are calculated, so let's just make sure its not teh default when no data is returned
|
||||||
|
expect(endpointSummary['Policy status']).not.be('—');
|
||||||
|
expect(endpointSummary['Agent status']).not.to.be('—');
|
||||||
|
});
|
||||||
|
|
||||||
|
// FIXME: this area (detections) is unstable and due to time, skipping it.
|
||||||
|
// The page does not always (its intermittent) display with the created roles. Sometimes you get a
|
||||||
|
// "not enought priviliges" and others the data shows up.
|
||||||
|
it.skip('should display endpoint data on Alert Details', async () => {
|
||||||
|
await PageObjects.detections.navigateToAlerts();
|
||||||
|
await PageObjects.detections.openFirstAlertDetailsForHostName(
|
||||||
|
indexedData.hosts[0].host.name
|
||||||
|
);
|
||||||
|
|
||||||
|
const hostAgentStatus = await testSubjects.getVisibleText('rowHostStatus');
|
||||||
|
|
||||||
|
expect(hostAgentStatus).to.eql('Healthy');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
|
@ -18,6 +18,7 @@ export default function (providerContext: FtrProviderContext) {
|
||||||
describe('endpoint', function () {
|
describe('endpoint', function () {
|
||||||
const ingestManager = getService('ingestManager');
|
const ingestManager = getService('ingestManager');
|
||||||
const log = getService('log');
|
const log = getService('log');
|
||||||
|
const endpointTestResources = getService('endpointTestResources');
|
||||||
|
|
||||||
if (!isRegistryEnabled()) {
|
if (!isRegistryEnabled()) {
|
||||||
log.warning('These tests are being run with an external package registry');
|
log.warning('These tests are being run with an external package registry');
|
||||||
|
@ -27,12 +28,17 @@ export default function (providerContext: FtrProviderContext) {
|
||||||
log.info(`Package registry URL for tests: ${registryUrl}`);
|
log.info(`Package registry URL for tests: ${registryUrl}`);
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
|
log.info('calling Fleet setup');
|
||||||
await ingestManager.setup();
|
await ingestManager.setup();
|
||||||
|
|
||||||
|
log.info('installing/upgrading Endpoint fleet package');
|
||||||
|
await endpointTestResources.installOrUpgradeEndpointFleetPackage();
|
||||||
});
|
});
|
||||||
loadTestFile(require.resolve('./endpoint_list'));
|
loadTestFile(require.resolve('./endpoint_list'));
|
||||||
loadTestFile(require.resolve('./policy_details'));
|
loadTestFile(require.resolve('./policy_details'));
|
||||||
loadTestFile(require.resolve('./endpoint_telemetry'));
|
loadTestFile(require.resolve('./endpoint_telemetry'));
|
||||||
loadTestFile(require.resolve('./trusted_apps_list'));
|
loadTestFile(require.resolve('./trusted_apps_list'));
|
||||||
loadTestFile(require.resolve('./fleet_integrations'));
|
loadTestFile(require.resolve('./fleet_integrations'));
|
||||||
|
loadTestFile(require.resolve('./endpoint_permissions'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,8 @@ import { TrustedAppsPageProvider } from './trusted_apps_page';
|
||||||
import { EndpointPageUtils } from './page_utils';
|
import { EndpointPageUtils } from './page_utils';
|
||||||
import { IngestManagerCreatePackagePolicy } from './ingest_manager_create_package_policy_page';
|
import { IngestManagerCreatePackagePolicy } from './ingest_manager_create_package_policy_page';
|
||||||
import { FleetIntegrations } from './fleet_integrations_page';
|
import { FleetIntegrations } from './fleet_integrations_page';
|
||||||
|
import { DetectionsPageObject } from '../../security_solution_ftr/page_objects/detections';
|
||||||
|
import { HostsPageObject } from '../../security_solution_ftr/page_objects/hosts';
|
||||||
|
|
||||||
export const pageObjects = {
|
export const pageObjects = {
|
||||||
...xpackFunctionalPageObjects,
|
...xpackFunctionalPageObjects,
|
||||||
|
@ -21,4 +23,6 @@ export const pageObjects = {
|
||||||
endpointPageUtils: EndpointPageUtils,
|
endpointPageUtils: EndpointPageUtils,
|
||||||
ingestManagerCreatePackagePolicy: IngestManagerCreatePackagePolicy,
|
ingestManagerCreatePackagePolicy: IngestManagerCreatePackagePolicy,
|
||||||
fleetIntegrations: FleetIntegrations,
|
fleetIntegrations: FleetIntegrations,
|
||||||
|
detections: DetectionsPageObject,
|
||||||
|
hosts: HostsPageObject,
|
||||||
};
|
};
|
||||||
|
|
195
x-pack/test/security_solution_endpoint/services/endpoint.ts
Normal file
195
x-pack/test/security_solution_endpoint/services/endpoint.ts
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
/*
|
||||||
|
* 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 { ResponseError } from '@elastic/elasticsearch/lib/errors';
|
||||||
|
import { Client } from '@elastic/elasticsearch';
|
||||||
|
import { FtrService } from '../../functional/ftr_provider_context';
|
||||||
|
import {
|
||||||
|
metadataCurrentIndexPattern,
|
||||||
|
metadataTransformPrefix,
|
||||||
|
} from '../../../plugins/security_solution/common/endpoint/constants';
|
||||||
|
import { EndpointError } from '../../../plugins/security_solution/server';
|
||||||
|
import {
|
||||||
|
deleteIndexedHostsAndAlerts,
|
||||||
|
IndexedHostsAndAlertsResponse,
|
||||||
|
indexHostsAndAlerts,
|
||||||
|
} from '../../../plugins/security_solution/common/endpoint/index_data';
|
||||||
|
import { TransformPivotConfig } from '../../../plugins/transform/common/types/transform';
|
||||||
|
import { GetTransformsResponseSchema } from '../../../plugins/transform/common/api_schemas/transforms';
|
||||||
|
import { catchAndWrapError } from '../../../plugins/security_solution/server/endpoint/utils';
|
||||||
|
import { installOrUpgradeEndpointFleetPackage } from '../../../plugins/security_solution/common/endpoint/data_loaders/setup_fleet_for_endpoint';
|
||||||
|
|
||||||
|
export class EndpointTestResources extends FtrService {
|
||||||
|
private readonly esClient = this.ctx.getService('es');
|
||||||
|
private readonly retry = this.ctx.getService('retry');
|
||||||
|
private readonly kbnClient = this.ctx.getService('kibanaServer');
|
||||||
|
private readonly transform = this.ctx.getService('transform');
|
||||||
|
|
||||||
|
private generateTransformId(endpointPackageVersion?: string): string {
|
||||||
|
return `${metadataTransformPrefix}-${endpointPackageVersion ?? ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the information for the endpoint transform
|
||||||
|
*
|
||||||
|
* @param [endpointPackageVersion] if set, it will be used to get the specific transform this this package version. Else just returns first one found
|
||||||
|
*/
|
||||||
|
async getTransform(endpointPackageVersion?: string): Promise<TransformPivotConfig> {
|
||||||
|
const transformId = this.generateTransformId(endpointPackageVersion);
|
||||||
|
let transform: TransformPivotConfig | undefined;
|
||||||
|
|
||||||
|
if (endpointPackageVersion) {
|
||||||
|
await this.transform.api.waitForTransformToExist(transformId);
|
||||||
|
|
||||||
|
transform = ((
|
||||||
|
await this.transform.api
|
||||||
|
.getTransform(transformId)
|
||||||
|
.catch(catchAndWrapError)
|
||||||
|
.then((response: { body: GetTransformsResponseSchema }) => response)
|
||||||
|
).body as GetTransformsResponseSchema).transforms[0];
|
||||||
|
} else {
|
||||||
|
transform = (
|
||||||
|
await this.transform.api.getTransformList(100).catch(catchAndWrapError)
|
||||||
|
).transforms.find((t) => t.id.startsWith(transformId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!transform) {
|
||||||
|
throw new EndpointError('Endpoint metadata transform not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setMetadataTransformFrequency(
|
||||||
|
frequency: string,
|
||||||
|
/** Used to update the transform installed with the given package version */
|
||||||
|
endpointPackageVersion?: string
|
||||||
|
): Promise<void> {
|
||||||
|
const transform = await this.getTransform(endpointPackageVersion).catch(catchAndWrapError);
|
||||||
|
await this.transform.api.updateTransform(transform.id, { frequency }).catch(catchAndWrapError);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads endpoint host/alert/event data into elasticsearch
|
||||||
|
* @param [options]
|
||||||
|
* @param [options.numHosts=1] Number of Endpoint Hosts to be loaded
|
||||||
|
* @param [options.numHostDocs=1] Number of Document to be loaded per Endpoint Host (Endpoint hosts index uses a append-only index)
|
||||||
|
* @param [options.alertsPerHost=1] Number of Alerts and Events to be loaded per Endpoint Host
|
||||||
|
* @param [options.enableFleetIntegration=true] When set to `true`, Fleet data will also be loaded (ex. Integration Policies, Agent Policies, "fake" Agents)
|
||||||
|
* @param [options.generatorSeed='seed`] The seed to be used by the data generator. Important in order to ensure the same data is generated on very run.
|
||||||
|
* @param [options.waitUntilTransformed=true] If set to `true`, the data loading process will wait until the endpoint hosts metadata is processd by the transform
|
||||||
|
*/
|
||||||
|
async loadEndpointData(
|
||||||
|
options: Partial<{
|
||||||
|
numHosts: number;
|
||||||
|
numHostDocs: number;
|
||||||
|
alertsPerHost: number;
|
||||||
|
enableFleetIntegration: boolean;
|
||||||
|
generatorSeed: string;
|
||||||
|
waitUntilTransformed: boolean;
|
||||||
|
}> = {}
|
||||||
|
): Promise<IndexedHostsAndAlertsResponse> {
|
||||||
|
const {
|
||||||
|
numHosts = 1,
|
||||||
|
numHostDocs = 1,
|
||||||
|
alertsPerHost = 1,
|
||||||
|
enableFleetIntegration = true,
|
||||||
|
generatorSeed = 'seed',
|
||||||
|
waitUntilTransformed = true,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
// load data into the system
|
||||||
|
const indexedData = await indexHostsAndAlerts(
|
||||||
|
this.esClient as Client,
|
||||||
|
this.kbnClient,
|
||||||
|
generatorSeed,
|
||||||
|
numHosts,
|
||||||
|
numHostDocs,
|
||||||
|
'metrics-endpoint.metadata-default',
|
||||||
|
'metrics-endpoint.policy-default',
|
||||||
|
'logs-endpoint.events.process-default',
|
||||||
|
'logs-endpoint.alerts-default',
|
||||||
|
alertsPerHost,
|
||||||
|
enableFleetIntegration
|
||||||
|
);
|
||||||
|
|
||||||
|
if (waitUntilTransformed) {
|
||||||
|
await this.waitForEndpoints(indexedData.hosts.map((host) => host.agent.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return indexedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the loaded data created via `loadEndpointData()`
|
||||||
|
* @param indexedData
|
||||||
|
*/
|
||||||
|
async unloadEndpointData(indexedData: IndexedHostsAndAlertsResponse) {
|
||||||
|
return deleteIndexedHostsAndAlerts(this.esClient as Client, this.kbnClient, indexedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits for endpoints to show up on the `metadata-current` index.
|
||||||
|
* Optionally, specific endpoint IDs (agent.id) can be provided to ensure those specific ones show up.
|
||||||
|
*
|
||||||
|
* @param [ids] optional list of ids to check for. If empty, it will just check if data exists in the index
|
||||||
|
*/
|
||||||
|
async waitForEndpoints(ids: string[] = []) {
|
||||||
|
const body = ids.length
|
||||||
|
? {
|
||||||
|
query: {
|
||||||
|
bool: {
|
||||||
|
filter: [
|
||||||
|
{
|
||||||
|
terms: {
|
||||||
|
'agent.id': ids,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
query: {
|
||||||
|
match_all: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// If we have a specific number of endpoint hosts to check for, then use that number,
|
||||||
|
// else we just want to make sure the index has data, thus just having one in the index will do
|
||||||
|
const size = ids.length || 1;
|
||||||
|
|
||||||
|
await this.retry.waitFor('wait for endpoints hosts', async () => {
|
||||||
|
try {
|
||||||
|
const searchResponse = await this.esClient.search({
|
||||||
|
index: metadataCurrentIndexPattern,
|
||||||
|
size,
|
||||||
|
body,
|
||||||
|
rest_total_hits_as_int: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return searchResponse.body.hits.total === size;
|
||||||
|
} catch (error) {
|
||||||
|
// We ignore 404's (index might not exist)
|
||||||
|
if (error instanceof ResponseError && error.statusCode === 404) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap the ES error so that we get a good stack trace
|
||||||
|
throw new EndpointError(error.message, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* installs (or upgrades) the Endpoint Fleet package
|
||||||
|
* (NOTE: ensure that fleet is setup first before calling this function)
|
||||||
|
*/
|
||||||
|
async installOrUpgradeEndpointFleetPackage(): Promise<void> {
|
||||||
|
return installOrUpgradeEndpointFleetPackage(this.kbnClient);
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,6 +23,7 @@ import { Immutable } from '../../../plugins/security_solution/common/endpoint/ty
|
||||||
|
|
||||||
// NOTE: import path below should be the deep path to the actual module - else we get CI errors
|
// NOTE: import path below should be the deep path to the actual module - else we get CI errors
|
||||||
import { pkgKeyFromPackageInfo } from '../../../plugins/fleet/public/services/pkg_key_from_package_info';
|
import { pkgKeyFromPackageInfo } from '../../../plugins/fleet/public/services/pkg_key_from_package_info';
|
||||||
|
import { EndpointError } from '../../../plugins/security_solution/server';
|
||||||
|
|
||||||
const INGEST_API_ROOT = '/api/fleet';
|
const INGEST_API_ROOT = '/api/fleet';
|
||||||
const INGEST_API_AGENT_POLICIES = `${INGEST_API_ROOT}/agent_policies`;
|
const INGEST_API_AGENT_POLICIES = `${INGEST_API_ROOT}/agent_policies`;
|
||||||
|
@ -119,6 +120,19 @@ export function EndpointPolicyTestResourcesProvider({ getService }: FtrProviderC
|
||||||
return pkgKeyFromPackageInfo((await retrieveEndpointPackageInfo())!);
|
return pkgKeyFromPackageInfo((await retrieveEndpointPackageInfo())!);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the currently installed endpoint package
|
||||||
|
*/
|
||||||
|
async getEndpointPackage(): Promise<Immutable<GetPackagesResponse['response'][0]>> {
|
||||||
|
const endpointPackage = await retrieveEndpointPackageInfo();
|
||||||
|
|
||||||
|
if (!endpointPackage) {
|
||||||
|
throw new EndpointError(`endpoint package not instealled`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return endpointPackage;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the full Agent policy, which mirrors what the Elastic Agent would get
|
* Retrieves the full Agent policy, which mirrors what the Elastic Agent would get
|
||||||
* once they checkin.
|
* once they checkin.
|
||||||
|
|
|
@ -9,9 +9,11 @@ import { services as xPackFunctionalServices } from '../../functional/services';
|
||||||
import { EndpointPolicyTestResourcesProvider } from './endpoint_policy';
|
import { EndpointPolicyTestResourcesProvider } from './endpoint_policy';
|
||||||
import { IngestManagerProvider } from '../../common/services/ingest_manager';
|
import { IngestManagerProvider } from '../../common/services/ingest_manager';
|
||||||
import { EndpointTelemetryTestResourcesProvider } from './endpoint_telemetry';
|
import { EndpointTelemetryTestResourcesProvider } from './endpoint_telemetry';
|
||||||
|
import { EndpointTestResources } from './endpoint';
|
||||||
|
|
||||||
export const services = {
|
export const services = {
|
||||||
...xPackFunctionalServices,
|
...xPackFunctionalServices,
|
||||||
|
endpointTestResources: EndpointTestResources,
|
||||||
policyTestResources: EndpointPolicyTestResourcesProvider,
|
policyTestResources: EndpointPolicyTestResourcesProvider,
|
||||||
telemetryTestResources: EndpointTelemetryTestResourcesProvider,
|
telemetryTestResources: EndpointTelemetryTestResourcesProvider,
|
||||||
ingestManager: IngestManagerProvider,
|
ingestManager: IngestManagerProvider,
|
||||||
|
|
|
@ -12,11 +12,17 @@ export class DetectionsPageObject extends FtrService {
|
||||||
private readonly find = this.ctx.getService('find');
|
private readonly find = this.ctx.getService('find');
|
||||||
private readonly common = this.ctx.getPageObject('common');
|
private readonly common = this.ctx.getPageObject('common');
|
||||||
private readonly testSubjects = this.ctx.getService('testSubjects');
|
private readonly testSubjects = this.ctx.getService('testSubjects');
|
||||||
|
private readonly headerPageObjects = this.ctx.getPageObject('header');
|
||||||
|
|
||||||
async navigateHome(): Promise<void> {
|
async navigateHome(): Promise<void> {
|
||||||
await this.navigateToDetectionsPage();
|
await this.navigateToDetectionsPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async navigateToAlerts(): Promise<void> {
|
||||||
|
await this.navigateToDetectionsPage('alerts');
|
||||||
|
await this.headerPageObjects.waitUntilLoadingHasFinished();
|
||||||
|
}
|
||||||
|
|
||||||
async navigateToRules(): Promise<void> {
|
async navigateToRules(): Promise<void> {
|
||||||
await this.navigateToDetectionsPage('rules');
|
await this.navigateToDetectionsPage('rules');
|
||||||
}
|
}
|
||||||
|
@ -139,6 +145,39 @@ export class DetectionsPageObject extends FtrService {
|
||||||
await this.common.clickAndValidate('thresholdRuleType', 'input');
|
await this.common.clickAndValidate('thresholdRuleType', 'input');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async ensureOnAlertsPage(): Promise<void> {
|
||||||
|
await this.testSubjects.existOrFail('detectionsAlertsPage');
|
||||||
|
}
|
||||||
|
|
||||||
|
async openFirstAlertDetailsForHostName(hostName: string): Promise<void> {
|
||||||
|
await this.ensureOnAlertsPage();
|
||||||
|
|
||||||
|
let foundAndHandled = false;
|
||||||
|
|
||||||
|
// Get all event rows
|
||||||
|
const allEvents = await this.testSubjects.findAll('event');
|
||||||
|
|
||||||
|
for (const eventRow of allEvents) {
|
||||||
|
const hostNameButton = await this.testSubjects.findDescendant(
|
||||||
|
'host-details-button',
|
||||||
|
eventRow
|
||||||
|
);
|
||||||
|
const eventRowHostName = (await hostNameButton.getVisibleText()).trim();
|
||||||
|
|
||||||
|
if (eventRowHostName === hostName) {
|
||||||
|
const expandAlertButton = await this.testSubjects.findDescendant('expand-event', eventRow);
|
||||||
|
await expandAlertButton.click();
|
||||||
|
await this.testSubjects.existOrFail('eventDetails');
|
||||||
|
foundAndHandled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundAndHandled) {
|
||||||
|
throw new Error(`no alerts found for host: ${hostName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async navigateToDetectionsPage(path: string = ''): Promise<void> {
|
private async navigateToDetectionsPage(path: string = ''): Promise<void> {
|
||||||
const subUrl = `detections${path ? `/${path}` : ''}`;
|
const subUrl = `detections${path ? `/${path}` : ''}`;
|
||||||
await this.common.navigateToUrl('securitySolution', subUrl, {
|
await this.common.navigateToUrl('securitySolution', subUrl, {
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* 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 { FtrService } from '../../../functional/ftr_provider_context';
|
||||||
|
import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper';
|
||||||
|
|
||||||
|
export class HostsPageObject extends FtrService {
|
||||||
|
private readonly pageObjects = this.ctx.getPageObjects(['common', 'header']);
|
||||||
|
private readonly testSubjects = this.ctx.getService('testSubjects');
|
||||||
|
|
||||||
|
async navigateToHostDetails(hostName: string): Promise<void> {
|
||||||
|
await this.pageObjects.common.navigateToUrl('securitySolution', `hosts/${hostName}`, {
|
||||||
|
shouldUseHashForSubUrl: false,
|
||||||
|
});
|
||||||
|
await this.pageObjects.header.waitUntilLoadingHasFinished();
|
||||||
|
}
|
||||||
|
|
||||||
|
async ensureOnHostDetails(): Promise<void> {
|
||||||
|
await this.testSubjects.existOrFail('hostDetailsPage');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an object with the Endpoint overview data, where the keys are the visible labels in the UI.
|
||||||
|
* Must be on the Host details in order for this page object to work
|
||||||
|
*/
|
||||||
|
async hostDetailsEndpointOverviewData(): Promise<Record<string, string>> {
|
||||||
|
await this.ensureOnHostDetails();
|
||||||
|
const endpointDescriptionLists: WebElementWrapper[] = await this.testSubjects.findAll(
|
||||||
|
'endpoint-overview'
|
||||||
|
);
|
||||||
|
|
||||||
|
const data: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (const dlElement of endpointDescriptionLists) {
|
||||||
|
const $ = await dlElement.parseDomContent();
|
||||||
|
|
||||||
|
const title = $('dt')
|
||||||
|
.text()
|
||||||
|
.replace(/ /g, '')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
// The value could be draggable, in which case we need to grab the value displayed from a deeper element
|
||||||
|
const $ddElement = $('dd');
|
||||||
|
const $valueContainer = $ddElement.find('.draggable-keyboard-wrapper .euiToolTipAnchor');
|
||||||
|
|
||||||
|
const value = ($valueContainer.length > 0 ? $valueContainer : $ddElement)
|
||||||
|
.text()
|
||||||
|
.replace(/ /g, '')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
data[title] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue