mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[EDR Workflows] Add Crowdstrike connector and Actions (#180175)
This commit is contained in:
parent
e47ac7f2d9
commit
b66be1f69b
35 changed files with 1345 additions and 48 deletions
|
@ -19,6 +19,7 @@ import {
|
|||
ActionTypeSecrets,
|
||||
ActionTypeParams,
|
||||
} from './types';
|
||||
import { isBidirectionalConnectorType } from './lib/bidirectional_connectors';
|
||||
|
||||
export interface ActionTypeRegistryOpts {
|
||||
licensing: LicensingPluginSetup;
|
||||
|
@ -230,8 +231,10 @@ export class ActionTypeRegistry {
|
|||
.filter(([_, actionType]) =>
|
||||
featureId ? actionType.supportedFeatureIds.includes(featureId) : true
|
||||
)
|
||||
// Temporarily don't return SentinelOne connector for Security Solution Rule Actions
|
||||
.filter(([actionTypeId]) => (featureId ? actionTypeId !== '.sentinelone' : true))
|
||||
// Temporarily don't return SentinelOne and Crowdstrike connector for Security Solution Rule Actions
|
||||
.filter(([actionTypeId]) =>
|
||||
featureId ? !isBidirectionalConnectorType(actionTypeId) : true
|
||||
)
|
||||
.map(([actionTypeId, actionType]) => ({
|
||||
id: actionTypeId,
|
||||
name: actionType.name,
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
CONNECTORS_ADVANCED_EXECUTE_PRIVILEGE_API_TAG,
|
||||
CONNECTORS_BASIC_EXECUTE_PRIVILEGE_API_TAG,
|
||||
} from '../feature';
|
||||
import { forEach } from 'lodash';
|
||||
|
||||
const request = {} as KibanaRequest;
|
||||
|
||||
|
@ -237,50 +238,54 @@ describe('ensureAuthorized', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('checks SentinelOne connector privileges correctly', async () => {
|
||||
const { authorization } = mockSecurity();
|
||||
const checkPrivileges: jest.MockedFunction<
|
||||
ReturnType<typeof authorization.checkPrivilegesDynamicallyWithRequest>
|
||||
> = jest.fn();
|
||||
describe('Bi-directional connectors', () => {
|
||||
forEach(['.sentinelone', '.crowdstrike'], (actionTypeId) => {
|
||||
test(`checks ${actionTypeId} connector privileges correctly`, async () => {
|
||||
const { authorization } = mockSecurity();
|
||||
const checkPrivileges: jest.MockedFunction<
|
||||
ReturnType<typeof authorization.checkPrivilegesDynamicallyWithRequest>
|
||||
> = jest.fn();
|
||||
|
||||
authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
|
||||
const actionsAuthorization = new ActionsAuthorization({
|
||||
request,
|
||||
authorization,
|
||||
});
|
||||
authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
|
||||
const actionsAuthorization = new ActionsAuthorization({
|
||||
request,
|
||||
authorization,
|
||||
});
|
||||
|
||||
checkPrivileges.mockResolvedValueOnce({
|
||||
username: 'some-user',
|
||||
hasAllRequested: true,
|
||||
privileges: [
|
||||
{
|
||||
privilege: mockAuthorizationAction('myType', 'execute'),
|
||||
authorized: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
checkPrivileges.mockResolvedValueOnce({
|
||||
username: 'some-user',
|
||||
hasAllRequested: true,
|
||||
privileges: [
|
||||
{
|
||||
privilege: mockAuthorizationAction('myType', 'execute'),
|
||||
authorized: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await actionsAuthorization.ensureAuthorized({
|
||||
operation: 'execute',
|
||||
actionTypeId: '.sentinelone',
|
||||
});
|
||||
await actionsAuthorization.ensureAuthorized({
|
||||
operation: 'execute',
|
||||
actionTypeId,
|
||||
});
|
||||
|
||||
expect(authorization.actions.savedObject.get).toHaveBeenCalledWith(
|
||||
ACTION_SAVED_OBJECT_TYPE,
|
||||
'get'
|
||||
);
|
||||
expect(authorization.actions.savedObject.get).toHaveBeenCalledWith(
|
||||
ACTION_SAVED_OBJECT_TYPE,
|
||||
'get'
|
||||
);
|
||||
|
||||
expect(authorization.actions.savedObject.get).toHaveBeenCalledWith(
|
||||
ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
|
||||
'create'
|
||||
);
|
||||
expect(authorization.actions.savedObject.get).toHaveBeenCalledWith(
|
||||
ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
|
||||
'create'
|
||||
);
|
||||
|
||||
expect(checkPrivileges).toHaveBeenCalledWith({
|
||||
kibana: [
|
||||
mockAuthorizationAction(ACTION_SAVED_OBJECT_TYPE, 'get'),
|
||||
mockAuthorizationAction(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'),
|
||||
ADVANCED_EXECUTE_AUTHZ,
|
||||
],
|
||||
expect(checkPrivileges).toHaveBeenCalledWith({
|
||||
kibana: [
|
||||
mockAuthorizationAction(ACTION_SAVED_OBJECT_TYPE, 'get'),
|
||||
mockAuthorizationAction(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'),
|
||||
ADVANCED_EXECUTE_AUTHZ,
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
ACTION_SAVED_OBJECT_TYPE,
|
||||
ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
|
||||
} from '../constants/saved_objects';
|
||||
import { isBidirectionalConnectorType } from '../lib/bidirectional_connectors';
|
||||
import { AuthorizationMode } from './get_authorization_mode_by_source';
|
||||
|
||||
export interface ConstructorOptions {
|
||||
|
@ -44,6 +45,7 @@ export class ActionsAuthorization {
|
|||
private readonly request: KibanaRequest;
|
||||
private readonly authorization?: SecurityPluginSetup['authz'];
|
||||
private readonly authorizationMode: AuthorizationMode;
|
||||
|
||||
constructor({
|
||||
request,
|
||||
authorization,
|
||||
|
@ -77,9 +79,9 @@ export class ActionsAuthorization {
|
|||
kibana: [
|
||||
...privileges,
|
||||
...additionalPrivileges,
|
||||
// SentinelOne sub-actions require that a user have `all` privilege to Actions and Connectors.
|
||||
// SentinelOne and Crowdstrike sub-actions require that a user have `all` privilege to Actions and Connectors.
|
||||
// This is a temporary solution until a more robust RBAC approach can be implemented for sub-actions
|
||||
actionTypeId === '.sentinelone'
|
||||
isBidirectionalConnectorType(actionTypeId)
|
||||
? 'api:actions:execute-advanced-connectors'
|
||||
: 'api:actions:execute-basic-connectors',
|
||||
],
|
||||
|
|
|
@ -44,6 +44,7 @@ import { RelatedSavedObjects } from './related_saved_objects';
|
|||
import { createActionEventLogRecordObject } from './create_action_event_log_record_object';
|
||||
import { ActionExecutionError, ActionExecutionErrorReason } from './errors/action_execution_error';
|
||||
import type { ActionsAuthorization } from '../authorization/actions_authorization';
|
||||
import { isBidirectionalConnectorType } from './bidirectional_connectors';
|
||||
|
||||
// 1,000,000 nanoseconds in 1 millisecond
|
||||
const Millis2Nanos = 1000 * 1000;
|
||||
|
@ -698,8 +699,8 @@ const ensureAuthorizedToExecute = async ({
|
|||
additionalPrivileges,
|
||||
actionTypeId,
|
||||
});
|
||||
} else if (actionTypeId === '.sentinelone') {
|
||||
// SentinelOne sub-actions require that a user have `all` privilege to Actions and Connectors.
|
||||
} else if (isBidirectionalConnectorType(actionTypeId)) {
|
||||
// SentinelOne and Crowdstrike sub-actions require that a user have `all` privilege to Actions and Connectors.
|
||||
// This is a temporary solution until a more robust RBAC approach can be implemented for sub-actions
|
||||
await authorization.ensureAuthorized({
|
||||
operation: 'execute',
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const BIDIRECTIONAL_CONNECTOR_TYPES = ['.sentinelone', '.crowdstrike'];
|
||||
export const isBidirectionalConnectorType = (type: string | undefined) => {
|
||||
if (!type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return BIDIRECTIONAL_CONNECTOR_TYPES.includes(type);
|
||||
};
|
|
@ -88,7 +88,7 @@ export abstract class SubActionConnector<Config, Secrets> {
|
|||
}
|
||||
|
||||
private getHeaders(headers?: AxiosRequestHeaders): Record<string, AxiosHeaderValue> {
|
||||
return { ...headers, 'Content-Type': 'application/json' };
|
||||
return { 'Content-Type': 'application/json', ...headers };
|
||||
}
|
||||
|
||||
private validateResponse(responseSchema: Type<unknown>, data: unknown) {
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 const CROWDSTRIKE_TITLE = 'CrowdStrike';
|
||||
export const CROWDSTRIKE_CONNECTOR_ID = '.crowdstrike';
|
||||
export const API_MAX_RESULTS = 1000;
|
||||
|
||||
export enum SUB_ACTION {
|
||||
GET_AGENT_DETAILS = 'getAgentDetails',
|
||||
HOST_ACTIONS = 'hostActions',
|
||||
}
|
238
x-pack/plugins/stack_connectors/common/crowdstrike/schema.ts
Normal file
238
x-pack/plugins/stack_connectors/common/crowdstrike/schema.ts
Normal file
|
@ -0,0 +1,238 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
import { SUB_ACTION } from './constants';
|
||||
|
||||
// Connector schema
|
||||
export const CrowdstrikeConfigSchema = schema.object({
|
||||
url: schema.string(),
|
||||
});
|
||||
export const CrowdstrikeSecretsSchema = schema.object({
|
||||
clientId: schema.string(),
|
||||
clientSecret: schema.string(),
|
||||
});
|
||||
|
||||
export const CrowdstrikeBaseApiResponseSchema = schema.object(
|
||||
{
|
||||
resources: schema.arrayOf(schema.any()),
|
||||
errors: schema.nullable(schema.arrayOf(schema.any())),
|
||||
meta: schema.object(
|
||||
{
|
||||
query_time: schema.maybe(schema.number()),
|
||||
powered_by: schema.maybe(schema.string()),
|
||||
trace_id: schema.maybe(schema.string()),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
);
|
||||
|
||||
export const CrowdstrikeGetAgentsResponseSchema = schema.object(
|
||||
{
|
||||
resources: schema.arrayOf(
|
||||
schema.arrayOf(
|
||||
schema.object(
|
||||
{
|
||||
device_id: schema.maybe(schema.string()),
|
||||
cid: schema.maybe(schema.string()),
|
||||
agent_load_flags: schema.maybe(schema.string()),
|
||||
agent_local_time: schema.maybe(schema.string()),
|
||||
agent_version: schema.maybe(schema.string()),
|
||||
bios_manufacturer: schema.maybe(schema.string()),
|
||||
bios_version: schema.maybe(schema.string()),
|
||||
config_id_base: schema.maybe(schema.string()),
|
||||
config_id_build: schema.maybe(schema.string()),
|
||||
config_id_platform: schema.maybe(schema.string()),
|
||||
cpu_signature: schema.maybe(schema.string()),
|
||||
cpu_vendor: schema.maybe(schema.string()),
|
||||
external_ip: schema.maybe(schema.string()),
|
||||
mac_address: schema.maybe(schema.string()),
|
||||
instance_id: schema.maybe(schema.string()),
|
||||
service_provider: schema.maybe(schema.string()),
|
||||
service_provider_account_id: schema.maybe(schema.string()),
|
||||
hostname: schema.maybe(schema.string()),
|
||||
first_seen: schema.maybe(schema.string()),
|
||||
last_login_timestamp: schema.maybe(schema.string()),
|
||||
last_login_user: schema.maybe(schema.string()),
|
||||
last_login_uid: schema.maybe(schema.string()),
|
||||
last_seen: schema.maybe(schema.string()),
|
||||
local_ip: schema.maybe(schema.string()),
|
||||
major_version: schema.maybe(schema.string()),
|
||||
minor_version: schema.maybe(schema.string()),
|
||||
os_version: schema.maybe(schema.string()),
|
||||
platform_id: schema.maybe(schema.string()),
|
||||
platform_name: schema.maybe(schema.string()),
|
||||
policies: schema.maybe(
|
||||
schema.arrayOf(
|
||||
schema.object(
|
||||
{
|
||||
policy_type: schema.maybe(schema.string()),
|
||||
policy_id: schema.maybe(schema.string()),
|
||||
applied: schema.maybe(schema.boolean()),
|
||||
settings_hash: schema.maybe(schema.string()),
|
||||
assigned_date: schema.maybe(schema.string()),
|
||||
applied_date: schema.maybe(schema.string()),
|
||||
rule_groups: schema.maybe(schema.any()),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
)
|
||||
)
|
||||
),
|
||||
reduced_functionality_mode: schema.maybe(schema.string()),
|
||||
device_policies: schema.maybe(
|
||||
schema.object(
|
||||
{
|
||||
prevention: schema.object(
|
||||
{
|
||||
policy_type: schema.maybe(schema.string()),
|
||||
policy_id: schema.maybe(schema.string()),
|
||||
applied: schema.maybe(schema.boolean()),
|
||||
settings_hash: schema.maybe(schema.string()),
|
||||
assigned_date: schema.maybe(schema.string()),
|
||||
applied_date: schema.maybe(schema.string()),
|
||||
rule_groups: schema.any(),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
),
|
||||
sensor_update: schema.object(
|
||||
{
|
||||
policy_type: schema.maybe(schema.string()),
|
||||
policy_id: schema.maybe(schema.string()),
|
||||
applied: schema.maybe(schema.boolean()),
|
||||
settings_hash: schema.maybe(schema.string()),
|
||||
assigned_date: schema.maybe(schema.string()),
|
||||
applied_date: schema.maybe(schema.string()),
|
||||
uninstall_protection: schema.maybe(schema.string()),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
),
|
||||
global_config: schema.object(
|
||||
{
|
||||
policy_type: schema.maybe(schema.string()),
|
||||
policy_id: schema.maybe(schema.string()),
|
||||
applied: schema.maybe(schema.boolean()),
|
||||
settings_hash: schema.maybe(schema.string()),
|
||||
assigned_date: schema.maybe(schema.string()),
|
||||
applied_date: schema.maybe(schema.string()),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
),
|
||||
remote_response: schema.object(
|
||||
{
|
||||
policy_type: schema.maybe(schema.string()),
|
||||
policy_id: schema.maybe(schema.string()),
|
||||
applied: schema.maybe(schema.boolean()),
|
||||
settings_hash: schema.maybe(schema.string()),
|
||||
assigned_date: schema.maybe(schema.string()),
|
||||
applied_date: schema.maybe(schema.string()),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
)
|
||||
),
|
||||
groups: schema.maybe(schema.arrayOf(schema.any())),
|
||||
group_hash: schema.maybe(schema.string()),
|
||||
product_type_desc: schema.maybe(schema.string()),
|
||||
provision_status: schema.maybe(schema.string()),
|
||||
serial_number: schema.maybe(schema.string()),
|
||||
status: schema.maybe(schema.string()),
|
||||
system_manufacturer: schema.maybe(schema.string()),
|
||||
system_product_name: schema.maybe(schema.string()),
|
||||
tags: schema.maybe(schema.arrayOf(schema.any())),
|
||||
modified_timestamp: schema.any(),
|
||||
meta: schema.maybe(
|
||||
schema.object(
|
||||
{
|
||||
version: schema.maybe(schema.string()),
|
||||
version_string: schema.maybe(schema.string()),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
)
|
||||
),
|
||||
zone_group: schema.maybe(schema.string()),
|
||||
kernel_version: schema.maybe(schema.string()),
|
||||
chassis_type: schema.maybe(schema.string()),
|
||||
chassis_type_desc: schema.maybe(schema.string()),
|
||||
connection_ip: schema.maybe(schema.string()),
|
||||
default_gateway_ip: schema.maybe(schema.string()),
|
||||
connection_mac_address: schema.maybe(schema.string()),
|
||||
linux_sensor_mode: schema.maybe(schema.string()),
|
||||
deployment_type: schema.maybe(schema.string()),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
)
|
||||
)
|
||||
),
|
||||
errors: schema.nullable(schema.arrayOf(schema.any())),
|
||||
meta: schema.object(
|
||||
{
|
||||
query_time: schema.maybe(schema.number()),
|
||||
powered_by: schema.maybe(schema.string()),
|
||||
trace_id: schema.maybe(schema.string()),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
);
|
||||
export const CrowdstrikeHostActionsResponseSchema = schema.object(
|
||||
{
|
||||
resources: schema.arrayOf(
|
||||
schema.object(
|
||||
{
|
||||
id: schema.maybe(schema.string()),
|
||||
path: schema.maybe(schema.string()),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
)
|
||||
),
|
||||
meta: schema.object(
|
||||
{
|
||||
query_time: schema.maybe(schema.number()),
|
||||
powered_by: schema.maybe(schema.string()),
|
||||
trace_id: schema.maybe(schema.string()),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
),
|
||||
errors: schema.nullable(schema.arrayOf(schema.any())),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
);
|
||||
|
||||
export const CrowdstrikeHostActionsParamsSchema = schema.object({
|
||||
command: schema.oneOf([schema.literal('contain'), schema.literal('lift_containment')]),
|
||||
actionParameters: schema.maybe(schema.object({}, { unknowns: 'allow' })),
|
||||
ids: schema.arrayOf(schema.string()),
|
||||
alertIds: schema.maybe(schema.arrayOf(schema.string())),
|
||||
});
|
||||
|
||||
export const CrowdstrikeGetAgentsParamsSchema = schema.object({
|
||||
ids: schema.arrayOf(schema.string()),
|
||||
});
|
||||
export const CrowdstrikeGetTokenResponseSchema = schema.object(
|
||||
{
|
||||
access_token: schema.string(),
|
||||
expires_in: schema.number(),
|
||||
token_type: schema.string(),
|
||||
id_token: schema.maybe(schema.string()),
|
||||
issued_token_type: schema.maybe(schema.string()),
|
||||
refresh_token: schema.maybe(schema.string()),
|
||||
scope: schema.maybe(schema.string()),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
);
|
||||
|
||||
export const CrowdstrikeHostActionsSchema = schema.object({
|
||||
subAction: schema.literal(SUB_ACTION.HOST_ACTIONS),
|
||||
subActionParams: CrowdstrikeHostActionsParamsSchema,
|
||||
});
|
||||
|
||||
export const CrowdstrikeActionParamsSchema = schema.oneOf([CrowdstrikeHostActionsSchema]);
|
31
x-pack/plugins/stack_connectors/common/crowdstrike/types.ts
Normal file
31
x-pack/plugins/stack_connectors/common/crowdstrike/types.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { TypeOf } from '@kbn/config-schema';
|
||||
import {
|
||||
CrowdstrikeBaseApiResponseSchema,
|
||||
CrowdstrikeConfigSchema,
|
||||
CrowdstrikeGetAgentsParamsSchema,
|
||||
CrowdstrikeGetAgentsResponseSchema,
|
||||
CrowdstrikeHostActionsParamsSchema,
|
||||
CrowdstrikeSecretsSchema,
|
||||
CrowdstrikeActionParamsSchema,
|
||||
CrowdstrikeGetTokenResponseSchema,
|
||||
} from './schema';
|
||||
|
||||
export type CrowdstrikeConfig = TypeOf<typeof CrowdstrikeConfigSchema>;
|
||||
export type CrowdstrikeSecrets = TypeOf<typeof CrowdstrikeSecretsSchema>;
|
||||
|
||||
export type CrowdstrikeBaseApiResponse = TypeOf<typeof CrowdstrikeBaseApiResponseSchema>;
|
||||
|
||||
export type CrowdstrikeGetAgentsParams = Partial<TypeOf<typeof CrowdstrikeGetAgentsParamsSchema>>;
|
||||
export type CrowdstrikeGetAgentsResponse = TypeOf<typeof CrowdstrikeGetAgentsResponseSchema>;
|
||||
export type CrowdstrikeGetTokenResponse = TypeOf<typeof CrowdstrikeGetTokenResponseSchema>;
|
||||
|
||||
export type CrowdstrikeHostActionsParams = TypeOf<typeof CrowdstrikeHostActionsParamsSchema>;
|
||||
|
||||
export type CrowdstrikeActionParams = TypeOf<typeof CrowdstrikeActionParamsSchema>;
|
|
@ -14,6 +14,7 @@ export type ExperimentalFeatures = typeof allowedExperimentalValues;
|
|||
export const allowedExperimentalValues = Object.freeze({
|
||||
isMustacheAutocompleteOn: false,
|
||||
sentinelOneConnectorOn: true,
|
||||
crowdstrikeConnectorOn: false,
|
||||
});
|
||||
|
||||
export type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
|
||||
|
|
|
@ -16,5 +16,13 @@ import SentinelOneLogo from '../connector_types/sentinelone/logo';
|
|||
export { SENTINELONE_CONNECTOR_ID, SUB_ACTION } from '../../common/sentinelone/constants';
|
||||
export { SentinelOneLogo };
|
||||
|
||||
import CrowdstrikeLogo from '../connector_types/crowdstrike/logo';
|
||||
|
||||
export {
|
||||
CROWDSTRIKE_CONNECTOR_ID,
|
||||
SUB_ACTION as CROWDSTRIKE_SUB_ACTION,
|
||||
} from '../../common/crowdstrike/constants';
|
||||
export { CrowdstrikeLogo };
|
||||
|
||||
export { BEDROCK_CONNECTOR_ID } from '../../common/bedrock/constants';
|
||||
export { BedrockLogo };
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 { lazy } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type {
|
||||
ActionTypeModel as ConnectorTypeModel,
|
||||
GenericValidationResult,
|
||||
} from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import {
|
||||
CROWDSTRIKE_CONNECTOR_ID,
|
||||
CROWDSTRIKE_TITLE,
|
||||
SUB_ACTION,
|
||||
} from '../../../common/crowdstrike/constants';
|
||||
import type {
|
||||
CrowdstrikeConfig,
|
||||
CrowdstrikeSecrets,
|
||||
CrowdstrikeActionParams,
|
||||
} from '../../../common/crowdstrike/types';
|
||||
|
||||
interface ValidationErrors {
|
||||
subAction: string[];
|
||||
}
|
||||
|
||||
export function getConnectorType(): ConnectorTypeModel<
|
||||
CrowdstrikeConfig,
|
||||
CrowdstrikeSecrets,
|
||||
CrowdstrikeActionParams
|
||||
> {
|
||||
return {
|
||||
id: CROWDSTRIKE_CONNECTOR_ID,
|
||||
actionTypeTitle: CROWDSTRIKE_TITLE,
|
||||
iconClass: lazy(() => import('./logo')),
|
||||
isExperimental: true,
|
||||
selectMessage: i18n.translate(
|
||||
'xpack.stackConnectors.security.crowdstrike.config.selectMessageText',
|
||||
{
|
||||
defaultMessage: 'Execute CrowdStrike response actions',
|
||||
}
|
||||
),
|
||||
validateParams: async (
|
||||
actionParams: CrowdstrikeActionParams
|
||||
): Promise<GenericValidationResult<ValidationErrors>> => {
|
||||
const translations = await import('./translations');
|
||||
const errors: ValidationErrors = {
|
||||
subAction: [],
|
||||
};
|
||||
const { subAction } = actionParams;
|
||||
|
||||
// The internal "subAction" param should always be valid, ensure it is only if "subActionParams" are valid
|
||||
if (!subAction) {
|
||||
errors.subAction.push(translations.ACTION_REQUIRED);
|
||||
} else if (!Object.values(SUB_ACTION).includes(subAction)) {
|
||||
errors.subAction.push(translations.INVALID_ACTION);
|
||||
}
|
||||
return { errors };
|
||||
},
|
||||
actionConnectorFields: lazy(() => import('./crowdstrike_connector')),
|
||||
actionParamsFields: lazy(() => import('./crowdstrike_params_empty')),
|
||||
// TODO: Enable once we add support for automated response actions
|
||||
// actionParamsFields: lazy(() => import('./crowdstrike_params')),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
|
||||
import {
|
||||
ActionConnectorFieldsProps,
|
||||
ConfigFieldSchema,
|
||||
SecretsFieldSchema,
|
||||
SimpleConnectorForm,
|
||||
} from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const CROWDSTRIKE_DEFAULT_API_URL = 'https://api.crowdstrike.com';
|
||||
const configFormSchema: ConfigFieldSchema[] = [
|
||||
{
|
||||
id: 'url',
|
||||
label: i18n.URL_LABEL,
|
||||
isUrlField: true,
|
||||
defaultValue: CROWDSTRIKE_DEFAULT_API_URL,
|
||||
},
|
||||
];
|
||||
|
||||
const secretsFormSchema: SecretsFieldSchema[] = [
|
||||
{
|
||||
id: 'clientId',
|
||||
label: i18n.CLIENT_ID_LABEL,
|
||||
isPasswordField: false,
|
||||
isRequired: true,
|
||||
},
|
||||
{
|
||||
id: 'clientSecret',
|
||||
label: i18n.CLIENT_SECRET_LABEL,
|
||||
isPasswordField: true,
|
||||
isRequired: true,
|
||||
},
|
||||
];
|
||||
|
||||
const CrowdstrikeActionConnectorFields: React.FunctionComponent<ActionConnectorFieldsProps> = ({
|
||||
readOnly,
|
||||
isEdit,
|
||||
}) => (
|
||||
<SimpleConnectorForm
|
||||
isEdit={isEdit}
|
||||
readOnly={readOnly}
|
||||
configFormSchema={configFormSchema}
|
||||
secretsFormSchema={secretsFormSchema}
|
||||
/>
|
||||
);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { CrowdstrikeActionConnectorFields as default };
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
|
||||
const CrowdstrikeParamsFields = () => <></>;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { CrowdstrikeParamsFields as default };
|
|
@ -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 { getConnectorType as getCrowdStrikeConnectorType } from './crowdstrike';
|
|
@ -0,0 +1,41 @@
|
|||
<svg version="1.1" id="Layer_1" xmlns:x="ns_extend;" xmlns:i="ns_ai;" xmlns:graph="ns_graphs;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 116.4 88.8" style="enable-background:new 0 0 116.4 88.8;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#FC0000;}
|
||||
</style>
|
||||
<metadata>
|
||||
<sfw xmlns="ns_sfw;">
|
||||
<slices>
|
||||
</slices>
|
||||
<sliceSourceBounds bottomLeftOrigin="true" height="88.7" width="116.4" x="0" y="0.1">
|
||||
</sliceSourceBounds>
|
||||
</sfw>
|
||||
</metadata>
|
||||
<desc>
|
||||
Created with Sketch.
|
||||
</desc>
|
||||
<g id="Page-1">
|
||||
<g id="Home-v4.1" transform="translate(-26.000000, -22.000000)">
|
||||
<g id="CS-Logo" transform="translate(26.000000, 22.000000)">
|
||||
<path id="Fill-110" class="st0" d="M113.3,71.7c-2.6-0.2-7.1-0.8-12.9,1.8c-5.7,2.7-8,2.8-10.8,2.5c0.8,1.4,2.5,3.3,7.7,3.7
|
||||
c5.2,0.3,7.8,0.5,5,6.6c0.1-1.8-0.4-5.4-5.6-4.8s-6.4,5-0.8,7.1c-1.8,0.3-5.7,0.5-8.4-6.1c-1.9,0.8-4.8,2.3-10.2-1.5
|
||||
c1.9,0.6,4.2,0.7,4.2,0.7c-4.7-2.1-9.3-6-12.1-9.7c2.3,1.6,4.8,3.2,7.4,3.5c-3-3.4-10-10.3-18.6-17.3c5.5,3.3,12.2,8.6,23,7.4
|
||||
C92.1,64.4,99.4,62.1,113.3,71.7">
|
||||
</path>
|
||||
<path id="Fill-112" class="st0" d="M67.4,70.4c-7.3-2.7-8.8-3.3-18.2-5.3c-9.3-2.1-18.5-6.3-24.7-13c4.3,2.8,13.2,8.3,22.3,7.7
|
||||
c-1.4-1.8-3.9-3.1-7-4.5C43.2,56,53.6,58.3,67.4,70.4">
|
||||
</path>
|
||||
<path id="Fill-118" class="st0" d="M104.1,64.3c6.4,0.6,6.1,1.5,6.1,3.1C107.5,65.4,104.1,64.3,104.1,64.3 M65.5,31.2
|
||||
C37.9,23.3,26.9,13.4,18.4,3.1c3.9,11.9,13.1,16.2,23,24.2s10.5,12.3,13.4,17.1c6.5,10.6,7.5,12.3,14,16.9
|
||||
c7.6,5,16.8,1.6,26.9,3.2s18.4,9.2,20.2,12.1c2.1-3.7-2.9-9.1-4.3-10.5c0.7-4.9-10.9-7.1-15.4-8.7c-0.9-0.3-3-0.8-1.2-5.2
|
||||
C97.5,46.1,100.2,40.8,65.5,31.2">
|
||||
</path>
|
||||
<path id="Fill-114" class="st0" d="M52.1,45.9c-1.8-4.9-4.9-11.1-19.9-20.4C24.8,20.8,14.1,15,0,0c1,4,5.5,14.5,27.9,28.2
|
||||
C35.3,33.1,44.9,36.1,52.1,45.9">
|
||||
</path>
|
||||
<path id="Fill-116" class="st0" d="M52.1,55.1c-1.7-4.1-5.3-9.4-19-16.9c-6.4-3.6-17.2-9.2-27-19.8C7,22.2,11.5,30.6,31,41.1
|
||||
C36.4,44.2,45.5,47,52.1,55.1">
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import CrowdstrikeLogo from './logo.svg';
|
||||
const Logo = () => {
|
||||
return <img width="32" height="32" src={CrowdstrikeLogo} alt="CrowdStrike" />;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { Logo as default };
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
// config form
|
||||
|
||||
export const URL_LABEL = i18n.translate(
|
||||
'xpack.stackConnectors.security.crowdstrike.config.urlTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Crowdstrike API URL',
|
||||
}
|
||||
);
|
||||
|
||||
export const CLIENT_ID_LABEL = i18n.translate(
|
||||
'xpack.stackConnectors.security.crowdstrike.config.clientIdTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Crowdstrike Client ID',
|
||||
}
|
||||
);
|
||||
|
||||
export const CLIENT_SECRET_LABEL = i18n.translate(
|
||||
'xpack.stackConnectors.security.crowdstrike.config.clientSecretTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Client Secret',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTION_REQUIRED = i18n.translate(
|
||||
'xpack.stackConnectors.security.crowdstrike.params.error.requiredActionText',
|
||||
{
|
||||
defaultMessage: 'Action is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const INVALID_ACTION = i18n.translate(
|
||||
'xpack.stackConnectors.security.crowdstrike.params.error.invalidActionText',
|
||||
{
|
||||
defaultMessage: 'Invalid action name.',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 { CrowdstrikeHostActionsParams } from '../../../common/crowdstrike/types';
|
||||
import type { SUB_ACTION } from '../../../common/crowdstrike/constants';
|
||||
|
||||
export type CrowdstrikeExecuteSubActionParams = CrowdstrikeHostActionsParams;
|
||||
|
||||
export interface CrowdstrikeExecuteActionParams {
|
||||
subAction: SUB_ACTION;
|
||||
subActionParams: CrowdstrikeExecuteSubActionParams;
|
||||
}
|
|
@ -31,6 +31,7 @@ import { getXmattersConnectorType } from './xmatters';
|
|||
import { getD3SecurityConnectorType } from './d3security';
|
||||
import { ExperimentalFeaturesService } from '../common/experimental_features_service';
|
||||
import { getSentinelOneConnectorType } from './sentinelone';
|
||||
import { getCrowdStrikeConnectorType } from './crowdstrike';
|
||||
|
||||
export interface RegistrationServices {
|
||||
validateEmailAddresses: (
|
||||
|
@ -72,4 +73,7 @@ export function registerConnectorTypes({
|
|||
if (ExperimentalFeaturesService.get().sentinelOneConnectorOn) {
|
||||
connectorTypeRegistry.register(getSentinelOneConnectorType());
|
||||
}
|
||||
if (ExperimentalFeaturesService.get().crowdstrikeConnectorOn) {
|
||||
connectorTypeRegistry.register(getCrowdStrikeConnectorType());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,211 @@
|
|||
/*
|
||||
* 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 { CrowdstrikeConnector } from './crowdstrike';
|
||||
import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock';
|
||||
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
|
||||
import { actionsMock } from '@kbn/actions-plugin/server/mocks';
|
||||
import { CROWDSTRIKE_CONNECTOR_ID } from '../../../public/common';
|
||||
|
||||
const tokenPath = 'https://api.crowdstrike.com/oauth2/token';
|
||||
const hostPath = 'https://api.crowdstrike.com/devices/entities/devices/v2';
|
||||
const actionsPath = 'https://api.crowdstrike.com/devices/entities/devices-actions/v2';
|
||||
describe('CrowdstrikeConnector', () => {
|
||||
const connector = new CrowdstrikeConnector({
|
||||
configurationUtilities: actionsConfigMock.create(),
|
||||
connector: { id: '1', type: CROWDSTRIKE_CONNECTOR_ID },
|
||||
config: { url: 'https://api.crowdstrike.com' },
|
||||
secrets: { clientId: '123', clientSecret: 'secret' },
|
||||
logger: loggingSystemMock.createLogger(),
|
||||
services: actionsMock.createServices(),
|
||||
});
|
||||
let mockedRequest: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-expect-error private static - but I still want to reset it
|
||||
CrowdstrikeConnector.token = null;
|
||||
// @ts-expect-error
|
||||
mockedRequest = connector.request = jest.fn() as jest.Mock;
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('executeHostActions', () => {
|
||||
it('should make a POST request to the correct URL with correct data', async () => {
|
||||
const mockResponse = { data: { id: 'testid', path: 'testpath' } };
|
||||
//
|
||||
mockedRequest.mockResolvedValueOnce({ data: { access_token: 'testToken' } });
|
||||
mockedRequest.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await connector.executeHostActions({
|
||||
command: 'contain',
|
||||
ids: ['id1', 'id2'],
|
||||
});
|
||||
expect(mockedRequest).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
authorization: expect.any(String),
|
||||
},
|
||||
method: 'post',
|
||||
responseSchema: expect.any(Object),
|
||||
url: tokenPath,
|
||||
})
|
||||
);
|
||||
expect(mockedRequest).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
url: actionsPath,
|
||||
method: 'post',
|
||||
params: { action_name: 'contain' },
|
||||
data: { ids: ['id1', 'id2'] },
|
||||
paramsSerializer: expect.any(Function),
|
||||
responseSchema: expect.any(Object),
|
||||
})
|
||||
);
|
||||
expect(result).toEqual({ id: 'testid', path: 'testpath' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAgentDetails', () => {
|
||||
it('should make a GET request to the correct URL with correct params', async () => {
|
||||
const mockResponse = { data: { resources: [{}] } };
|
||||
|
||||
mockedRequest.mockResolvedValueOnce({ data: { access_token: 'testToken' } });
|
||||
mockedRequest.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await connector.getAgentDetails({ ids: ['id1', 'id2'] });
|
||||
|
||||
expect(mockedRequest).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
authorization: expect.any(String),
|
||||
},
|
||||
method: 'post',
|
||||
responseSchema: expect.any(Object),
|
||||
url: tokenPath,
|
||||
})
|
||||
);
|
||||
expect(mockedRequest).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer testToken',
|
||||
}),
|
||||
method: 'GET',
|
||||
params: { ids: ['id1', 'id2'] },
|
||||
paramsSerializer: expect.any(Function),
|
||||
responseSchema: expect.any(Object),
|
||||
url: hostPath,
|
||||
})
|
||||
);
|
||||
expect(result).toEqual({ resources: [{}] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTokenRequest', () => {
|
||||
it('should make a POST request to the correct URL with correct headers', async () => {
|
||||
const mockResponse = { data: { access_token: 'testToken' } };
|
||||
mockedRequest.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
// @ts-expect-error private method - but I still want to
|
||||
const result = await connector.getTokenRequest();
|
||||
|
||||
expect(mockedRequest).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: tokenPath,
|
||||
method: 'post',
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
authorization: expect.stringContaining('Basic'),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(result).toEqual('testToken');
|
||||
});
|
||||
it('should not call getTokenRequest if the token already exists', async () => {
|
||||
const mockResponse = { data: { resources: [{}] } };
|
||||
|
||||
mockedRequest.mockResolvedValueOnce({ data: { access_token: 'testToken' } });
|
||||
mockedRequest.mockResolvedValue(mockResponse);
|
||||
|
||||
await connector.getAgentDetails({ ids: ['id1', 'id2'] });
|
||||
|
||||
expect(mockedRequest).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
authorization: expect.any(String),
|
||||
},
|
||||
method: 'post',
|
||||
responseSchema: expect.any(Object),
|
||||
url: tokenPath,
|
||||
})
|
||||
);
|
||||
expect(mockedRequest).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer testToken',
|
||||
}),
|
||||
method: 'GET',
|
||||
params: { ids: ['id1', 'id2'] },
|
||||
paramsSerializer: expect.any(Function),
|
||||
responseSchema: expect.any(Object),
|
||||
url: hostPath,
|
||||
})
|
||||
);
|
||||
expect(mockedRequest).toHaveBeenCalledTimes(2);
|
||||
await connector.getAgentDetails({ ids: ['id1', 'id2'] });
|
||||
expect(mockedRequest).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer testToken',
|
||||
}),
|
||||
method: 'GET',
|
||||
params: { ids: ['id1', 'id2'] },
|
||||
paramsSerializer: expect.any(Function),
|
||||
responseSchema: expect.any(Object),
|
||||
url: hostPath,
|
||||
})
|
||||
);
|
||||
expect(mockedRequest).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
it('should throw error when something goes wrong', async () => {
|
||||
const mockResponse = { code: 400, message: 'something goes wrong' };
|
||||
|
||||
mockedRequest.mockResolvedValueOnce({ data: { access_token: 'testToken' } });
|
||||
mockedRequest.mockRejectedValueOnce(mockResponse);
|
||||
|
||||
// expect(mockedRequest).toThrowError('access denied, invalid bearer token');
|
||||
await expect(() => connector.getAgentDetails({ ids: ['id1', 'id2'] })).rejects.toThrowError(
|
||||
'something goes wrong'
|
||||
);
|
||||
expect(mockedRequest).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
it('should repeat the call one time if theres 401 error ', async () => {
|
||||
const mockResponse = { code: 401, message: 'access denied, invalid bearer token' };
|
||||
|
||||
mockedRequest.mockResolvedValueOnce({ data: { access_token: 'testToken' } });
|
||||
mockedRequest.mockRejectedValueOnce(mockResponse);
|
||||
|
||||
// expect(mockedRequest).toThrowError('access denied, invalid bearer token');
|
||||
await expect(() => connector.getAgentDetails({ ids: ['id1', 'id2'] })).rejects.toThrowError();
|
||||
expect(mockedRequest).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
* 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 { ServiceParams, SubActionConnector } from '@kbn/actions-plugin/server';
|
||||
import type { AxiosError } from 'axios';
|
||||
import { SubActionRequestParams } from '@kbn/actions-plugin/server/sub_action_framework/types';
|
||||
import type {
|
||||
CrowdstrikeConfig,
|
||||
CrowdstrikeSecrets,
|
||||
CrowdstrikeGetAgentsResponse,
|
||||
CrowdstrikeGetAgentsParams,
|
||||
CrowdstrikeBaseApiResponse,
|
||||
CrowdstrikeHostActionsParams,
|
||||
CrowdstrikeGetTokenResponse,
|
||||
} from '../../../common/crowdstrike/types';
|
||||
import {
|
||||
CrowdstrikeGetAgentsResponseSchema,
|
||||
CrowdstrikeHostActionsParamsSchema,
|
||||
CrowdstrikeGetAgentsParamsSchema,
|
||||
CrowdstrikeGetTokenResponseSchema,
|
||||
CrowdstrikeHostActionsResponseSchema,
|
||||
} from '../../../common/crowdstrike/schema';
|
||||
import { SUB_ACTION } from '../../../common/crowdstrike/constants';
|
||||
import { CrowdstrikeError } from './error';
|
||||
|
||||
const paramsSerializer = (params: Record<string, string>) => {
|
||||
return Object.entries(params)
|
||||
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
||||
.join('&');
|
||||
};
|
||||
|
||||
/**
|
||||
* Crowdstrike Connector
|
||||
* @constructor
|
||||
* @param {string} token - Authorization token received from OAuth2 API, that needs to be sent along with each request.
|
||||
* @param {number} tokenExpiryTimeout - Tokens are valid for 30 minutes, so we will refresh them every 29 minutes
|
||||
* @param {base64} base64encodedToken - The base64 encoded token used for authentication.
|
||||
*/
|
||||
|
||||
export class CrowdstrikeConnector extends SubActionConnector<
|
||||
CrowdstrikeConfig,
|
||||
CrowdstrikeSecrets
|
||||
> {
|
||||
private static token: string | null;
|
||||
private static tokenExpiryTimeout: NodeJS.Timeout;
|
||||
private static base64encodedToken: string;
|
||||
private urls: {
|
||||
getToken: string;
|
||||
agents: string;
|
||||
hostAction: string;
|
||||
};
|
||||
|
||||
constructor(params: ServiceParams<CrowdstrikeConfig, CrowdstrikeSecrets>) {
|
||||
super(params);
|
||||
this.urls = {
|
||||
getToken: `${this.config.url}/oauth2/token`,
|
||||
hostAction: `${this.config.url}/devices/entities/devices-actions/v2`,
|
||||
agents: `${this.config.url}/devices/entities/devices/v2`,
|
||||
};
|
||||
|
||||
if (!CrowdstrikeConnector.base64encodedToken) {
|
||||
CrowdstrikeConnector.base64encodedToken = Buffer.from(
|
||||
this.secrets.clientId + ':' + this.secrets.clientSecret
|
||||
).toString('base64');
|
||||
}
|
||||
|
||||
this.registerSubActions();
|
||||
}
|
||||
|
||||
private registerSubActions() {
|
||||
this.registerSubAction({
|
||||
name: SUB_ACTION.GET_AGENT_DETAILS,
|
||||
method: 'getAgentDetails',
|
||||
schema: CrowdstrikeGetAgentsParamsSchema,
|
||||
});
|
||||
|
||||
this.registerSubAction({
|
||||
name: SUB_ACTION.HOST_ACTIONS,
|
||||
method: 'executeHostActions',
|
||||
schema: CrowdstrikeHostActionsParamsSchema,
|
||||
});
|
||||
}
|
||||
|
||||
public async executeHostActions({ alertIds, ...payload }: CrowdstrikeHostActionsParams) {
|
||||
return this.crowdstrikeApiRequest({
|
||||
url: this.urls.hostAction,
|
||||
method: 'post',
|
||||
params: {
|
||||
action_name: payload.command,
|
||||
},
|
||||
data: {
|
||||
ids: payload.ids,
|
||||
...(payload.actionParameters
|
||||
? {
|
||||
action_parameters: Object.entries(payload.actionParameters).map(([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
})),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
paramsSerializer,
|
||||
responseSchema: CrowdstrikeHostActionsResponseSchema,
|
||||
});
|
||||
}
|
||||
|
||||
public async getAgentDetails(
|
||||
payload: CrowdstrikeGetAgentsParams
|
||||
): Promise<CrowdstrikeGetAgentsResponse> {
|
||||
return this.crowdstrikeApiRequest({
|
||||
url: this.urls.agents,
|
||||
method: 'GET',
|
||||
params: {
|
||||
ids: payload.ids,
|
||||
},
|
||||
paramsSerializer,
|
||||
responseSchema: CrowdstrikeGetAgentsResponseSchema,
|
||||
});
|
||||
}
|
||||
|
||||
private async getTokenRequest() {
|
||||
const response = await this.request<CrowdstrikeGetTokenResponse>({
|
||||
url: this.urls.getToken,
|
||||
method: 'post',
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
authorization: 'Basic ' + this.base64encodedToken,
|
||||
},
|
||||
responseSchema: CrowdstrikeGetTokenResponseSchema,
|
||||
});
|
||||
const token = response.data?.access_token;
|
||||
if (token) {
|
||||
// Clear any existing timeout
|
||||
clearTimeout(CrowdstrikeConnector.tokenExpiryTimeout);
|
||||
|
||||
// Set a timeout to reset the token after 29 minutes (it expires after 30 minutes)
|
||||
CrowdstrikeConnector.tokenExpiryTimeout = setTimeout(() => {
|
||||
CrowdstrikeConnector.token = null;
|
||||
}, 29 * 60 * 1000);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
private async crowdstrikeApiRequest<R extends CrowdstrikeBaseApiResponse>(
|
||||
req: SubActionRequestParams<R>,
|
||||
retried?: boolean
|
||||
): Promise<R> {
|
||||
try {
|
||||
if (!CrowdstrikeConnector.token) {
|
||||
CrowdstrikeConnector.token = (await this.getTokenRequest()) as string;
|
||||
}
|
||||
|
||||
const response = await this.request<R>({
|
||||
...req,
|
||||
headers: {
|
||||
...req.headers,
|
||||
Authorization: `Bearer ${CrowdstrikeConnector.token}`,
|
||||
},
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (error.code === 401 && !retried) {
|
||||
CrowdstrikeConnector.token = null;
|
||||
return this.crowdstrikeApiRequest(req, true);
|
||||
}
|
||||
|
||||
throw new CrowdstrikeError(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
protected getResponseErrorMessage(
|
||||
error: AxiosError<{ errors: [{ message: string; code: number }] }>
|
||||
): string {
|
||||
const errorData = error.response?.data?.errors?.[0];
|
||||
if (errorData) {
|
||||
return errorData.message;
|
||||
}
|
||||
|
||||
if (!error.response?.status) {
|
||||
return `Unknown API Error: ${JSON.stringify(error.response?.data ?? {})}`;
|
||||
}
|
||||
|
||||
return `API Error: ${JSON.stringify(error.response.data ?? {})}`;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const parseStatusMessage = (message: string): { code: number; message: string } => {
|
||||
const regex = /Status code: (\d+). Message: (.+)/;
|
||||
const match = message.match(regex);
|
||||
if (match) {
|
||||
return {
|
||||
code: parseInt(match[1], 10),
|
||||
message: match[2],
|
||||
};
|
||||
}
|
||||
return {
|
||||
code: 500,
|
||||
message,
|
||||
};
|
||||
};
|
||||
|
||||
export class CrowdstrikeError extends Error {
|
||||
public code: number;
|
||||
public message: string;
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
const parsedMessage = parseStatusMessage(message);
|
||||
this.code = parsedMessage?.code;
|
||||
this.message = parsedMessage?.message || 'Unknown Crowdstrike error';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 {
|
||||
SubActionConnectorType,
|
||||
ValidatorType,
|
||||
} from '@kbn/actions-plugin/server/sub_action_framework/types';
|
||||
import { SecurityConnectorFeatureId } from '@kbn/actions-plugin/common';
|
||||
import { urlAllowListValidator } from '@kbn/actions-plugin/server';
|
||||
import { CROWDSTRIKE_CONNECTOR_ID, CROWDSTRIKE_TITLE } from '../../../common/crowdstrike/constants';
|
||||
import {
|
||||
CrowdstrikeConfigSchema,
|
||||
CrowdstrikeSecretsSchema,
|
||||
} from '../../../common/crowdstrike/schema';
|
||||
import { CrowdstrikeConfig, CrowdstrikeSecrets } from '../../../common/crowdstrike/types';
|
||||
import { CrowdstrikeConnector } from './crowdstrike';
|
||||
|
||||
export const getCrowdstrikeConnectorType = (): SubActionConnectorType<
|
||||
CrowdstrikeConfig,
|
||||
CrowdstrikeSecrets
|
||||
> => ({
|
||||
id: CROWDSTRIKE_CONNECTOR_ID,
|
||||
name: CROWDSTRIKE_TITLE,
|
||||
getService: (params) => new CrowdstrikeConnector(params),
|
||||
schema: {
|
||||
config: CrowdstrikeConfigSchema,
|
||||
secrets: CrowdstrikeSecretsSchema,
|
||||
},
|
||||
validators: [{ type: ValidatorType.CONFIG, validator: urlAllowListValidator('url') }],
|
||||
supportedFeatureIds: [SecurityConnectorFeatureId],
|
||||
minimumLicenseRequired: 'enterprise' as const,
|
||||
});
|
|
@ -32,6 +32,7 @@ import { getOpsgenieConnectorType } from './opsgenie';
|
|||
import type { ActionParamsType as ServiceNowITSMActionParams } from './servicenow_itsm';
|
||||
import type { ActionParamsType as ServiceNowSIRActionParams } from './servicenow_sir';
|
||||
import { getSentinelOneConnectorType } from './sentinelone';
|
||||
import { getCrowdstrikeConnectorType } from './crowdstrike';
|
||||
import { ExperimentalFeatures } from '../../common/experimental_features';
|
||||
|
||||
export { ConnectorTypeId as CasesWebhookConnectorTypeId } from './cases_webhook';
|
||||
|
@ -112,4 +113,7 @@ export function registerConnectorTypes({
|
|||
if (experimentalFeatures.sentinelOneConnectorOn) {
|
||||
actions.registerSubActionConnectorType(getSentinelOneConnectorType());
|
||||
}
|
||||
if (experimentalFeatures.crowdstrikeConnectorOn) {
|
||||
actions.registerSubActionConnectorType(getCrowdstrikeConnectorType());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { FtrConfigProviderContext, findTestPluginPaths } from '@kbn/test';
|
|||
import { getAllExternalServiceSimulatorPaths } from '@kbn/actions-simulators-plugin/server/plugin';
|
||||
import { ExperimentalConfigKeys } from '@kbn/stack-connectors-plugin/common/experimental_features';
|
||||
import { SENTINELONE_CONNECTOR_ID } from '@kbn/stack-connectors-plugin/common/sentinelone/constants';
|
||||
import { CROWDSTRIKE_CONNECTOR_ID } from '@kbn/stack-connectors-plugin/common/crowdstrike/constants';
|
||||
import { services } from './services';
|
||||
import { getTlsWebhookServerUrls } from './lib/get_tls_webhook_servers';
|
||||
|
||||
|
@ -52,6 +53,7 @@ const enabledActionTypes = [
|
|||
'.gen-ai',
|
||||
'.d3security',
|
||||
SENTINELONE_CONNECTOR_ID,
|
||||
CROWDSTRIKE_CONNECTOR_ID,
|
||||
'.slack',
|
||||
'.slack_api',
|
||||
'.tines',
|
||||
|
|
|
@ -16,5 +16,5 @@ export default createTestConfig('security_and_spaces', {
|
|||
publicBaseUrl: true,
|
||||
testFiles: [require.resolve('./tests')],
|
||||
useDedicatedTaskRunner: true,
|
||||
experimentalFeatures: ['sentinelOneConnectorOn'],
|
||||
experimentalFeatures: ['sentinelOneConnectorOn', 'crowdstrikeConnectorOn'],
|
||||
});
|
||||
|
|
|
@ -16,5 +16,5 @@ export default createTestConfig('security_and_spaces', {
|
|||
publicBaseUrl: true,
|
||||
testFiles: [require.resolve('./tests')],
|
||||
useDedicatedTaskRunner: false,
|
||||
experimentalFeatures: ['sentinelOneConnectorOn'],
|
||||
experimentalFeatures: ['sentinelOneConnectorOn', 'crowdstrikeConnectorOn'],
|
||||
});
|
||||
|
|
|
@ -16,5 +16,5 @@ export default createTestConfig('security_and_spaces', {
|
|||
publicBaseUrl: true,
|
||||
testFiles: [require.resolve('.')],
|
||||
useDedicatedTaskRunner: true,
|
||||
experimentalFeatures: ['sentinelOneConnectorOn'],
|
||||
experimentalFeatures: ['sentinelOneConnectorOn', 'crowdstrikeConnectorOn'],
|
||||
});
|
||||
|
|
|
@ -0,0 +1,233 @@
|
|||
/*
|
||||
* 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 {
|
||||
CROWDSTRIKE_CONNECTOR_ID,
|
||||
SUB_ACTION,
|
||||
} from '@kbn/stack-connectors-plugin/common/crowdstrike/constants';
|
||||
import { FeaturesPrivileges, Role } from '@kbn/security-plugin/common';
|
||||
import SuperTest from 'supertest';
|
||||
import expect from '@kbn/expect';
|
||||
import { getUrlPrefix } from '../../../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
||||
import { createSupertestErrorLogger } from '../../../../../common/lib/log_supertest_errors';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function createCrowdstrikeTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const securityService = getService('security');
|
||||
const log = getService('log');
|
||||
const logErrorDetails = createSupertestErrorLogger(log);
|
||||
|
||||
describe('Crowdstrike', () => {
|
||||
describe('sub-actions authz', () => {
|
||||
interface CreatedUser {
|
||||
username: string;
|
||||
password: string;
|
||||
deleteUser: () => Promise<void>;
|
||||
}
|
||||
|
||||
// Crowdstrike supported sub-actions
|
||||
const crowdstrikeSubActions = [SUB_ACTION.HOST_ACTIONS, SUB_ACTION.GET_AGENT_DETAILS];
|
||||
|
||||
let connectorId: string;
|
||||
|
||||
const createUser = async ({
|
||||
username,
|
||||
password = 'changeme',
|
||||
kibanaFeatures = { actions: ['all'] },
|
||||
}: {
|
||||
username: string;
|
||||
password?: string;
|
||||
kibanaFeatures?: FeaturesPrivileges;
|
||||
}): Promise<CreatedUser> => {
|
||||
const role: Role = {
|
||||
name: username,
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
indices: [],
|
||||
run_as: [],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
// Important: Saved Objects Managemnt should be set to `all` to ensure that authz
|
||||
// is not defaulted to the check done against SO's for Crowdstrike
|
||||
savedObjectsManagement: ['all'],
|
||||
...kibanaFeatures,
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await securityService.role.create(role.name, {
|
||||
kibana: role.kibana,
|
||||
elasticsearch: role.elasticsearch,
|
||||
});
|
||||
|
||||
await securityService.user.create(username, {
|
||||
password: 'changeme',
|
||||
full_name: role.name,
|
||||
roles: [role.name],
|
||||
});
|
||||
|
||||
return {
|
||||
username,
|
||||
password,
|
||||
deleteUser: async () => {
|
||||
await securityService.user.delete(role.name);
|
||||
await securityService.role.delete(role.name);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
before(async () => {
|
||||
const response = await supertest
|
||||
.post(`${getUrlPrefix('default')}/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.on('error', logErrorDetails)
|
||||
.send({
|
||||
name: 'My sub connector',
|
||||
connector_type_id: CROWDSTRIKE_CONNECTOR_ID,
|
||||
config: { url: 'https://some.non.existent.com' },
|
||||
secrets: { clientId: 'abc-123', clientSecret: 'test-secret' },
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
connectorId = response.body.id;
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
if (connectorId) {
|
||||
await supertest
|
||||
.delete(`${getUrlPrefix('default')}/api/actions/connector/${connectorId}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(({ ok, status }) => {
|
||||
// Should cover all success codes (ex. 204 (no content), 200, etc...)
|
||||
if (!ok) {
|
||||
throw new Error(
|
||||
`Expected delete to return a status code in the 200, but got ${status}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
connectorId = '';
|
||||
}
|
||||
});
|
||||
|
||||
const executeSubAction = async ({
|
||||
subAction,
|
||||
subActionParams,
|
||||
expectedHttpCode = 200,
|
||||
username = 'elastic',
|
||||
password = 'changeme',
|
||||
errorLogger = logErrorDetails,
|
||||
}: {
|
||||
supertest: SuperTest.SuperTest<SuperTest.Test>;
|
||||
subAction: string;
|
||||
subActionParams: Record<string, unknown>;
|
||||
expectedHttpCode?: number;
|
||||
username?: string;
|
||||
password?: string;
|
||||
errorLogger?: (err: any) => void;
|
||||
}) => {
|
||||
return supertestWithoutAuth
|
||||
.post(`${getUrlPrefix('default')}/api/actions/connector/${connectorId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.on('error', errorLogger)
|
||||
.auth(username, password)
|
||||
.send({
|
||||
params: {
|
||||
subAction,
|
||||
subActionParams,
|
||||
},
|
||||
})
|
||||
.expect(expectedHttpCode);
|
||||
};
|
||||
|
||||
describe('and user has NO privileges', () => {
|
||||
let user: CreatedUser;
|
||||
|
||||
before(async () => {
|
||||
user = await createUser({
|
||||
username: 'read_access_user',
|
||||
kibanaFeatures: { actions: ['read'] },
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
if (user) {
|
||||
await user.deleteUser();
|
||||
}
|
||||
});
|
||||
|
||||
for (const crowdstrikeSubAction of crowdstrikeSubActions) {
|
||||
it(`should deny execute of ${crowdstrikeSubAction}`, async () => {
|
||||
const execRes = await executeSubAction({
|
||||
supertest: supertestWithoutAuth,
|
||||
subAction: crowdstrikeSubAction,
|
||||
subActionParams: {},
|
||||
username: user.username,
|
||||
password: user.password,
|
||||
expectedHttpCode: 403,
|
||||
errorLogger: logErrorDetails.ignoreCodes([403]),
|
||||
});
|
||||
|
||||
expect(execRes.body).to.eql({
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: 'Unauthorized to execute a ".crowdstrike" action',
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('and user has proper privileges', () => {
|
||||
let user: CreatedUser;
|
||||
|
||||
before(async () => {
|
||||
user = await createUser({
|
||||
username: 'all_access_user',
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
if (user) {
|
||||
await user.deleteUser();
|
||||
// @ts-expect-error
|
||||
user = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
for (const crowdstrikeSubAction of crowdstrikeSubActions) {
|
||||
it(`should allow execute of ${crowdstrikeSubAction}`, async () => {
|
||||
const {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
body: { status, message, connector_id },
|
||||
} = await executeSubAction({
|
||||
supertest: supertestWithoutAuth,
|
||||
subAction: crowdstrikeSubAction,
|
||||
subActionParams: {},
|
||||
username: user.username,
|
||||
password: user.password,
|
||||
});
|
||||
|
||||
expect({ status, message, connector_id }).to.eql({
|
||||
status: 'error',
|
||||
message: 'an error occurred while running the action',
|
||||
connector_id: connectorId,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -21,6 +21,7 @@ export default function connectorsTests({ loadTestFile, getService }: FtrProvide
|
|||
|
||||
loadTestFile(require.resolve('./connector_types/oauth_access_token'));
|
||||
loadTestFile(require.resolve('./connector_types/cases_webhook'));
|
||||
loadTestFile(require.resolve('./connector_types/crowdstrike'));
|
||||
loadTestFile(require.resolve('./connector_types/jira'));
|
||||
loadTestFile(require.resolve('./connector_types/resilient'));
|
||||
loadTestFile(require.resolve('./connector_types/servicenow_itsm'));
|
||||
|
|
|
@ -53,6 +53,7 @@ export default function createRegisteredConnectorTypeTests({ getService }: FtrPr
|
|||
'.bedrock',
|
||||
'.sentinelone',
|
||||
'.cases',
|
||||
'.crowdstrike',
|
||||
].sort()
|
||||
);
|
||||
});
|
||||
|
|
|
@ -22,4 +22,5 @@ export default createTestConfig('spaces_only', {
|
|||
testFiles: [require.resolve('.')],
|
||||
reportName: 'X-Pack Alerting API Integration Tests - Actions',
|
||||
enableFooterInEmail: false,
|
||||
experimentalFeatures: ['crowdstrikeConnectorOn'],
|
||||
});
|
||||
|
|
|
@ -36,6 +36,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
'--xpack.task_manager.monitored_aggregated_stats_refresh_rate=5000',
|
||||
'--xpack.task_manager.ephemeral_tasks.enabled=false',
|
||||
'--xpack.task_manager.ephemeral_tasks.request_capacity=100',
|
||||
`--xpack.stack_connectors.enableExperimental=${JSON.stringify(['crowdstrikeConnectorOn'])}`,
|
||||
...findTestPluginPaths(path.resolve(__dirname, 'plugins')),
|
||||
],
|
||||
},
|
||||
|
|
|
@ -54,6 +54,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'actions:.bedrock',
|
||||
'actions:.cases',
|
||||
'actions:.cases-webhook',
|
||||
'actions:.crowdstrike',
|
||||
'actions:.d3security',
|
||||
'actions:.email',
|
||||
'actions:.gen-ai',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue