diff --git a/src/platform/packages/shared/kbn-actions-types/action_types.ts b/src/platform/packages/shared/kbn-actions-types/action_types.ts index 4f04c02a5fa34..f1f42c3348cb1 100644 --- a/src/platform/packages/shared/kbn-actions-types/action_types.ts +++ b/src/platform/packages/shared/kbn-actions-types/action_types.ts @@ -9,6 +9,11 @@ import type { LicenseType } from '@kbn/licensing-plugin/common/types'; +export enum SUB_FEATURE { + endpointSecurity, +} +export type SubFeature = keyof typeof SUB_FEATURE; + export interface ActionType { id: string; name: string; @@ -18,4 +23,5 @@ export interface ActionType { minimumLicenseRequired: LicenseType; supportedFeatureIds: string[]; isSystemActionType: boolean; + subFeature?: SubFeature; } diff --git a/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/apis/fetch_connector_types/transform_connector_types_response.test.ts b/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/apis/fetch_connector_types/transform_connector_types_response.test.ts index 2f0e70cdd8d50..26d5dd34baf6e 100644 --- a/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/apis/fetch_connector_types/transform_connector_types_response.test.ts +++ b/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/apis/fetch_connector_types/transform_connector_types_response.test.ts @@ -21,6 +21,7 @@ describe('transformConnectorTypesResponse', () => { minimum_license_required: 'basic', supported_feature_ids: ['stackAlerts'], is_system_action_type: true, + sub_feature: 'endpointSecurity', }, { id: 'actionType2Id', @@ -44,6 +45,7 @@ describe('transformConnectorTypesResponse', () => { minimumLicenseRequired: 'basic', supportedFeatureIds: ['stackAlerts'], isSystemActionType: true, + subFeature: 'endpointSecurity', }, { id: 'actionType2Id', diff --git a/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/apis/fetch_connector_types/transform_connector_types_response.ts b/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/apis/fetch_connector_types/transform_connector_types_response.ts index 576df0c171964..5f8041c480e36 100644 --- a/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/apis/fetch_connector_types/transform_connector_types_response.ts +++ b/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/apis/fetch_connector_types/transform_connector_types_response.ts @@ -15,6 +15,7 @@ const transformConnectorType: RewriteRequestCase = ({ minimum_license_required: minimumLicenseRequired, supported_feature_ids: supportedFeatureIds, is_system_action_type: isSystemActionType, + sub_feature: subFeature, ...res }: AsApiContract) => ({ enabledInConfig, @@ -22,6 +23,7 @@ const transformConnectorType: RewriteRequestCase = ({ minimumLicenseRequired, supportedFeatureIds, isSystemActionType, + subFeature, ...res, }); diff --git a/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/types/action_types.ts b/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/types/action_types.ts index 753db07057e55..6c125a795d874 100644 --- a/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/types/action_types.ts +++ b/src/platform/packages/shared/kbn-alerts-ui-shared/src/common/types/action_types.ts @@ -11,6 +11,7 @@ import type { ComponentType, ReactNode } from 'react'; import type { RuleActionParam, ActionVariable } from '@kbn/alerting-types'; import { IconType, RecursivePartial } from '@elastic/eui'; import { PublicMethodsOf } from '@kbn/utility-types'; +import { SubFeature } from '@kbn/actions-types'; import { TypeRegistry } from '../type_registry'; import { RuleFormParamsErrors } from './rule_types'; @@ -130,6 +131,7 @@ export interface ActionTypeModel = PublicMethodsOf< diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/mock/connectors.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/mock/connectors.ts index 34dfeb98ce29d..6f72a89205251 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/mock/connectors.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/mock/connectors.ts @@ -18,6 +18,7 @@ export const mockActionTypes = [ minimumLicenseRequired: 'basic', isSystemActionType: true, supportedFeatureIds: ['generativeAI'], + subFeature: undefined, } as ActionType, { id: '.bedrock', @@ -28,6 +29,7 @@ export const mockActionTypes = [ minimumLicenseRequired: 'basic', isSystemActionType: true, supportedFeatureIds: ['generativeAI'], + subFeature: undefined, } as ActionType, { id: '.gemini', @@ -38,6 +40,7 @@ export const mockActionTypes = [ minimumLicenseRequired: 'basic', isSystemActionType: true, supportedFeatureIds: ['generativeAI'], + subFeature: undefined, } as ActionType, ]; diff --git a/x-pack/platform/plugins/shared/actions/common/connector_feature_config.ts b/x-pack/platform/plugins/shared/actions/common/connector_feature_config.ts index cffa4c433b8f7..5a6b4c06476c4 100644 --- a/x-pack/platform/plugins/shared/actions/common/connector_feature_config.ts +++ b/x-pack/platform/plugins/shared/actions/common/connector_feature_config.ts @@ -28,6 +28,14 @@ export const SecurityConnectorFeatureId = 'siem'; export const GenerativeAIForSecurityConnectorFeatureId = 'generativeAIForSecurity'; export const GenerativeAIForObservabilityConnectorFeatureId = 'generativeAIForObservability'; export const GenerativeAIForSearchPlaygroundConnectorFeatureId = 'generativeAIForSearchPlayground'; +export const EndpointSecurityConnectorFeatureId = 'endpointSecurity'; + +const compatibilityEndpointSecurity = i18n.translate( + 'xpack.actions.availableConnectorFeatures.compatibility.endpointSecurity', + { + defaultMessage: 'Endpoint Security', + } +); const compatibilityGenerativeAIForSecurity = i18n.translate( 'xpack.actions.availableConnectorFeatures.compatibility.generativeAIForSecurity', @@ -120,6 +128,12 @@ export const GenerativeAIForSearchPlaygroundFeature: ConnectorFeatureConfig = { compatibility: compatibilityGenerativeAIForSearchPlayground, }; +export const EndpointSecurityConnectorFeature: ConnectorFeatureConfig = { + id: EndpointSecurityConnectorFeatureId, + name: compatibilityEndpointSecurity, + compatibility: compatibilityEndpointSecurity, +}; + const AllAvailableConnectorFeatures = { [AlertingConnectorFeature.id]: AlertingConnectorFeature, [CasesConnectorFeature.id]: CasesConnectorFeature, @@ -128,6 +142,7 @@ const AllAvailableConnectorFeatures = { [GenerativeAIForSecurityFeature.id]: GenerativeAIForSecurityFeature, [GenerativeAIForObservabilityFeature.id]: GenerativeAIForObservabilityFeature, [GenerativeAIForSearchPlaygroundFeature.id]: GenerativeAIForSearchPlaygroundFeature, + [EndpointSecurityConnectorFeature.id]: EndpointSecurityConnectorFeature, }; export function areValidFeatures(ids: string[]) { diff --git a/x-pack/platform/plugins/shared/actions/common/routes/connector/response/schemas/v1.ts b/x-pack/platform/plugins/shared/actions/common/routes/connector/response/schemas/v1.ts index 096e2f2943d80..d9e33dcdfce24 100644 --- a/x-pack/platform/plugins/shared/actions/common/routes/connector/response/schemas/v1.ts +++ b/x-pack/platform/plugins/shared/actions/common/routes/connector/response/schemas/v1.ts @@ -97,6 +97,13 @@ export const connectorTypesResponseSchema = schema.object({ is_system_action_type: schema.boolean({ meta: { description: 'Indicates whether the action is a system action.' }, }), + sub_feature: schema.maybe( + schema.oneOf([schema.literal('endpointSecurity')], { + meta: { + description: 'Indicates the sub-feature type the connector is grouped under.', + }, + }) + ), }); export const connectorExecuteResponseSchema = schema.object({ diff --git a/x-pack/platform/plugins/shared/actions/common/routes/connector/response/types/v1.ts b/x-pack/platform/plugins/shared/actions/common/routes/connector/response/types/v1.ts index 499cc2ec21d48..0dcee6bd4d3de 100644 --- a/x-pack/platform/plugins/shared/actions/common/routes/connector/response/types/v1.ts +++ b/x-pack/platform/plugins/shared/actions/common/routes/connector/response/types/v1.ts @@ -41,6 +41,7 @@ export interface ConnectorTypesResponse { minimum_license_required: ConnectorTypesResponseSchemaType['minimum_license_required']; supported_feature_ids: ConnectorTypesResponseSchemaType['supported_feature_ids']; is_system_action_type: ConnectorTypesResponseSchemaType['is_system_action_type']; + sub_feature?: ConnectorTypesResponseSchemaType['sub_feature']; } type ConnectorExecuteResponseSchemaType = TypeOf; diff --git a/x-pack/platform/plugins/shared/actions/common/types.ts b/x-pack/platform/plugins/shared/actions/common/types.ts index 7540056057873..153d529f76c5c 100644 --- a/x-pack/platform/plugins/shared/actions/common/types.ts +++ b/x-pack/platform/plugins/shared/actions/common/types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { SUB_FEATURE } from '@kbn/actions-types'; import { LicenseType } from '@kbn/licensing-plugin/common/types'; import { TaskErrorSource } from '@kbn/task-manager-plugin/common'; @@ -15,6 +16,9 @@ export { SecurityConnectorFeatureId, GenerativeAIForSecurityConnectorFeatureId, } from './connector_feature_config'; + +export type SubFeature = keyof typeof SUB_FEATURE; + export interface ActionType { id: string; name: string; @@ -24,6 +28,7 @@ export interface ActionType { minimumLicenseRequired: LicenseType; supportedFeatureIds: string[]; isSystemActionType: boolean; + subFeature?: SubFeature; } export enum InvalidEmailReason { diff --git a/x-pack/platform/plugins/shared/actions/server/action_type_registry.mock.ts b/x-pack/platform/plugins/shared/actions/server/action_type_registry.mock.ts index 399bf6ed22684..1671aa22278d4 100644 --- a/x-pack/platform/plugins/shared/actions/server/action_type_registry.mock.ts +++ b/x-pack/platform/plugins/shared/actions/server/action_type_registry.mock.ts @@ -19,7 +19,8 @@ const createActionTypeRegistryMock = () => { isActionExecutable: jest.fn(), isSystemActionType: jest.fn(), getUtils: jest.fn(), - getSystemActionKibanaPrivileges: jest.fn(), + getActionKibanaPrivileges: jest.fn(), + hasSubFeature: jest.fn(), }; return mocked; }; diff --git a/x-pack/platform/plugins/shared/actions/server/action_type_registry.test.ts b/x-pack/platform/plugins/shared/actions/server/action_type_registry.test.ts index 88ae6c3678552..609105924c4c7 100644 --- a/x-pack/platform/plugins/shared/actions/server/action_type_registry.test.ts +++ b/x-pack/platform/plugins/shared/actions/server/action_type_registry.test.ts @@ -10,7 +10,7 @@ import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; import { schema } from '@kbn/config-schema'; import { ActionTypeRegistry, ActionTypeRegistryOpts } from './action_type_registry'; import { ActionType, ExecutorType } from './types'; -import { ActionExecutor, ILicenseState, TaskRunnerFactory } from './lib'; +import { ActionExecutionSourceType, ActionExecutor, ILicenseState, TaskRunnerFactory } from './lib'; import { actionsConfigMock } from './actions_config.mock'; import { licenseStateMock } from './lib/license_state.mock'; import { ActionsConfigurationUtilities } from './actions_config'; @@ -249,7 +249,7 @@ describe('actionTypeRegistry', () => { ).not.toThrow(); }); - test('throws if the kibana privileges are defined but the action type is not a system action type', () => { + test('throws if the kibana privileges are defined but the action type is not a system action type or sub-feature type', () => { const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); expect(() => @@ -268,7 +268,7 @@ describe('actionTypeRegistry', () => { executor, }) ).toThrowErrorMatchingInlineSnapshot( - `"Kibana privilege authorization is only supported for system action types"` + `"Kibana privilege authorization is only supported for system actions and action types that are registered under a sub-feature"` ); }); }); @@ -421,6 +421,42 @@ describe('actionTypeRegistry', () => { }, ]); }); + + test('sets the subFeature correctly for sub-feature type actions', () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); + + actionTypeRegistry.register({ + id: 'test.sub-feature-action', + name: 'Test', + minimumLicenseRequired: 'platinum', + supportedFeatureIds: ['siem'], + getKibanaPrivileges: () => ['test/create-sub-feature'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, + }, + subFeature: 'endpointSecurity', + executor, + }); + + const actionTypes = actionTypeRegistry.list(); + + expect(actionTypes).toEqual([ + { + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + id: 'test.sub-feature-action', + isSystemActionType: false, + minimumLicenseRequired: 'platinum', + name: 'Test', + subFeature: 'endpointSecurity', + supportedFeatureIds: ['siem'], + }, + ]); + }); }); describe('has()', () => { @@ -767,8 +803,67 @@ describe('actionTypeRegistry', () => { }); }); - describe('getSystemActionKibanaPrivileges()', () => { - it('should get the kibana privileges correctly for system actions', () => { + describe('hasSubFeature()', () => { + it('should return true if the action type has a sub-feature type', () => { + const registry = new ActionTypeRegistry(actionTypeRegistryParams); + + registry.register({ + id: 'test.sub-feature-action', + name: 'Test', + minimumLicenseRequired: 'platinum', + supportedFeatureIds: ['siem'], + getKibanaPrivileges: () => ['test/create-sub-feature'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, + }, + subFeature: 'endpointSecurity', + executor, + }); + + const result = registry.hasSubFeature('test.sub-feature-action'); + expect(result).toBe(true); + }); + + it('should return false if the action type does not have a sub-feature type', () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + + const registry = new ActionTypeRegistry(actionTypeRegistryParams); + + registry.register({ + id: 'foo', + name: 'Foo', + minimumLicenseRequired: 'basic', + supportedFeatureIds: ['alerting'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, + }, + executor, + }); + + const allTypes = registry.getAllTypes(); + expect(allTypes.length).toBe(1); + + const result = registry.hasSubFeature('foo'); + expect(result).toBe(false); + }); + + it('should return false if the action type does not exists', () => { + const registry = new ActionTypeRegistry(actionTypeRegistryParams); + + const allTypes = registry.getAllTypes(); + expect(allTypes.length).toBe(0); + + const result = registry.hasSubFeature('not-exist'); + expect(result).toBe(false); + }); + }); + + describe('getActionKibanaPrivileges()', () => { + it('should get the kibana privileges correctly', () => { const registry = new ActionTypeRegistry(actionTypeRegistryParams); registry.register({ @@ -785,12 +880,28 @@ describe('actionTypeRegistry', () => { isSystemActionType: true, executor, }); + registry.register({ + id: 'test.sub-feature-action', + name: 'Test', + minimumLicenseRequired: 'platinum', + supportedFeatureIds: ['siem'], + getKibanaPrivileges: () => ['test/create-sub-feature'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, + }, + subFeature: 'endpointSecurity', + executor, + }); - const result = registry.getSystemActionKibanaPrivileges('test.system-action'); + let result = registry.getActionKibanaPrivileges('test.system-action'); expect(result).toEqual(['test/create']); + result = registry.getActionKibanaPrivileges('test.sub-feature-action'); + expect(result).toEqual(['test/create-sub-feature']); }); - it('should return an empty array if the system action does not define any kibana privileges', () => { + it('should return an empty array if the action type does not define any kibana privileges', () => { const registry = new ActionTypeRegistry(actionTypeRegistryParams); registry.register({ @@ -806,12 +917,27 @@ describe('actionTypeRegistry', () => { isSystemActionType: true, executor, }); + registry.register({ + id: 'test.sub-feature-action', + name: 'Test', + minimumLicenseRequired: 'platinum', + supportedFeatureIds: ['siem'], + validate: { + config: { schema: schema.object({}) }, + secrets: { schema: schema.object({}) }, + params: { schema: schema.object({}) }, + }, + subFeature: 'endpointSecurity', + executor, + }); - const result = registry.getSystemActionKibanaPrivileges('test.system-action'); + let result = registry.getActionKibanaPrivileges('test.system-action'); + expect(result).toEqual([]); + result = registry.getActionKibanaPrivileges('test.sub-feature-action'); expect(result).toEqual([]); }); - it('should return an empty array if the action type is not a system action', () => { + it('should return an empty array if the action type is not a system action or a sub-feature type action', () => { const registry = new ActionTypeRegistry(actionTypeRegistryParams); registry.register({ @@ -827,11 +953,11 @@ describe('actionTypeRegistry', () => { executor, }); - const result = registry.getSystemActionKibanaPrivileges('foo'); + const result = registry.getActionKibanaPrivileges('foo'); expect(result).toEqual([]); }); - it('should pass the params correctly', () => { + it('should pass the params and source correctly', () => { const registry = new ActionTypeRegistry(actionTypeRegistryParams); const getKibanaPrivileges = jest.fn().mockReturnValue(['test/create']); @@ -850,8 +976,15 @@ describe('actionTypeRegistry', () => { executor, }); - registry.getSystemActionKibanaPrivileges('test.system-action', { foo: 'bar' }); - expect(getKibanaPrivileges).toHaveBeenCalledWith({ params: { foo: 'bar' } }); + registry.getActionKibanaPrivileges( + 'test.system-action', + { foo: 'bar' }, + ActionExecutionSourceType.HTTP_REQUEST + ); + expect(getKibanaPrivileges).toHaveBeenCalledWith({ + params: { foo: 'bar' }, + source: ActionExecutionSourceType.HTTP_REQUEST, + }); }); }); }); diff --git a/x-pack/platform/plugins/shared/actions/server/action_type_registry.ts b/x-pack/platform/plugins/shared/actions/server/action_type_registry.ts index a5626450d9346..d0c79f25a63fe 100644 --- a/x-pack/platform/plugins/shared/actions/server/action_type_registry.ts +++ b/x-pack/platform/plugins/shared/actions/server/action_type_registry.ts @@ -11,7 +11,12 @@ import { RunContext, TaskManagerSetupContract, TaskCost } from '@kbn/task-manage import { LicensingPluginSetup } from '@kbn/licensing-plugin/server'; import { ActionType as CommonActionType, areValidFeatures } from '../common'; import { ActionsConfigurationUtilities } from './actions_config'; -import { getActionTypeFeatureUsageName, TaskRunnerFactory, ILicenseState } from './lib'; +import { + getActionTypeFeatureUsageName, + TaskRunnerFactory, + ILicenseState, + ActionExecutionSourceType, +} from './lib'; import { ActionType, InMemoryConnector, @@ -19,7 +24,6 @@ import { ActionTypeSecrets, ActionTypeParams, } from './types'; -import { isBidirectionalConnectorType } from './lib/bidirectional_connectors'; export interface ActionTypeRegistryOpts { licensing: LicensingPluginSetup; @@ -113,19 +117,25 @@ export class ActionTypeRegistry { Boolean(this.actionTypes.get(actionTypeId)?.isSystemActionType); /** - * Returns the kibana privileges of a system action type + * Returns true if the connector type has a sub-feature type defined */ - public getSystemActionKibanaPrivileges( + public hasSubFeature = (actionTypeId: string): boolean => + Boolean(this.actionTypes.get(actionTypeId)?.subFeature); + + /** + * Returns the kibana privileges + */ + public getActionKibanaPrivileges( actionTypeId: string, - params?: Params + params?: Params, + source?: ActionExecutionSourceType ): string[] { const actionType = this.actionTypes.get(actionTypeId); - if (!actionType?.isSystemActionType) { + if (!actionType?.isSystemActionType && !actionType?.subFeature) { return []; } - - return actionType?.getKibanaPrivileges?.({ params }) ?? []; + return actionType?.getKibanaPrivileges?.({ params, source }) ?? []; } /** @@ -175,11 +185,15 @@ export class ActionTypeRegistry { ); } - if (!actionType.isSystemActionType && actionType.getKibanaPrivileges) { + if ( + !actionType.isSystemActionType && + !actionType.subFeature && + actionType.getKibanaPrivileges + ) { throw new Error( i18n.translate('xpack.actions.actionTypeRegistry.register.invalidKibanaPrivileges', { defaultMessage: - 'Kibana privilege authorization is only supported for system action types', + 'Kibana privilege authorization is only supported for system actions and action types that are registered under a sub-feature', }) ); } @@ -233,26 +247,21 @@ export class ActionTypeRegistry { * Returns a list of registered action types [{ id, name, enabled }], filtered by featureId if provided. */ public list(featureId?: string): CommonActionType[] { - return ( - Array.from(this.actionTypes) - .filter(([_, actionType]) => - featureId ? actionType.supportedFeatureIds.includes(featureId) : 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, - minimumLicenseRequired: actionType.minimumLicenseRequired, - enabled: this.isActionTypeEnabled(actionTypeId), - enabledInConfig: this.actionsConfigUtils.isActionTypeEnabled(actionTypeId), - enabledInLicense: !!this.licenseState.isLicenseValidForActionType(actionType).isValid, - supportedFeatureIds: actionType.supportedFeatureIds, - isSystemActionType: !!actionType.isSystemActionType, - })) - ); + return Array.from(this.actionTypes) + .filter(([_, actionType]) => { + return featureId ? actionType.supportedFeatureIds.includes(featureId) : true; + }) + .map(([actionTypeId, actionType]) => ({ + id: actionTypeId, + name: actionType.name, + minimumLicenseRequired: actionType.minimumLicenseRequired, + enabled: this.isActionTypeEnabled(actionTypeId), + enabledInConfig: this.actionsConfigUtils.isActionTypeEnabled(actionTypeId), + enabledInLicense: !!this.licenseState.isLicenseValidForActionType(actionType).isValid, + supportedFeatureIds: actionType.supportedFeatureIds, + isSystemActionType: !!actionType.isSystemActionType, + subFeature: actionType.subFeature, + })); } /** diff --git a/x-pack/platform/plugins/shared/actions/server/application/connector/methods/execute/execute.ts b/x-pack/platform/plugins/shared/actions/server/application/connector/methods/execute/execute.ts index fc96f3a38caf5..7b19652e8e34f 100644 --- a/x-pack/platform/plugins/shared/actions/server/application/connector/methods/execute/execute.ts +++ b/x-pack/platform/plugins/shared/actions/server/application/connector/methods/execute/execute.ts @@ -7,7 +7,7 @@ import { v4 as uuidv4 } from 'uuid'; import { RawAction, ActionTypeExecutorResult } from '../../../../types'; -import { getSystemActionKibanaPrivileges } from '../../../../lib/get_system_action_kibana_privileges'; +import { getActionKibanaPrivileges } from '../../../../lib/get_action_kibana_privileges'; import { isPreconfigured } from '../../../../lib/is_preconfigured'; import { isSystemAction } from '../../../../lib/is_system_action'; import { ConnectorExecuteParams } from './types'; @@ -20,7 +20,6 @@ export async function execute( ): Promise> { const log = context.logger; const { actionId, params, source, relatedSavedObjects } = connectorExecuteParams; - const additionalPrivileges = getSystemActionKibanaPrivileges(context, actionId, params); let actionTypeId: string | undefined; try { @@ -42,6 +41,12 @@ export async function execute( log.debug(`Failed to retrieve actionTypeId for action [${actionId}]`, err); } + const additionalPrivileges = getActionKibanaPrivileges( + context, + actionTypeId, + params, + source?.type + ); await context.authorization.ensureAuthorized({ operation: 'execute', additionalPrivileges, diff --git a/x-pack/platform/plugins/shared/actions/server/application/connector/schemas/connector_type_schema.ts b/x-pack/platform/plugins/shared/actions/server/application/connector/schemas/connector_type_schema.ts index e5556ab5c4a33..44a0f9d29e577 100644 --- a/x-pack/platform/plugins/shared/actions/server/application/connector/schemas/connector_type_schema.ts +++ b/x-pack/platform/plugins/shared/actions/server/application/connector/schemas/connector_type_schema.ts @@ -23,4 +23,5 @@ export const connectorTypeSchema = schema.object({ ]), supportedFeatureIds: schema.arrayOf(schema.string()), isSystemActionType: schema.boolean(), + subFeature: schema.maybe(schema.oneOf([schema.literal('endpointSecurity')])), }); diff --git a/x-pack/platform/plugins/shared/actions/server/application/connector/types/connector_type.ts b/x-pack/platform/plugins/shared/actions/server/application/connector/types/connector_type.ts index 64be01365a8ba..8bce2832290c6 100644 --- a/x-pack/platform/plugins/shared/actions/server/application/connector/types/connector_type.ts +++ b/x-pack/platform/plugins/shared/actions/server/application/connector/types/connector_type.ts @@ -19,4 +19,5 @@ export interface ConnectorType { minimumLicenseRequired: ConnectorTypeSchemaType['minimumLicenseRequired']; supportedFeatureIds: ConnectorTypeSchemaType['supportedFeatureIds']; isSystemActionType: ConnectorTypeSchemaType['isSystemActionType']; + subFeature?: ConnectorTypeSchemaType['subFeature']; } diff --git a/x-pack/platform/plugins/shared/actions/server/authorization/actions_authorization.test.ts b/x-pack/platform/plugins/shared/actions/server/authorization/actions_authorization.test.ts index 7755071e69c24..ded29e83500ec 100644 --- a/x-pack/platform/plugins/shared/actions/server/authorization/actions_authorization.test.ts +++ b/x-pack/platform/plugins/shared/actions/server/authorization/actions_authorization.test.ts @@ -12,17 +12,10 @@ import { ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, } from '../constants/saved_objects'; -import { - CONNECTORS_ADVANCED_EXECUTE_PRIVILEGE_API_TAG, - CONNECTORS_BASIC_EXECUTE_PRIVILEGE_API_TAG, -} from '../feature'; -import { forEach } from 'lodash'; const request = {} as KibanaRequest; const mockAuthorizationAction = (type: string, operation: string) => `${type}/${operation}`; -const BASIC_EXECUTE_AUTHZ = `api:${CONNECTORS_BASIC_EXECUTE_PRIVILEGE_API_TAG}`; -const ADVANCED_EXECUTE_AUTHZ = `api:${CONNECTORS_ADVANCED_EXECUTE_PRIVILEGE_API_TAG}`; function mockSecurity() { const security = securityMock.createSetup(); @@ -87,7 +80,7 @@ describe('ensureAuthorized', () => { expect(authorization.actions.savedObject.get).toHaveBeenCalledWith('action', 'create'); expect(checkPrivileges).toHaveBeenCalledWith({ - kibana: [mockAuthorizationAction('action', 'create'), BASIC_EXECUTE_AUTHZ], + kibana: [mockAuthorizationAction('action', 'create')], }); }); @@ -127,7 +120,6 @@ describe('ensureAuthorized', () => { kibana: [ mockAuthorizationAction(ACTION_SAVED_OBJECT_TYPE, 'get'), mockAuthorizationAction(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'), - BASIC_EXECUTE_AUTHZ, ], }); }); @@ -207,59 +199,7 @@ describe('ensureAuthorized', () => { mockAuthorizationAction(ACTION_SAVED_OBJECT_TYPE, 'get'), mockAuthorizationAction(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'), 'test/create', - BASIC_EXECUTE_AUTHZ, ], }); }); - - describe('Bi-directional connectors', () => { - forEach(['.sentinelone', '.crowdstrike'], (actionTypeId) => { - test(`checks ${actionTypeId} connector privileges correctly`, async () => { - const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction< - ReturnType - > = jest.fn(); - - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - const actionsAuthorization = new ActionsAuthorization({ - request, - authorization, - }); - - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: true, - privileges: [ - { - privilege: mockAuthorizationAction('myType', 'execute'), - authorized: true, - }, - ], - }); - - await actionsAuthorization.ensureAuthorized({ - operation: 'execute', - actionTypeId, - }); - - 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(checkPrivileges).toHaveBeenCalledWith({ - kibana: [ - mockAuthorizationAction(ACTION_SAVED_OBJECT_TYPE, 'get'), - mockAuthorizationAction(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'), - ADVANCED_EXECUTE_AUTHZ, - ], - }); - }); - }); - }); }); diff --git a/x-pack/platform/plugins/shared/actions/server/authorization/actions_authorization.ts b/x-pack/platform/plugins/shared/actions/server/authorization/actions_authorization.ts index e392f8bbcc14a..26400a31c59eb 100644 --- a/x-pack/platform/plugins/shared/actions/server/authorization/actions_authorization.ts +++ b/x-pack/platform/plugins/shared/actions/server/authorization/actions_authorization.ts @@ -12,7 +12,6 @@ import { ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, } from '../constants/saved_objects'; -import { isBidirectionalConnectorType } from '../lib/bidirectional_connectors'; export interface ConstructorOptions { request: KibanaRequest; @@ -56,15 +55,7 @@ export class ActionsAuthorization { : [authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, operation)]; const { hasAllRequested } = await checkPrivileges({ - kibana: [ - ...privileges, - ...additionalPrivileges, - // 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 - isBidirectionalConnectorType(actionTypeId) - ? 'api:actions:execute-advanced-connectors' - : 'api:actions:execute-basic-connectors', - ], + kibana: [...privileges, ...additionalPrivileges], }); if (!hasAllRequested) { throw Boom.forbidden( diff --git a/x-pack/platform/plugins/shared/actions/server/feature.ts b/x-pack/platform/plugins/shared/actions/server/feature.ts index d4a9d3a3537bf..b9997ce64365f 100644 --- a/x-pack/platform/plugins/shared/actions/server/feature.ts +++ b/x-pack/platform/plugins/shared/actions/server/feature.ts @@ -7,15 +7,16 @@ import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; import { i18n } from '@kbn/i18n'; -import { KibanaFeatureScope } from '@kbn/features-plugin/common'; +import { KibanaFeatureConfig, KibanaFeatureScope } from '@kbn/features-plugin/common'; import { ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, CONNECTOR_TOKEN_SAVED_OBJECT_TYPE, } from './constants/saved_objects'; -export const CONNECTORS_ADVANCED_EXECUTE_PRIVILEGE_API_TAG = 'actions:execute-advanced-connectors'; -export const CONNECTORS_BASIC_EXECUTE_PRIVILEGE_API_TAG = 'actions:execute-basic-connectors'; +const ENDPOINT_SECURITY_EXECUTE_PRIVILEGE_API_TAG = 'actions:execute-endpoint-security-connectors'; +export const ENDPOINT_SECURITY_EXECUTE_PRIVILEGE = `api:${ENDPOINT_SECURITY_EXECUTE_PRIVILEGE_API_TAG}`; +export const ENDPOINT_SECURITY_SUB_ACTIONS_EXECUTE_PRIVILEGE = `api:actions:execute-endpoint-security-sub-actions`; /** * The order of appearance in the feature privilege page @@ -23,7 +24,7 @@ export const CONNECTORS_BASIC_EXECUTE_PRIVILEGE_API_TAG = 'actions:execute-basic */ const FEATURE_ORDER = 3000; -export const ACTIONS_FEATURE = { +export const ACTIONS_FEATURE: KibanaFeatureConfig = { id: 'actions', name: i18n.translate('xpack.actions.featureRegistry.actionsFeatureName', { defaultMessage: 'Actions and Connectors', @@ -38,10 +39,7 @@ export const ACTIONS_FEATURE = { privileges: { all: { app: [], - api: [ - CONNECTORS_ADVANCED_EXECUTE_PRIVILEGE_API_TAG, - CONNECTORS_BASIC_EXECUTE_PRIVILEGE_API_TAG, - ], + api: [], catalogue: [], management: { insightsAndAlerting: ['triggersActions', 'triggersActionsConnectors'], @@ -58,7 +56,7 @@ export const ACTIONS_FEATURE = { }, read: { app: [], - api: [CONNECTORS_BASIC_EXECUTE_PRIVILEGE_API_TAG], + api: [], catalogue: [], management: { insightsAndAlerting: ['triggersActions', 'triggersActionsConnectors'], @@ -71,4 +69,37 @@ export const ACTIONS_FEATURE = { ui: ['show', 'execute'], }, }, + subFeatures: [ + { + name: i18n.translate('xpack.actions.featureRegistry.endpointSecuritySubFeatureName', { + defaultMessage: 'Endpoint Security', + }), + description: i18n.translate( + 'xpack.actions.featureRegistry.endpointSecuritySubFeatureDescription', + { + defaultMessage: 'Includes: Sentinel One, Crowdstrike', + } + ), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + api: [ENDPOINT_SECURITY_EXECUTE_PRIVILEGE_API_TAG], + id: 'endpoint_security_execute', + name: i18n.translate( + 'xpack.actions.featureRegistry.endpointSecuritySubFeaturePrivilege', + { + defaultMessage: 'Execute', + } + ), + includeIn: 'all', + savedObject: { all: [], read: [] }, + ui: ['endpointSecurityExecute'], + }, + ], + }, + ], + }, + ], }; diff --git a/x-pack/platform/plugins/shared/actions/server/index.ts b/x-pack/platform/plugins/shared/actions/server/index.ts index d391911ff0fc3..5cfe1a9211297 100644 --- a/x-pack/platform/plugins/shared/actions/server/index.ts +++ b/x-pack/platform/plugins/shared/actions/server/index.ts @@ -53,3 +53,4 @@ export const config: PluginConfigDescriptor = { }; export { urlAllowListValidator } from './sub_action_framework/helpers'; +export { ActionExecutionSourceType } from './lib/action_execution_source'; diff --git a/x-pack/platform/plugins/shared/actions/server/lib/action_executor.test.ts b/x-pack/platform/plugins/shared/actions/server/lib/action_executor.test.ts index b89b997ca749d..167bdaf61a022 100644 --- a/x-pack/platform/plugins/shared/actions/server/lib/action_executor.test.ts +++ b/x-pack/platform/plugins/shared/actions/server/lib/action_executor.test.ts @@ -20,13 +20,13 @@ import { spacesServiceMock } from '@kbn/spaces-plugin/server/spaces_service/spac import { ActionType as ConnectorType, ConnectorUsageCollector } from '../types'; import { actionsAuthorizationMock, actionsMock } from '../mocks'; import { + ActionExecutionSourceType, asBackgroundTaskExecutionSource, asHttpRequestExecutionSource, asSavedObjectExecutionSource, } from './action_execution_source'; import { finished } from 'stream/promises'; import { PassThrough } from 'stream'; -import { SecurityConnectorFeatureId } from '../../common'; import { TaskErrorSource } from '@kbn/task-manager-plugin/common'; import { createTaskRunError, getErrorSource } from '@kbn/task-manager-plugin/server/task_running'; import { GEN_AI_TOKEN_COUNT_EVENT } from './event_based_telemetry'; @@ -42,6 +42,7 @@ const eventLogger = eventLoggerMock.create(); const CONNECTOR_ID = '1'; const ACTION_EXECUTION_ID = '2'; const ACTION_PARAMS = { foo: true }; +const SOURCE = { type: ActionExecutionSourceType.HTTP_REQUEST, source: 'test' }; const executeUnsecuredParams = { actionExecutionId: ACTION_EXECUTION_ID, @@ -56,6 +57,7 @@ const executeParams = { params: ACTION_PARAMS, executionId: '123abc', request: {} as KibanaRequest, + source: SOURCE, }; const spacesMock = spacesServiceMock.createStartContract(); @@ -132,6 +134,20 @@ const systemConnectorType: jest.Mocked = { executor: jest.fn(), }; +const subFeatureConnectorType: jest.Mocked = { + id: 'test.sub-feature-action', + name: 'Test', + minimumLicenseRequired: 'platinum', + supportedFeatureIds: ['siem'], + subFeature: 'endpointSecurity', + validate: { + config: { schema: schema.any() }, + secrets: { schema: schema.any() }, + params: { schema: schema.any() }, + }, + executor: jest.fn(), +}; + const connectorSavedObject = { id: CONNECTOR_ID, type: 'action', @@ -149,6 +165,16 @@ const connectorSavedObject = { references: [], }; +const subFeatureConnectorSavedObject = { + ...connectorSavedObject, + attributes: { + ...connectorSavedObject.attributes, + config: {}, + secrets: {}, + actionTypeId: 'test.sub-feature-action', + }, +}; + interface ActionUsage { request_body_bytes: number; } @@ -162,6 +188,7 @@ const getBaseExecuteStartEventLogDoc = (unsecured: boolean) => { kibana: { action: { execution: { + ...(unsecured ? {} : { source: 'http_request' }), uuid: ACTION_EXECUTION_ID, }, id: CONNECTOR_ID, @@ -303,6 +330,7 @@ describe('Action Executor', () => { params: { foo: true }, logger: loggerMock, connectorUsageCollector: expect.any(ConnectorUsageCollector), + ...(executeUnsecure ? {} : { source: SOURCE }), }); expect(loggerMock.debug).toBeCalledWith('executing action test:1: 1'); @@ -458,6 +486,7 @@ describe('Action Executor', () => { params: { foo: true }, logger: loggerMock, connectorUsageCollector: expect.any(ConnectorUsageCollector), + ...(executeUnsecure ? {} : { source: SOURCE }), }); expect(loggerMock.debug).toBeCalledWith('executing action test:preconfigured: Preconfigured'); @@ -542,6 +571,7 @@ describe('Action Executor', () => { logger: loggerMock, request: {}, connectorUsageCollector: expect.any(ConnectorUsageCollector), + ...(executeUnsecure ? {} : { source: SOURCE }), }); } @@ -619,6 +649,100 @@ describe('Action Executor', () => { }); }); + test(`${label} with sub-feature connector`, async () => { + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce( + subFeatureConnectorSavedObject + ); + connectorTypeRegistry.get.mockReturnValueOnce(subFeatureConnectorType); + connectorTypeRegistry.hasSubFeature.mockReturnValueOnce(true); + + if (executeUnsecure) { + await actionExecutor.executeUnsecured(executeUnsecuredParams); + } else { + await actionExecutor.execute(executeParams); + } + + if (executeUnsecure) { + expect(connectorTypeRegistry.hasSubFeature).not.toHaveBeenCalled(); + } else { + expect(connectorTypeRegistry.hasSubFeature).toHaveBeenCalled(); + expect(authorizationMock.ensureAuthorized).toBeCalled(); + } + + expect(encryptedSavedObjectsClient.getDecryptedAsInternalUser).toHaveBeenCalledWith( + 'action', + CONNECTOR_ID, + { namespace: 'some-namespace' } + ); + + expect(connectorTypeRegistry.get).toHaveBeenCalledWith('test.sub-feature-action'); + expect(connectorTypeRegistry.isActionExecutable).toHaveBeenCalledWith( + CONNECTOR_ID, + 'test.sub-feature-action', + { + notifyUsage: true, + } + ); + + expect(subFeatureConnectorType.executor).toHaveBeenCalledWith({ + actionId: CONNECTOR_ID, + services: expect.anything(), + config: {}, + secrets: {}, + params: { foo: true }, + logger: loggerMock, + connectorUsageCollector: expect.any(ConnectorUsageCollector), + ...(executeUnsecure ? {} : { source: SOURCE }), + }); + + expect(loggerMock.debug).toBeCalledWith('executing action test.sub-feature-action:1: 1'); + expect(eventLogger.logEvent).toHaveBeenCalledTimes(2); + + const execStartDoc = getBaseExecuteStartEventLogDoc(executeUnsecure); + const execDoc = getBaseExecuteEventLogDoc(executeUnsecure); + + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { + ...execStartDoc, + kibana: { + ...execStartDoc.kibana, + action: { + ...execStartDoc.kibana.action, + type_id: 'test.sub-feature-action', + }, + saved_objects: [ + { + id: '1', + namespace: 'some-namespace', + rel: 'primary', + type: 'action', + type_id: 'test.sub-feature-action', + }, + ], + }, + message: 'action started: test.sub-feature-action:1: 1', + }); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, { + ...execDoc, + kibana: { + ...execDoc.kibana, + action: { + ...execDoc.kibana.action, + type_id: 'test.sub-feature-action', + }, + saved_objects: [ + { + id: '1', + namespace: 'some-namespace', + rel: 'primary', + type: 'action', + type_id: 'test.sub-feature-action', + }, + ], + }, + message: 'action executed: test.sub-feature-action:1: 1', + }); + }); + test(`${label} should return error status with error message when executor returns an error`, async () => { encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce( connectorSavedObject @@ -646,59 +770,6 @@ describe('Action Executor', () => { }); }); - test(`${label} should handle SentinelOne connector type`, async () => { - const sentinelOneConnectorType: jest.Mocked = { - id: '.sentinelone', - name: 'sentinelone', - minimumLicenseRequired: 'enterprise', - supportedFeatureIds: [SecurityConnectorFeatureId], - validate: { - config: { schema: schema.any() }, - secrets: { schema: schema.any() }, - params: { schema: schema.any() }, - }, - executor: jest.fn(), - }; - const sentinelOneSavedObject = { - id: '1', - type: 'action', - attributes: { - name: '1', - actionTypeId: '.sentinelone', - config: { - bar: true, - }, - secrets: { - baz: true, - }, - isMissingSecrets: false, - }, - references: [], - }; - - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce( - sentinelOneSavedObject - ); - connectorTypeRegistry.get.mockReturnValueOnce(sentinelOneConnectorType); - - if (executeUnsecure) { - await actionExecutor.executeUnsecured({ - ...executeUnsecuredParams, - actionId: 'sentinel-one-connector-authz', - }); - expect(authorizationMock.ensureAuthorized).not.toHaveBeenCalled(); - } else { - await actionExecutor.execute({ - ...executeParams, - actionId: 'sentinel-one-connector-authz', - }); - expect(authorizationMock.ensureAuthorized).toHaveBeenCalledWith({ - operation: 'execute', - actionTypeId: '.sentinelone', - }); - } - }); - test(`${label} with taskInfo`, async () => { if (executeUnsecure) return; @@ -923,6 +994,7 @@ describe('Action Executor', () => { params: { foo: true }, logger: loggerMock, connectorUsageCollector: expect.any(ConnectorUsageCollector), + ...(executeUnsecure ? {} : { source: SOURCE }), }); }); @@ -955,6 +1027,7 @@ describe('Action Executor', () => { logger: loggerMock, request: {}, connectorUsageCollector: expect.any(ConnectorUsageCollector), + source: SOURCE, }); }); @@ -1024,6 +1097,7 @@ describe('Action Executor', () => { params: { foo: true }, logger: loggerMock, connectorUsageCollector: expect.any(ConnectorUsageCollector), + ...(executeUnsecure ? {} : { source: SOURCE }), }); expect(loggerMock.debug).toBeCalledWith('executing action test:preconfigured: Preconfigured'); @@ -1117,6 +1191,7 @@ describe('Action Executor', () => { logger: loggerMock, request: {}, connectorUsageCollector: expect.any(ConnectorUsageCollector), + source: SOURCE, }); expect(loggerMock.debug).toBeCalledWith( @@ -1337,6 +1412,7 @@ describe('Action Executor', () => { params: { foo: true }, logger: loggerMock, connectorUsageCollector: expect.any(ConnectorUsageCollector), + source: SOURCE, }); } }); @@ -1371,7 +1447,7 @@ describe('System actions', () => { getKibanaPrivileges: () => ['test/create'], }); connectorTypeRegistry.isSystemActionType.mockReturnValueOnce(true); - connectorTypeRegistry.getSystemActionKibanaPrivileges.mockReturnValueOnce(['test/create']); + connectorTypeRegistry.getActionKibanaPrivileges.mockReturnValueOnce(['test/create']); await actionExecutor.execute({ ...executeParams, actionId: 'system-connector-.cases' }); @@ -1382,13 +1458,13 @@ describe('System actions', () => { }); }); - test('pass the params to the connectorTypeRegistry when authorizing system actions', async () => { + test('pass the params and source to the connectorTypeRegistry when authorizing system actions', async () => { connectorTypeRegistry.get.mockReturnValueOnce({ ...systemConnectorType, getKibanaPrivileges: () => ['test/create'], }); connectorTypeRegistry.isSystemActionType.mockReturnValueOnce(true); - connectorTypeRegistry.getSystemActionKibanaPrivileges.mockReturnValueOnce(['test/create']); + connectorTypeRegistry.getActionKibanaPrivileges.mockReturnValueOnce(['test/create']); await actionExecutor.execute({ ...executeParams, @@ -1396,9 +1472,13 @@ describe('System actions', () => { actionId: 'system-connector-.cases', }); - expect(connectorTypeRegistry.getSystemActionKibanaPrivileges).toHaveBeenCalledWith('.cases', { - foo: 'bar', - }); + expect(connectorTypeRegistry.getActionKibanaPrivileges).toHaveBeenCalledWith( + '.cases', + { + foo: 'bar', + }, + ActionExecutionSourceType.HTTP_REQUEST + ); expect(authorizationMock.ensureAuthorized).toBeCalledWith({ actionTypeId: '.cases', @@ -1407,6 +1487,59 @@ describe('System actions', () => { }); }); }); + +describe('Sub-feature connectors', () => { + test('calls ensureAuthorized on sub-feature connectors if additional privileges are specified', async () => { + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce( + subFeatureConnectorSavedObject + ); + connectorTypeRegistry.get.mockReturnValueOnce({ + ...subFeatureConnectorType, + getKibanaPrivileges: () => ['test/create'], + }); + connectorTypeRegistry.hasSubFeature.mockReturnValueOnce(true); + connectorTypeRegistry.getActionKibanaPrivileges.mockReturnValueOnce(['test/create']); + + await actionExecutor.execute(executeParams); + + expect(authorizationMock.ensureAuthorized).toBeCalledWith({ + actionTypeId: 'test.sub-feature-action', + operation: 'execute', + additionalPrivileges: ['test/create'], + }); + }); + + test('pass the params and source to the connectorTypeRegistry when authorizing sub-feature connectors', async () => { + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce( + subFeatureConnectorSavedObject + ); + connectorTypeRegistry.get.mockReturnValueOnce({ + ...subFeatureConnectorType, + getKibanaPrivileges: () => ['test/create'], + }); + connectorTypeRegistry.hasSubFeature.mockReturnValueOnce(true); + connectorTypeRegistry.getActionKibanaPrivileges.mockReturnValueOnce(['test/create']); + + await actionExecutor.execute({ + ...executeParams, + params: { foo: 'bar' }, + }); + + expect(connectorTypeRegistry.getActionKibanaPrivileges).toHaveBeenCalledWith( + 'test.sub-feature-action', + { + foo: 'bar', + }, + ActionExecutionSourceType.HTTP_REQUEST + ); + + expect(authorizationMock.ensureAuthorized).toBeCalledWith({ + actionTypeId: 'test.sub-feature-action', + operation: 'execute', + additionalPrivileges: ['test/create'], + }); + }); +}); describe('Event log', () => { test('writes to event log for execute timeout', async () => { setupActionExecutorMock(); @@ -1474,6 +1607,7 @@ describe('Event log', () => { kibana: { action: { execution: { + source: 'http_request', uuid: '2', }, name: 'action-1', @@ -1528,6 +1662,7 @@ describe('Event log', () => { kibana: { action: { execution: { + source: 'http_request', uuid: '2', }, name: 'action-1', @@ -1591,6 +1726,7 @@ describe('Event log', () => { kibana: { action: { execution: { + source: 'http_request', usage: { request_body_bytes: 0, }, @@ -1675,6 +1811,7 @@ describe('Event log', () => { gen_ai: { usage: mockGenAi.usage, }, + source: 'http_request', usage: { request_body_bytes: 0, }, @@ -1775,6 +1912,7 @@ describe('Event log', () => { total_tokens: 35, }, }, + source: 'http_request', usage: { request_body_bytes: 0, }, diff --git a/x-pack/platform/plugins/shared/actions/server/lib/action_executor.ts b/x-pack/platform/plugins/shared/actions/server/lib/action_executor.ts index b0d8e7c5b469c..f94ba76ae3b91 100644 --- a/x-pack/platform/plugins/shared/actions/server/lib/action_executor.ts +++ b/x-pack/platform/plugins/shared/actions/server/lib/action_executor.ts @@ -48,12 +48,11 @@ import { ValidatorServices, } from '../types'; import { EVENT_LOG_ACTIONS } from '../constants/event_log'; -import { ActionExecutionSource } from './action_execution_source'; +import { ActionExecutionSource, ActionExecutionSourceType } from './action_execution_source'; 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; @@ -169,6 +168,7 @@ export class ActionExecutor { actionTypeId: connectorTypeId, actionTypeRegistry, authorization, + source: source?.type, }); }, executeLabel: `execute_action`, @@ -719,6 +719,7 @@ interface EnsureAuthorizedToExecuteOpts { params: Record; actionTypeRegistry: ActionTypeRegistryContract; authorization: ActionsAuthorization; + source?: ActionExecutionSourceType; } const ensureAuthorizedToExecute = async ({ @@ -727,12 +728,17 @@ const ensureAuthorizedToExecute = async ({ params, actionTypeRegistry, authorization, + source, }: EnsureAuthorizedToExecuteOpts) => { try { - if (actionTypeRegistry.isSystemActionType(actionTypeId)) { - const additionalPrivileges = actionTypeRegistry.getSystemActionKibanaPrivileges( + if ( + actionTypeRegistry.isSystemActionType(actionTypeId) || + actionTypeRegistry.hasSubFeature(actionTypeId) + ) { + const additionalPrivileges = actionTypeRegistry.getActionKibanaPrivileges( actionTypeId, - params + params, + source ); await authorization.ensureAuthorized({ @@ -740,13 +746,6 @@ const ensureAuthorizedToExecute = async ({ additionalPrivileges, actionTypeId, }); - } 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', - actionTypeId, - }); } } catch (error) { throw new ActionExecutionError(error.message, ActionExecutionErrorReason.Authorization, { diff --git a/x-pack/platform/plugins/shared/actions/server/lib/bidirectional_connectors.ts b/x-pack/platform/plugins/shared/actions/server/lib/bidirectional_connectors.ts deleted file mode 100644 index 49d33f9d016c3..0000000000000 --- a/x-pack/platform/plugins/shared/actions/server/lib/bidirectional_connectors.ts +++ /dev/null @@ -1,15 +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. - */ - -const BIDIRECTIONAL_CONNECTOR_TYPES = ['.sentinelone', '.crowdstrike']; -export const isBidirectionalConnectorType = (type: string | undefined) => { - if (!type) { - return false; - } - - return BIDIRECTIONAL_CONNECTOR_TYPES.includes(type); -}; diff --git a/x-pack/platform/plugins/shared/actions/server/lib/get_action_kibana_privileges.ts b/x-pack/platform/plugins/shared/actions/server/lib/get_action_kibana_privileges.ts new file mode 100644 index 0000000000000..118b575401ab4 --- /dev/null +++ b/x-pack/platform/plugins/shared/actions/server/lib/get_action_kibana_privileges.ts @@ -0,0 +1,26 @@ +/* + * 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 { ActionsClientContext } from '../actions_client'; +import { ActionExecutionSourceType } from './action_execution_source'; +import { ExecuteOptions } from './action_executor'; + +export function getActionKibanaPrivileges( + context: ActionsClientContext, + actionTypeId?: string, + params?: ExecuteOptions['params'], + source?: ActionExecutionSourceType +) { + const additionalPrivileges = + actionTypeId && + (context.actionTypeRegistry.isSystemActionType(actionTypeId) || + context.actionTypeRegistry.hasSubFeature(actionTypeId)) + ? context.actionTypeRegistry.getActionKibanaPrivileges(actionTypeId, params, source) + : []; + + return additionalPrivileges; +} diff --git a/x-pack/platform/plugins/shared/actions/server/lib/get_system_action_kibana_privileges.ts b/x-pack/platform/plugins/shared/actions/server/lib/get_system_action_kibana_privileges.ts deleted file mode 100644 index ef3b8ff853d17..0000000000000 --- a/x-pack/platform/plugins/shared/actions/server/lib/get_system_action_kibana_privileges.ts +++ /dev/null @@ -1,28 +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 { ActionsClientContext } from '../actions_client'; -import { ExecuteOptions } from './action_executor'; - -export function getSystemActionKibanaPrivileges( - context: ActionsClientContext, - connectorId: string, - params?: ExecuteOptions['params'] -) { - const inMemoryConnector = context.inMemoryConnectors.find( - (connector) => connector.id === connectorId - ); - - const additionalPrivileges = inMemoryConnector?.isSystemAction - ? context.actionTypeRegistry.getSystemActionKibanaPrivileges( - inMemoryConnector.actionTypeId, - params - ) - : []; - - return additionalPrivileges; -} diff --git a/x-pack/platform/plugins/shared/actions/server/routes/connector/list_types/list_types.test.ts b/x-pack/platform/plugins/shared/actions/server/routes/connector/list_types/list_types.test.ts index bf1ab91c5b6ab..148fe9973818f 100644 --- a/x-pack/platform/plugins/shared/actions/server/routes/connector/list_types/list_types.test.ts +++ b/x-pack/platform/plugins/shared/actions/server/routes/connector/list_types/list_types.test.ts @@ -12,6 +12,7 @@ import { mockHandlerArguments } from '../../_mock_handler_arguments'; import { listTypesRoute } from './list_types'; import { verifyAccessAndContext } from '../../verify_access_and_context'; import { actionsClientMock } from '../../../mocks'; +import { SubFeature } from '../../../../common'; jest.mock('../../verify_access_and_context', () => ({ verifyAccessAndContext: jest.fn(), @@ -43,6 +44,7 @@ describe('listTypesRoute', () => { minimumLicenseRequired: 'gold' as LicenseType, supportedFeatureIds: ['alerting'], isSystemActionType: false, + subFeature: 'endpointSecurity' as SubFeature, }, ]; @@ -61,6 +63,7 @@ describe('listTypesRoute', () => { "is_system_action_type": false, "minimum_license_required": "gold", "name": "name", + "sub_feature": "endpointSecurity", "supported_feature_ids": Array [ "alerting", ], @@ -80,6 +83,7 @@ describe('listTypesRoute', () => { supported_feature_ids: ['alerting'], minimum_license_required: 'gold', is_system_action_type: false, + sub_feature: 'endpointSecurity', }, ], }); @@ -131,6 +135,7 @@ describe('listTypesRoute', () => { "is_system_action_type": false, "minimum_license_required": "gold", "name": "name", + "sub_feature": undefined, "supported_feature_ids": Array [ "alerting", ], diff --git a/x-pack/platform/plugins/shared/actions/server/routes/connector/list_types/transforms/transform_list_types_response/v1.ts b/x-pack/platform/plugins/shared/actions/server/routes/connector/list_types/transforms/transform_list_types_response/v1.ts index e32bec2f9e1a1..e1ba6dd56d732 100644 --- a/x-pack/platform/plugins/shared/actions/server/routes/connector/list_types/transforms/transform_list_types_response/v1.ts +++ b/x-pack/platform/plugins/shared/actions/server/routes/connector/list_types/transforms/transform_list_types_response/v1.ts @@ -21,6 +21,7 @@ export const transformListTypesResponse = ( minimumLicenseRequired, supportedFeatureIds, isSystemActionType, + subFeature, }) => ({ id, name, @@ -30,6 +31,7 @@ export const transformListTypesResponse = ( minimum_license_required: minimumLicenseRequired, supported_feature_ids: supportedFeatureIds, is_system_action_type: isSystemActionType, + sub_feature: subFeature, }) ); }; diff --git a/x-pack/platform/plugins/shared/actions/server/routes/connector/list_types_system/list_types_system.test.ts b/x-pack/platform/plugins/shared/actions/server/routes/connector/list_types_system/list_types_system.test.ts index 7398d020f5972..1402dc0820721 100644 --- a/x-pack/platform/plugins/shared/actions/server/routes/connector/list_types_system/list_types_system.test.ts +++ b/x-pack/platform/plugins/shared/actions/server/routes/connector/list_types_system/list_types_system.test.ts @@ -12,6 +12,7 @@ import { mockHandlerArguments } from '../../_mock_handler_arguments'; import { listTypesWithSystemRoute } from './list_types_system'; import { verifyAccessAndContext } from '../../verify_access_and_context'; import { actionsClientMock } from '../../../mocks'; +import { SubFeature } from '../../../../common'; jest.mock('../../verify_access_and_context', () => ({ verifyAccessAndContext: jest.fn(), @@ -43,6 +44,7 @@ describe('listTypesWithSystemRoute', () => { minimumLicenseRequired: 'gold' as LicenseType, supportedFeatureIds: ['alerting'], isSystemActionType: true, + subFeature: 'endpointSecurity' as SubFeature, }, ]; @@ -61,6 +63,7 @@ describe('listTypesWithSystemRoute', () => { "is_system_action_type": true, "minimum_license_required": "gold", "name": "name", + "sub_feature": "endpointSecurity", "supported_feature_ids": Array [ "alerting", ], @@ -80,6 +83,7 @@ describe('listTypesWithSystemRoute', () => { supported_feature_ids: ['alerting'], minimum_license_required: 'gold', is_system_action_type: true, + sub_feature: 'endpointSecurity', }, ], }); @@ -131,6 +135,7 @@ describe('listTypesWithSystemRoute', () => { "is_system_action_type": false, "minimum_license_required": "gold", "name": "name", + "sub_feature": undefined, "supported_feature_ids": Array [ "alerting", ], diff --git a/x-pack/platform/plugins/shared/actions/server/sub_action_framework/register.test.ts b/x-pack/platform/plugins/shared/actions/server/sub_action_framework/register.test.ts index 8ae7f3cf3350f..51bb97f585dee 100644 --- a/x-pack/platform/plugins/shared/actions/server/sub_action_framework/register.test.ts +++ b/x-pack/platform/plugins/shared/actions/server/sub_action_framework/register.test.ts @@ -117,7 +117,29 @@ describe('Registration', () => { }); }); - it('add support for setting the kibana privileges for system connectors', async () => { + it('registers a sub-feature connector correctly', async () => { + register({ + actionTypeRegistry, + connector: { ...connector, subFeature: 'endpointSecurity' }, + configurationUtilities: mockedActionsConfig, + logger, + }); + + expect(actionTypeRegistry.register).toHaveBeenCalledTimes(1); + expect(actionTypeRegistry.register).toHaveBeenCalledWith({ + id: connector.id, + name: connector.name, + minimumLicenseRequired: connector.minimumLicenseRequired, + supportedFeatureIds: connector.supportedFeatureIds, + validate: expect.anything(), + executor: expect.any(Function), + getService: expect.any(Function), + renderParameterTemplates: expect.any(Function), + subFeature: 'endpointSecurity', + }); + }); + + it('add support for setting the kibana privileges', async () => { const getKibanaPrivileges = () => ['my-privilege']; register({ diff --git a/x-pack/platform/plugins/shared/actions/server/sub_action_framework/register.ts b/x-pack/platform/plugins/shared/actions/server/sub_action_framework/register.ts index 04e7f0d9ea417..33b420577c378 100644 --- a/x-pack/platform/plugins/shared/actions/server/sub_action_framework/register.ts +++ b/x-pack/platform/plugins/shared/actions/server/sub_action_framework/register.ts @@ -41,6 +41,7 @@ export const register = { /** @@ -119,8 +121,10 @@ export interface SubActionConnectorType { getService: (params: ServiceParams) => SubActionConnector; renderParameterTemplates?: RenderParameterTemplates; isSystemActionType?: boolean; + subFeature?: SubFeature; getKibanaPrivileges?: (args?: { params?: { subAction: string; subActionParams: Record }; + source?: ActionExecutionSourceType; }) => string[]; preSaveHook?: (params: PreSaveConnectorHookParams) => Promise; postSaveHook?: (params: PostSaveConnectorHookParams) => Promise; diff --git a/x-pack/platform/plugins/shared/actions/server/types.ts b/x-pack/platform/plugins/shared/actions/server/types.ts index e95e46d6d8cfb..c07f900a02217 100644 --- a/x-pack/platform/plugins/shared/actions/server/types.ts +++ b/x-pack/platform/plugins/shared/actions/server/types.ts @@ -23,7 +23,7 @@ import { ServiceParams } from './sub_action_framework/types'; import { ActionTypeRegistry } from './action_type_registry'; import { PluginSetupContract, PluginStartContract } from './plugin'; import { ActionsClient } from './actions_client'; -import { ActionTypeExecutorResult } from '../common'; +import { ActionTypeExecutorResult, SubFeature } from '../common'; import { TaskInfo } from './lib/action_executor'; import { ConnectorTokenClient } from './lib/connector_token_client'; import { ActionsConfigurationUtilities } from './actions_config'; @@ -40,7 +40,7 @@ export type ActionTypeParams = Record; export type ConnectorTokenClientContract = PublicMethodsOf; import { Connector, ConnectorWithExtraFindData } from './application/connector/types'; -import type { ActionExecutionSource } from './lib'; +import type { ActionExecutionSource, ActionExecutionSourceType } from './lib'; export { ActionExecutionSourceType } from './lib'; import { ConnectorUsageCollector } from './usage'; export { ConnectorUsageCollector } from './usage'; @@ -197,6 +197,7 @@ export interface ActionType< connector?: (config: Config, secrets: Secrets) => string | null; }; isSystemActionType?: boolean; + subFeature?: SubFeature; /** * Additional Kibana privileges to be checked by the actions framework. * Use it if you want to perform extra authorization checks based on a Kibana feature. @@ -208,7 +209,10 @@ export interface ActionType< * It only works with system actions and only when executing an action. * For all other scenarios they will be ignored */ - getKibanaPrivileges?: (args?: { params?: Params }) => string[]; + getKibanaPrivileges?: (args?: { + params?: Params; + source?: ActionExecutionSourceType; + }) => string[]; renderParameterTemplates?: RenderParameterTemplates; executor: ExecutorType; getService?: (params: ServiceParams) => SubActionConnector; diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.test.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.test.ts index f0421bc53e639..b47b3c43e0e40 100644 --- a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.test.ts @@ -207,6 +207,8 @@ describe('bulkEdit()', () => { isDeprecated: false, }, ]); + actionsClient.listTypes.mockReset(); + actionsClient.listTypes.mockResolvedValue([]); rulesClientParams.getActionsClient.mockResolvedValue(actionsClient); authorization.getFindAuthorizationFilter.mockResolvedValue({ diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/create/create_rule.test.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/create/create_rule.test.ts index 61a2c8aad9442..651d74def840f 100644 --- a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/create/create_rule.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/create/create_rule.test.ts @@ -162,6 +162,8 @@ describe('create()', () => { isSystemAction: false, }, ]); + actionsClient.listTypes.mockReset(); + actionsClient.listTypes.mockResolvedValue([]); actionsClient.isSystemAction.mockImplementation((id: string) => id === 'system_action-id'); diff --git a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/update/update_rule.test.ts b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/update/update_rule.test.ts index 4031d0b3175cf..7def9349c94a0 100644 --- a/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/update/update_rule.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/application/rule/methods/update/update_rule.test.ts @@ -196,6 +196,8 @@ describe('update()', () => { isSystemAction: false, }, ]); + actionsClient.listTypes.mockReset(); + actionsClient.listTypes.mockResolvedValue([]); rulesClientParams.getActionsClient.mockResolvedValue(actionsClient); unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); diff --git a/x-pack/platform/plugins/shared/alerting/server/rules_client/lib/validate_actions.test.ts b/x-pack/platform/plugins/shared/alerting/server/rules_client/lib/validate_actions.test.ts index e565f8b1b51ed..717ca3a8444a4 100644 --- a/x-pack/platform/plugins/shared/alerting/server/rules_client/lib/validate_actions.test.ts +++ b/x-pack/platform/plugins/shared/alerting/server/rules_client/lib/validate_actions.test.ts @@ -13,6 +13,7 @@ import { NormalizedAlertAction, NormalizedSystemAction, RulesClientContext } fro describe('validateActions', () => { const loggerErrorMock = jest.fn(); const getBulkMock = jest.fn(); + const listTypesMock = jest.fn(); const ruleType: jest.Mocked = { id: 'test', name: 'My test rule', @@ -68,10 +69,15 @@ describe('validateActions', () => { getActionsClient: () => { return { getBulk: getBulkMock, + listTypes: listTypesMock, }; }, }; + beforeEach(() => { + listTypesMock.mockResolvedValue([]); + }); + afterEach(() => { jest.resetAllMocks(); }); @@ -307,4 +313,22 @@ describe('validateActions', () => { '"Failed to validate actions due to the following error: Action\'s alertsFilter days has invalid values: (111:[0,8]) "' ); }); + + it('should return error message if the action is an Endpoint Security sub-feature connector type', async () => { + getBulkMock.mockResolvedValueOnce([ + { actionTypeId: 'test.endpointSecurity', name: 'test name' }, + ]); + listTypesMock.mockResolvedValueOnce([ + { + id: 'test.endpointSecurity', + name: 'endpoint security connector type', + subFeature: 'endpointSecurity', + }, + ]); + await expect( + validateActions(context as unknown as RulesClientContext, ruleType, data, false) + ).rejects.toThrowErrorMatchingInlineSnapshot( + '"Failed to validate actions due to the following error: Endpoint security connectors cannot be used as alerting actions"' + ); + }); }); diff --git a/x-pack/platform/plugins/shared/alerting/server/rules_client/lib/validate_actions.ts b/x-pack/platform/plugins/shared/alerting/server/rules_client/lib/validate_actions.ts index 4f5be980bae89..903ac43cdd8ec 100644 --- a/x-pack/platform/plugins/shared/alerting/server/rules_client/lib/validate_actions.ts +++ b/x-pack/platform/plugins/shared/alerting/server/rules_client/lib/validate_actions.ts @@ -76,6 +76,26 @@ export async function validateActions( ); } } + + // check for invalid Endpoint Security connectors + const allConnectorTypes = await actionsClient.listTypes({}); + const endpointSecurityConnectorTypeIds = new Set( + allConnectorTypes + .filter((type) => type.subFeature === 'endpointSecurity') + .map((type) => type.id) + ); + const endpointSecurityActionTypeIds = actionResults + .map((result) => result.actionTypeId) + .filter((id) => endpointSecurityConnectorTypeIds.has(id)); + + if (endpointSecurityActionTypeIds.length > 0) { + errors.push( + i18n.translate('xpack.alerting.rulesClient.validateActions.endpointSecurityConnector', { + defaultMessage: 'Endpoint security connectors cannot be used as alerting actions', + }) + ); + } + // check for actions with invalid action groups const { actionGroups: alertTypeActionGroups } = ruleType; const usedAlertActionGroups = actions.map((action) => action.group); diff --git a/x-pack/platform/plugins/shared/observability_solution/observability_ai_assistant/server/plugin.ts b/x-pack/platform/plugins/shared/observability_solution/observability_ai_assistant/server/plugin.ts index 13c440a38387c..564cabf3ed1f5 100644 --- a/x-pack/platform/plugins/shared/observability_solution/observability_ai_assistant/server/plugin.ts +++ b/x-pack/platform/plugins/shared/observability_solution/observability_ai_assistant/server/plugin.ts @@ -14,11 +14,6 @@ import { } from '@kbn/core/server'; import { mapValues } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { - CONNECTOR_TOKEN_SAVED_OBJECT_TYPE, - ACTION_SAVED_OBJECT_TYPE, - ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, -} from '@kbn/actions-plugin/server/constants/saved_objects'; import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { OBSERVABILITY_AI_ASSISTANT_FEATURE_ID } from '../common/feature'; import type { ObservabilityAIAssistantConfig } from './config'; @@ -80,11 +75,7 @@ export class ObservabilityAIAssistantPlugin api: [OBSERVABILITY_AI_ASSISTANT_FEATURE_ID, 'ai_assistant', 'manage_llm_product_doc'], catalogue: [OBSERVABILITY_AI_ASSISTANT_FEATURE_ID], savedObject: { - all: [ - ACTION_SAVED_OBJECT_TYPE, - ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, - CONNECTOR_TOKEN_SAVED_OBJECT_TYPE, - ], + all: [], read: [], }, ui: [aiAssistantCapabilities.show], diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/crowdstrike/index.ts b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/crowdstrike/index.ts index 0617822837c0a..304252e24771b 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/crowdstrike/index.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/crowdstrike/index.ts @@ -9,10 +9,18 @@ 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 { EndpointSecurityConnectorFeatureId } from '@kbn/actions-plugin/common'; +import { urlAllowListValidator, ActionExecutionSourceType } from '@kbn/actions-plugin/server'; +import { + ENDPOINT_SECURITY_EXECUTE_PRIVILEGE, + ENDPOINT_SECURITY_SUB_ACTIONS_EXECUTE_PRIVILEGE, +} from '@kbn/actions-plugin/server/feature'; import { ExperimentalFeatures } from '../../../common/experimental_features'; -import { CROWDSTRIKE_CONNECTOR_ID, CROWDSTRIKE_TITLE } from '../../../common/crowdstrike/constants'; +import { + CROWDSTRIKE_CONNECTOR_ID, + CROWDSTRIKE_TITLE, + SUB_ACTION, +} from '../../../common/crowdstrike/constants'; import { CrowdstrikeConfigSchema, CrowdstrikeSecretsSchema, @@ -31,6 +39,17 @@ export const getCrowdstrikeConnectorType = ( secrets: CrowdstrikeSecretsSchema, }, validators: [{ type: ValidatorType.CONFIG, validator: urlAllowListValidator('url') }], - supportedFeatureIds: [SecurityConnectorFeatureId], + supportedFeatureIds: [EndpointSecurityConnectorFeatureId], minimumLicenseRequired: 'enterprise' as const, + subFeature: 'endpointSecurity', + getKibanaPrivileges: (args) => { + const privileges = [ENDPOINT_SECURITY_EXECUTE_PRIVILEGE]; + if ( + args?.source === ActionExecutionSourceType.HTTP_REQUEST && + args?.params?.subAction !== SUB_ACTION.GET_AGENT_DETAILS + ) { + privileges.push(ENDPOINT_SECURITY_SUB_ACTIONS_EXECUTE_PRIVILEGE); + } + return privileges; + }, }); diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/sentinelone/index.ts b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/sentinelone/index.ts index 849d54e276e11..272448a7e2bb0 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/sentinelone/index.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/sentinelone/index.ts @@ -9,9 +9,15 @@ 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 { EndpointSecurityConnectorFeatureId } from '@kbn/actions-plugin/common'; +import { urlAllowListValidator, ActionExecutionSourceType } from '@kbn/actions-plugin/server'; +import { + ENDPOINT_SECURITY_EXECUTE_PRIVILEGE, + ENDPOINT_SECURITY_SUB_ACTIONS_EXECUTE_PRIVILEGE, +} from '@kbn/actions-plugin/server/feature'; import { SENTINELONE_CONNECTOR_ID, SENTINELONE_TITLE } from '../../../common/sentinelone/constants'; +import { SUB_ACTION } from '../../../common/sentinelone/constants'; + import { SentinelOneConfigSchema, SentinelOneSecretsSchema, @@ -32,7 +38,18 @@ export const getSentinelOneConnectorType = (): SubActionConnectorType< secrets: SentinelOneSecretsSchema, }, validators: [{ type: ValidatorType.CONFIG, validator: urlAllowListValidator('url') }], - supportedFeatureIds: [SecurityConnectorFeatureId], + supportedFeatureIds: [EndpointSecurityConnectorFeatureId], minimumLicenseRequired: 'enterprise' as const, renderParameterTemplates, + subFeature: 'endpointSecurity', + getKibanaPrivileges: (args) => { + const privileges = [ENDPOINT_SECURITY_EXECUTE_PRIVILEGE]; + if ( + args?.source === ActionExecutionSourceType.HTTP_REQUEST && + args?.params?.subAction !== SUB_ACTION.GET_AGENTS + ) { + privileges.push(ENDPOINT_SECURITY_SUB_ACTIONS_EXECUTE_PRIVILEGE); + } + return privileges; + }, }); diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/lib/capabilities.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/lib/capabilities.ts index 6d12ffafe0ef8..92e1962670792 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/lib/capabilities.ts +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/lib/capabilities.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { SubFeature } from '@kbn/actions-plugin/common'; import { RuleType } from '../../types'; import { InitialRule } from '../sections/rule_form/rule_reducer'; @@ -18,8 +19,9 @@ type Capabilities = Record; export const hasShowActionsCapability = (capabilities: Capabilities) => capabilities?.actions?.show; export const hasSaveActionsCapability = (capabilities: Capabilities) => capabilities?.actions?.save; -export const hasExecuteActionsCapability = (capabilities: Capabilities, actionTypeId?: string) => - actionTypeId === '.sentinelone' ? capabilities?.actions?.save : capabilities?.actions?.execute; +export const hasExecuteActionsCapability = (capabilities: Capabilities, subFeature?: SubFeature) => + subFeature ? capabilities?.actions[`${subFeature}Execute`] : capabilities?.actions?.execute; + export const hasDeleteActionsCapability = (capabilities: Capabilities) => capabilities?.actions?.delete; diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/header.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/header.tsx index 691e7d1ff87b5..19c2fc81b72e3 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/header.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/header.tsx @@ -22,6 +22,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; +import { SubFeature } from '@kbn/actions-plugin/common'; import { TECH_PREVIEW_DESCRIPTION, TECH_PREVIEW_LABEL } from '../../translations'; import { EditConnectorTabs } from '../../../../types'; import { useKibana } from '../../../../common/lib/kibana'; @@ -29,9 +30,9 @@ import { hasExecuteActionsCapability } from '../../../lib/capabilities'; const FlyoutHeaderComponent: React.FC<{ isExperimental?: boolean; + subFeature?: SubFeature; isPreconfigured: boolean; connectorName: string; - connectorTypeId: string; connectorTypeDesc: string; selectedTab: EditConnectorTabs; setTab: (nextPage: EditConnectorTabs) => void; @@ -39,9 +40,9 @@ const FlyoutHeaderComponent: React.FC<{ }> = ({ icon, isExperimental = false, + subFeature, isPreconfigured, connectorName, - connectorTypeId, connectorTypeDesc, selectedTab, setTab, @@ -51,7 +52,7 @@ const FlyoutHeaderComponent: React.FC<{ } = useKibana().services; const { euiTheme } = useEuiTheme(); - const canExecute = hasExecuteActionsCapability(capabilities, connectorTypeId); + const canExecute = hasExecuteActionsCapability(capabilities, subFeature); const setConfigurationTab = useCallback(() => { setTab(EditConnectorTabs.Configuration); diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/index.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/index.tsx index 1188f06a87d56..09ac4c1827022 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/index.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/index.tsx @@ -346,12 +346,12 @@ const EditConnectorFlyoutComponent: React.FC = ({ {selectedTab === EditConnectorTabs.Configuration && renderConfigurationTab()} diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx index 19fe53e4ee88c..b9ca9a4bd8283 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx @@ -339,9 +339,12 @@ const ActionsConnectorsList = ({ => { minimum_license_required: minimumLicenseRequired, supported_feature_ids: supportedFeatureIds, is_system_action_type: isSystemActionType, + sub_feature: subFeature, ...res }: AsApiContract) => ({ ...res, @@ -180,6 +181,7 @@ export const fetchActionTypes = async (): Promise => { minimumLicenseRequired, supportedFeatureIds, isSystemActionType, + subFeature, }) ); }; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/crowdstrike.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/crowdstrike.ts index c9d1ca8814592..626a7c77b02d8 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/crowdstrike.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/crowdstrike.ts @@ -208,23 +208,37 @@ export default function createCrowdstrikeTests({ getService }: FtrProviderContex }); for (const crowdstrikeSubAction of crowdstrikeSubActions) { - it(`should allow execute of ${crowdstrikeSubAction}`, async () => { + const isAllowedSubAction = crowdstrikeSubAction === SUB_ACTION.GET_AGENT_DETAILS; + it(`should ${ + isAllowedSubAction ? 'allow' : 'deny' + } execute of ${crowdstrikeSubAction}`, async () => { const { // eslint-disable-next-line @typescript-eslint/naming-convention - body: { status, message, connector_id }, + body: { status, message, connector_id, statusCode, error }, } = await executeSubAction({ supertest: supertestWithoutAuth, subAction: crowdstrikeSubAction, subActionParams: {}, username: user.username, password: user.password, + ...(isAllowedSubAction + ? {} + : { expectedHttpCode: 403, errorLogger: logErrorDetails.ignoreCodes([403]) }), }); - expect({ status, message, connector_id }).to.eql({ - status: 'error', - message: 'an error occurred while running the action', - connector_id: connectorId, - }); + if (isAllowedSubAction) { + expect({ status, message, connector_id }).to.eql({ + status: 'error', + message: 'an error occurred while running the action', + connector_id: connectorId, + }); + } else { + expect({ statusCode, message, error }).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: 'Unauthorized to execute a ".crowdstrike" action', + }); + } }); } }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/sentinelone.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/sentinelone.ts index bf6f88be08fdf..c27958e537e70 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/sentinelone.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/sentinelone.ts @@ -217,23 +217,37 @@ export default function createSentinelOneTests({ getService }: FtrProviderContex }); for (const s1SubAction of s1SubActions) { - it(`should allow execute of ${s1SubAction}`, async () => { + const isAllowedSubAction = s1SubAction === SUB_ACTION.GET_AGENTS; + it(`should ${ + isAllowedSubAction ? 'allow' : 'deny' + } execute of ${s1SubAction}`, async () => { const { // eslint-disable-next-line @typescript-eslint/naming-convention - body: { status, message, connector_id }, + body: { status, message, connector_id, statusCode, error }, } = await executeSubAction({ supertest: supertestWithoutAuth, subAction: s1SubAction, subActionParams: {}, username: user.username, password: user.password, + ...(isAllowedSubAction + ? {} + : { expectedHttpCode: 403, errorLogger: logErrorDetails.ignoreCodes([403]) }), }); - expect({ status, message, connector_id }).to.eql({ - status: 'error', - message: 'an error occurred while running the action', - connector_id: connectorId, - }); + if (isAllowedSubAction) { + expect({ status, message, connector_id }).to.eql({ + status: 'error', + message: 'an error occurred while running the action', + connector_id: connectorId, + }); + } else { + expect({ statusCode, message, error }).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: 'Unauthorized to execute a ".sentinelone" action', + }); + } }); } }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group5/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group5/tests/alerting/create.ts index d0716df4e2db9..47af7e546e5b7 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group5/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group5/tests/alerting/create.ts @@ -8,6 +8,10 @@ import expect from '@kbn/expect'; import { RULE_SAVED_OBJECT_TYPE } from '@kbn/alerting-plugin/server'; import { ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; +import { + SENTINELONE_CONNECTOR_ID, + SUB_ACTION, +} from '@kbn/stack-connectors-plugin/common/sentinelone/constants'; import { systemActionScenario, UserAtSpaceScenarios } from '../../../scenarios'; import { checkAAD, @@ -601,6 +605,74 @@ export default function createAlertTests({ getService }: FtrProviderContext) { throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); + + it('should handle create alert request appropriately with Endpoint Security actions', async () => { + let response = await supertest + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My sub connector', + connector_type_id: SENTINELONE_CONNECTOR_ID, + config: { url: 'https://some.non.existent.com' }, + secrets: { token: 'abc-123' }, + }) + .expect(200); + const connectorId = response.body.id; + + response = await supertestWithoutAuth + .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .send( + getTestRuleData({ + actions: [ + { + id: connectorId, + group: 'default', + params: { + subAction: SUB_ACTION.GET_AGENTS, + subActionParams: {}, + }, + }, + ], + }) + ); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'global_read at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getUnauthorizedErrorMessage('create', 'test.noop', 'alertsFixture'), + statusCode: 403, + }); + break; + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: 'Unauthorized to get actions', + statusCode: 403, + }); + break; + case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': + case 'superuser at space1': + case 'system_actions at space1': + expect(response.statusCode).to.eql(400); + expect(response.body).to.eql({ + error: 'Bad Request', + message: + 'Failed to validate actions due to the following error: Endpoint security connectors cannot be used as alerting actions', + statusCode: 400, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); }); } }); diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index bb00c34bc5808..015dbb27b6455 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -75,7 +75,7 @@ export default function ({ getService }: FtrProviderContext) { 'settings_read', ], fleet: ['all', 'read', 'minimal_all', 'minimal_read'], - actions: ['all', 'read', 'minimal_all', 'minimal_read'], + actions: ['all', 'read', 'minimal_all', 'minimal_read', 'endpoint_security_execute'], stackAlerts: ['all', 'read', 'minimal_all', 'minimal_read'], ml: ['all', 'read', 'minimal_all', 'minimal_read'], siem: [ diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts index a7b8ee3fd2091..6b4f4b505e3ba 100644 --- a/x-pack/test/api_integration/apis/security/privileges_basic.ts +++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts @@ -164,7 +164,7 @@ export default function ({ getService }: FtrProviderContext) { 'settings_read', ], fleet: ['all', 'read', 'minimal_all', 'minimal_read'], - actions: ['all', 'read', 'minimal_all', 'minimal_read'], + actions: ['all', 'read', 'minimal_all', 'minimal_read', 'endpoint_security_execute'], stackAlerts: ['all', 'read', 'minimal_all', 'minimal_read'], ml: ['all', 'read', 'minimal_all', 'minimal_read'], siem: [