[Security Solution][Endpoint] Allow apiKey to be used when creating KBN/ES clients for use in scripts (#166187)

## Summary

- Adds support for `apiKey` to the CLI service methods:
`createKbnClient()`, `createEsClient()` and `createRuntimeServices()`
- Note: no existing CLI tools have been changed with this PR to support
`apiKey`. Only adding support to the above service methods so that they
can be used by existing or new CLI utilities
This commit is contained in:
Paul Tavares 2023-09-12 10:06:10 -04:00 committed by GitHub
parent 6fc5c806ed
commit 5a3a3c8039
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 73 additions and 18 deletions

3
.github/CODEOWNERS vendored
View file

@ -1304,8 +1304,7 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib
/x-pack/plugins/security_solution/server/lists_integration/endpoint/ @elastic/security-defend-workflows
/x-pack/plugins/security_solution/server/lib/license/ @elastic/security-defend-workflows
/x-pack/plugins/security_solution/server/fleet_integration/ @elastic/security-defend-workflows
/x-pack/plugins/security_solution/scripts/endpoint/event_filters/ @elastic/security-defend-workflows
/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/ @elastic/security-defend-workflows
/x-pack/plugins/security_solution/scripts/endpoint/ @elastic/security-defend-workflows
/x-pack/test/security_solution_endpoint/ @elastic/security-defend-workflows
/x-pack/test/security_solution_endpoint_api_int/ @elastic/security-defend-workflows
/x-pack/test_serverless/shared/lib/security/kibana_roles/ @elastic/security-defend-workflows

View file

@ -37,6 +37,7 @@ import type {
import nodeFetch from 'node-fetch';
import semver from 'semver';
import axios from 'axios';
import { fetchKibanaStatus } from './stack_services';
import { catchAxiosErrorFormatAndThrow } from './format_axios_error';
import { FleetAgentGenerator } from '../../../common/endpoint/data_generators/fleet_agent_generator';
@ -249,7 +250,7 @@ export const fetchAgentPolicyList = async (
export const getAgentVersionMatchingCurrentStack = async (
kbnClient: KbnClient
): Promise<string> => {
const kbnStatus = await kbnClient.status.get();
const kbnStatus = await fetchKibanaStatus(kbnClient);
const agentVersions = await axios
.get('https://artifacts-api.elastic.co/v1/versions')
.then((response) => map(response.data.versions, (version) => version.split('-SNAPSHOT')[0]));

View file

@ -7,10 +7,14 @@
import { Client } from '@elastic/elasticsearch';
import { ToolingLog } from '@kbn/tooling-log';
import type { KbnClientOptions } from '@kbn/test';
import { KbnClient } from '@kbn/test';
import type { StatusResponse } from '@kbn/core-status-common-internal';
import pRetry from 'p-retry';
import nodeFetch from 'node-fetch';
import type { ReqOptions } from '@kbn/test/src/kbn_client/kbn_client_requester';
import { type AxiosResponse } from 'axios';
import type { ClientOptions } from '@elastic/elasticsearch/lib/client';
import { catchAxiosErrorFormatAndThrow } from './format_axios_error';
import { isLocalhost } from './is_localhost';
import { getLocalhostRealIp } from './localhost_services';
@ -24,6 +28,7 @@ export interface RuntimeServices {
username: string;
password: string;
}>;
apiKey: string;
localhostRealIp: string;
kibana: {
url: string;
@ -51,6 +56,8 @@ interface CreateRuntimeServicesOptions {
fleetServerUrl?: string;
username: string;
password: string;
/** If defined, both `username` and `password` will be ignored */
apiKey?: string;
/** If undefined, ES username defaults to `username` */
esUsername?: string;
/** If undefined, ES password defaults to `password` */
@ -59,12 +66,41 @@ interface CreateRuntimeServicesOptions {
asSuperuser?: boolean;
}
class KbnClientExtended extends KbnClient {
private readonly apiKey: string | undefined;
constructor({ apiKey, url, ...options }: KbnClientOptions & { apiKey?: string }) {
super({
...options,
url: apiKey ? buildUrlWithCredentials(url, '', '') : url,
});
this.apiKey = apiKey;
}
async request<T>(options: ReqOptions): Promise<AxiosResponse<T>> {
const headers: ReqOptions['headers'] = {
...(options.headers ?? {}),
};
if (this.apiKey) {
headers.Authorization = `ApiKey ${this.apiKey}`;
}
return super.request({
...options,
headers,
});
}
}
export const createRuntimeServices = async ({
kibanaUrl,
elasticsearchUrl,
fleetServerUrl = 'https://localhost:8220',
username: _username,
password: _password,
apiKey,
esUsername,
esPassword,
log = new ToolingLog({ level: 'info', writeTo: process.stdout }),
@ -97,15 +133,17 @@ export const createRuntimeServices = async ({
const fleetURL = new URL(fleetServerUrl);
return {
kbnClient: createKbnClient({ log, url: kibanaUrl, username, password }),
kbnClient: createKbnClient({ log, url: kibanaUrl, username, password, apiKey }),
esClient: createEsClient({
log,
url: elasticsearchUrl,
username: esUsername ?? username,
password: esPassword ?? password,
apiKey,
}),
log,
localhostRealIp: await getLocalhostRealIp(),
apiKey: apiKey ?? '',
user: {
username,
password,
@ -148,40 +186,54 @@ export const createEsClient = ({
url,
username,
password,
apiKey,
log,
}: {
url: string;
username: string;
password: string;
/** If defined, both `username` and `password` will be ignored */
apiKey?: string;
log?: ToolingLog;
}): Client => {
const esUrl = buildUrlWithCredentials(url, username, password);
const clientOptions: ClientOptions = {
node: buildUrlWithCredentials(url, apiKey ? '' : username, apiKey ? '' : password),
};
if (log) {
log.verbose(`Creating Elasticsearch client with URL: ${esUrl}`);
if (apiKey) {
clientOptions.auth = { apiKey };
}
return new Client({ node: esUrl });
if (log) {
log.verbose(`Creating Elasticsearch client options: ${JSON.stringify(clientOptions)}`);
}
return new Client(clientOptions);
};
export const createKbnClient = ({
url,
username,
password,
apiKey,
log = new ToolingLog(),
}: {
url: string;
username: string;
password: string;
/** If defined, both `username` and `password` will be ignored */
apiKey?: string;
log?: ToolingLog;
}): KbnClient => {
const kbnUrl = buildUrlWithCredentials(url, username, password);
if (log) {
log.verbose(`Creating Kibana client with URL: ${kbnUrl}`);
log.verbose(
`Creating Kibana client with URL: ${kbnUrl} ${apiKey ? ` + ApiKey: ${apiKey}` : ''}`
);
}
return new KbnClient({ log, url: kbnUrl });
return new KbnClientExtended({ log, url: kbnUrl, apiKey });
};
/**
@ -189,14 +241,7 @@ export const createKbnClient = ({
* @param kbnClient
*/
export const fetchStackVersion = async (kbnClient: KbnClient): Promise<string> => {
const status = (
await kbnClient
.request<StatusResponse>({
method: 'GET',
path: '/api/status',
})
.catch(catchAxiosErrorFormatAndThrow)
).data;
const status = await fetchKibanaStatus(kbnClient);
if (!status?.version?.number) {
throw new Error(
@ -207,6 +252,16 @@ export const fetchStackVersion = async (kbnClient: KbnClient): Promise<string> =
return status.version.number;
};
export const fetchKibanaStatus = async (kbnClient: KbnClient): Promise<StatusResponse> => {
return kbnClient
.request<StatusResponse>({
method: 'GET',
path: '/api/status',
})
.catch(catchAxiosErrorFormatAndThrow)
.then((response) => response.data);
};
/**
* Checks to ensure Kibana is up and running
* @param kbnUrl