Skip to content

Commit

Permalink
[Security Solution][Endpoint] Add support for spaces to DEV CLI scrip…
Browse files Browse the repository at this point in the history
…ts (#192525)

## Summary

The following changes were made to dev's CLI tooling:

- support for space ID (`--spaceId`) was added to the following CLI [dev
scripts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/scripts/endpoint):
    - `run_endpoint_agent.js`
    - `run_sentinelone_host.js`
- Support API key (`--apiKey`) was dded to the `run_sentinelone_host.js`
CLI dev script (can now run script against cloud intances)
- Fleet Agent Policies created via our scripting common methods will now
set the `namespace` on the agent policy to match the active space
- A few areas of scripting were also updated so that Integration
Policies no longer define a `namespace`, thus allowing for it to default
to the Agent Policy `namespace` value
- SentinelOne SIEM rule, created when the `run_sentinelone_host` script
is run, will now set the namespace on the index patterns (retrieved from
the integration policy created)
- Ensures that the rule only pulls data from the scope (`namespace`) the
integration policy is setup with
- The space id was added to the VM name when creating an Endpoint or
Sentinelone VM (will help to identify what space a VM belongs to)
  • Loading branch information
paul-tavares authored Sep 13, 2024
1 parent c1b7d82 commit 25225c3
Show file tree
Hide file tree
Showing 12 changed files with 287 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { kibanaPackageJson } from '@kbn/repo-info';
import type { KbnClient } from '@kbn/test';
import type { ToolingLog } from '@kbn/tooling-log';
import { fetchActiveSpace } from './spaces';
import { isServerlessKibanaFlavor } from '../../../common/endpoint/utils/kibana_status';
import { fetchFleetLatestAvailableAgentVersion } from '../../../common/endpoint/utils/fetch_fleet_version';
import { prefixedOutputLogger } from './utils';
Expand Down Expand Up @@ -66,8 +67,10 @@ export const createAndEnrollEndpointHost = async ({
agentVersion = await fetchFleetLatestAvailableAgentVersion(kbnClient);
}
}
const activeSpaceId = (await fetchActiveSpace(kbnClient)).id;
const isRunningInCI = Boolean(process.env.CI);
const vmName = hostname ?? `test-host-${Math.random().toString().substring(2, 6)}`;
const vmName =
hostname ?? `test-host-${activeSpaceId}-${Math.random().toString().substring(2, 6)}`;
const { url: agentUrl } = await getAgentDownloadUrl(agentVersion, useClosestVersionMatch, log);
const agentDownload = isRunningInCI ? await downloadAndStoreAgent(agentUrl) : undefined;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ import execa from 'execa';
import chalk from 'chalk';
import assert from 'assert';
import pRetry from 'p-retry';
import type { AgentPolicy, CreateAgentPolicyResponse, Output } from '@kbn/fleet-plugin/common';
import type { Output } from '@kbn/fleet-plugin/common';
import {
AGENT_POLICY_API_ROUTES,
API_VERSIONS,
FLEET_SERVER_PACKAGE,
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
Expand Down Expand Up @@ -47,18 +46,23 @@ import { createToolingLogger } from '../../../../common/endpoint/data_loaders/ut
import type { FormattedAxiosError } from '../../../../common/endpoint/format_axios_error';
import { catchAxiosErrorFormatAndThrow } from '../../../../common/endpoint/format_axios_error';
import {
createAgentPolicy,
createIntegrationPolicy,
ensureFleetSetup,
fetchFleetOutputs,
fetchFleetServerHostList,
fetchFleetServerUrl,
fetchIntegrationPolicyList,
fetchPackageInfo,
generateFleetServiceToken,
getAgentVersionMatchingCurrentStack,
getFleetElasticsearchOutputHost,
randomAgentPolicyName,
waitForHostToEnroll,
} from '../fleet_services';
import { getLocalhostRealIp } from '../network_services';
import { isLocalhost } from '../is_localhost';
import { fetchActiveSpace } from '../spaces';

export const FLEET_SERVER_CUSTOM_CONFIG = resolve(__dirname, './fleet_server.yml');

Expand Down Expand Up @@ -195,30 +199,54 @@ const getOrCreateFleetServerAgentPolicyId = async (

log.info(`Creating new Fleet Server policy`);

const createdFleetServerPolicy: AgentPolicy = await kbnClient
.request<CreateAgentPolicyResponse>({
method: 'POST',
path: AGENT_POLICY_API_ROUTES.CREATE_PATTERN,
headers: { 'elastic-api-version': '2023-10-31' },
body: {
name: `Fleet Server policy (${Math.random().toString(32).substring(2)})`,
description: `Created by CLI Tool via: ${__filename}`,
namespace: 'default',
monitoring_enabled: ['logs', 'metrics'],
// This will ensure the Fleet Server integration policy
// is also created and added to the agent policy
has_fleet_server: true,
const [activeSpaceId, fleetServerPackageInfo] = await Promise.all([
fetchActiveSpace(kbnClient).then((space) => space.id),
fetchPackageInfo(kbnClient, 'fleet_server'),
]);

const agentPolicy = await createAgentPolicy({
kbnClient,
policy: {
namespace: activeSpaceId,
name: randomAgentPolicyName('Fleet server'),
monitoring_enabled: ['logs', 'metrics'],
},
});

log.verbose(agentPolicy);

const fleetServerIntegrationPolicy = await createIntegrationPolicy(kbnClient, {
name: randomAgentPolicyName('Fleet server integration'),
description: `Created from script at [${__filename}]`,
namespace: activeSpaceId,
policy_ids: [agentPolicy.id],
enabled: true,
force: false,
inputs: [
{
type: 'fleet-server',
policy_template: 'fleet_server',
enabled: true,
streams: [],
vars: {
max_agents: { type: 'integer' },
max_connections: { type: 'integer' },
custom: { value: '', type: 'yaml' },
},
},
})
.then((response) => response.data.item)
.catch(catchAxiosErrorFormatAndThrow);
],
package: {
name: 'fleet_server',
title: fleetServerPackageInfo.title,
version: fleetServerPackageInfo.version,
},
});

log.verbose(fleetServerIntegrationPolicy);

log.info(
`Agent Policy created: ${createdFleetServerPolicy.name} (${createdFleetServerPolicy.id})`
);
log.verbose(createdFleetServerPolicy);
log.info(`Agent Policy created: ${agentPolicy.name} (${agentPolicy.id})`);

return createdFleetServerPolicy.id;
return agentPolicy.id;
});
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import semver from 'semver';
import axios from 'axios';
import { userInfo } from 'os';
import pRetry from 'p-retry';
import { fetchActiveSpace } from './spaces';
import { fetchKibanaStatus } from '../../../common/endpoint/utils/kibana_status';
import { isFleetServerRunning } from './fleet_server/fleet_server_services';
import { getEndpointPackageInfo } from '../../../common/endpoint/utils/package';
Expand All @@ -83,11 +84,14 @@ const DEFAULT_AGENT_POLICY_NAME = `${CURRENT_USERNAME} test policy`;
/** A Fleet agent policy that includes integrations that don't actually require an agent to run on a host. Example: SenttinelOne */
export const DEFAULT_AGENTLESS_INTEGRATIONS_AGENT_POLICY_NAME = `${CURRENT_USERNAME} - agentless integrations`;

const randomAgentPolicyName = (() => {
/**
* Generate a random policy name
*/
export const randomAgentPolicyName = (() => {
let counter = fleetGenerator.randomN(100);

return (): string => {
return `agent policy - ${fleetGenerator.randomString(10)}_${counter++}`;
return (prefix: string = 'agent policy'): string => {
return `${prefix} - ${fleetGenerator.randomString(10)}_${counter++}`;
};
})();

Expand Down Expand Up @@ -814,7 +818,7 @@ export const createAgentPolicy = async ({
const body: CreateAgentPolicyRequest['body'] = policy ?? {
name: randomAgentPolicyName(),
description: `Policy created by security solution tooling: ${__filename}`,
namespace: 'default',
namespace: (await fetchActiveSpace(kbnClient)).id,
monitoring_enabled: ['logs', 'metrics'],
};

Expand Down Expand Up @@ -862,12 +866,13 @@ export const getOrCreateDefaultAgentPolicy = async ({

log.info(`Creating default test/dev Fleet agent policy with name: [${policyName}]`);

const spaceId = (await fetchActiveSpace(kbnClient)).id;
const newAgentPolicy = await createAgentPolicy({
kbnClient,
policy: {
name: policyName,
description: `Policy created by security solution tooling: ${__filename}`,
namespace: 'default',
namespace: spaceId,
monitoring_enabled: ['logs', 'metrics'],
},
});
Expand Down Expand Up @@ -989,7 +994,6 @@ export const addSentinelOneIntegrationToAgentPolicy = async ({
return createIntegrationPolicy(kbnClient, {
name: integrationPolicyName,
description: `Created by script: ${__filename}`,
namespace: 'default',
policy_id: agentPolicyId,
policy_ids: [agentPolicyId],
enabled: true,
Expand Down Expand Up @@ -1217,7 +1221,6 @@ export const addEndpointIntegrationToAgentPolicy = async ({
const newIntegrationPolicy = await createIntegrationPolicy(kbnClient, {
name,
description: `Created by: ${__filename}`,
namespace: 'default',
policy_id: agentPolicyId,
policy_ids: [agentPolicyId],
enabled: true,
Expand Down Expand Up @@ -1352,3 +1355,17 @@ export const fetchAllEndpointIntegrationPolicyListIds = async (

return policyIds;
};

/**
* Calls the Fleet internal API to enable space awareness
* @param kbnClient
*/
export const enableFleetSpaceAwareness = memoize(async (kbnClient: KbnClient): Promise<void> => {
await kbnClient
.request({
path: '/internal/fleet/enable_space_awareness',
headers: { 'Elastic-Api-Version': '1' },
method: 'POST',
})
.catch(catchAxiosErrorFormatAndThrow);
});
85 changes: 85 additions & 0 deletions x-pack/plugins/security_solution/scripts/endpoint/common/spaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { KbnClient } from '@kbn/test';
import { AxiosError } from 'axios';
import type { ToolingLog } from '@kbn/tooling-log';
import type { Space } from '@kbn/spaces-plugin/common';
import { DEFAULT_SPACE_ID, getSpaceIdFromPath } from '@kbn/spaces-plugin/common';
import { memoize } from 'lodash';
import { createToolingLogger } from '../../../common/endpoint/data_loaders/utils';
import { catchAxiosErrorFormatAndThrow } from '../../../common/endpoint/format_axios_error';

/**
* Check that a given space id exists in Kibana and created it if not.
*/
export const ensureSpaceIdExists = async (
kbnClient: KbnClient,
/** If space id is not defined, it will be derived from the `KbnClient` kibana url */
spaceId: string = getSpaceIdFromKbnClientUrl(kbnClient).spaceId,
{ log = createToolingLogger() }: { log?: ToolingLog } = {}
): Promise<void> => {
if (!spaceId || spaceId === DEFAULT_SPACE_ID) {
return;
}

const alreadyExists = await kbnClient.spaces
.get(spaceId)
.then(() => {
log.debug(`Space id [${spaceId}] already exists. Nothing to do.`);
return true;
})
.catch((err) => {
if (err instanceof AxiosError && (err.response?.status ?? err.status) === 404) {
return false;
}

throw err;
})
.catch(catchAxiosErrorFormatAndThrow);

if (!alreadyExists) {
log.info(`Creating space id [${spaceId}]`);

await kbnClient.spaces
.create({
name: spaceId,
id: spaceId,
})
.catch(catchAxiosErrorFormatAndThrow);
}
};

/**
* Get the current active space for the provided KbnClient
*
* NOTE: this utility may generate a `404` error if the `KbnClient` has been
* initialized for a specific space, but that space does not yet exist.
*
* @param kbnClient
*/
export const fetchActiveSpace = memoize(async (kbnClient: KbnClient): Promise<Space> => {
return kbnClient
.request<Space>({
method: 'GET',
path: `/internal/spaces/_active_space`,
})
.catch(catchAxiosErrorFormatAndThrow)
.then((response) => response.data);
});

/**
* Returns the space id that the provided KbnClient was initialized for by parsting its url
* @param kbnClient
*/
export const getSpaceIdFromKbnClientUrl = (
kbnClient: KbnClient
): ReturnType<typeof getSpaceIdFromPath> => {
const newUrl = new URL(kbnClient.resolveUrl('/'));

return getSpaceIdFromPath(newUrl.pathname); // NOTE: we are not currently supporting a Kibana base path prefix
};
Loading

0 comments on commit 25225c3

Please sign in to comment.