From 1dca350451ec7420b13bd1e8f826eb56632fc8fd Mon Sep 17 00:00:00 2001 From: Alexi Doak <109488926+doakalexi@users.noreply.github.com> Date: Mon, 6 Jan 2025 13:53:35 -0500 Subject: [PATCH] [ResponseOps] Granular Connector RBAC (#203503) Part of https://github.com/elastic/kibana/issues/180908 ## Summary **EDR Connector Subfeature Privilege** This PR creates a new EDR connector sub-feature privilege under the read privilege for connectors. The read privilege currently allows users to execute connectors, and this new privilege will limit some of the connectors that can be executed. When the EDR privilege is turned on, users will be able to execute EDR connectors, and when it is off they will not execute. This new privilege includes SentinelOne and Crowdstrike connectors. To determine which connectors are considered EDR connectors, we leverage`getKibanaPrivileges` in the connector type definition. I removed the restrictions to use this field only for system actions and renamed `getSystemActionKibanaPrivileges` to `getActionKibanaPrivileges`. I also added a field, `subFeatureType `, to the connector type definition to help disable testing/executing an connectors that are restricted under a sub-feature. **EDR Connector Execution for Testing** The execution of EDR connectors using the API is limited to a single sub-action for testing purposes. This ensures users can still configure/test EDR connectors. In a separate [PR](https://github.com/elastic/kibana/pull/204804), I added back the SentinelOne and Crowdstrike params UIs with options restricted to one sub-action. **Rule API and Feature Configuration Updates** Validation has been added to the rule APIs to enforce restrictions on adding EDR connectors. The connector feature configuration has been updated to include a new feature ID, EdrForSecurityFeature, which ensures that EDR connectors are hidden on the rule form. Note: I saw that EDR connectors are also temporarily restricted in the Security Solution UI. To streamline this, I removed the `isBidirectionalConnectorType` check in `action_type_registry.ts`. Instead, I removed `SecurityConnectorFeatureId` from the `supportedFeatureIds` of the SentinelOne connector type definition. ### Checklist Check the PR satisfies following conditions. - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ## To test **EDR Connector Subfeature Privilege** 1. Create a new role and disable EDR connectors under the Actions and Connectors privilege 2. Create a new user and assign that role to user 3. Create a Sentinel One connector (It doesn't need to work, you can use fake values for the url and token) 4. Login as the new user and run the following in Dev Tools to verify that you aren't authorized execute the Sentinel One connector ``` POST kbn:/api/actions/connector/$CONNECTOR_ID/_execute { "params": { "subAction": "getAgents", "subActionParams": {} } } ``` 7. Update the role to enable EDR connectors and repeat the steps to verify that you are authorized to run the connector. (It will fail but verify it's not Unauthorized) **EDR Connector Execution for Testing** 1. Enable the EDR connectors privilege in the role you created above and log in as the user you created above. 2. Run the following in Dev Tools to verify that you are authorized execute the Sentinel One connector using only the `getAgents` sub-action. (It will fail but verify it's not `Unauthorized`) ``` POST kbn:/api/actions/connector/$CONNECTOR_ID/_execute { "params": { "subAction": "getAgents", "subActionParams": {} } } ``` 3. Run it again but replace the `subAction` with `isolateHost`. Verify that you get an unauthorized error. **Rule API and Feature Configuration Updates** 1. 1. Enable the EDR connectors privilege in the role you created above and log in as the user you created above. 2. Go to Stack Management 3. Try to create a rule, and verify that you don't see the SentinelOne connector. 4. Try to create a rule using the API and add your SentinelOne connector, verify that the API throws an error. ``` POST kbn:/api/alerting/rule { "tags": [], "params": {}, "schedule": { "interval": "1m" }, "consumer": "alerts", "name": "Always firing rule", "rule_type_id": "example.always-firing", "actions": [ { "group": "small", "id": "$CONNECTOR_ID", "params": { "subAction": "isolateAgent", "subActionParams": {} }, "frequency": { "notify_when": "onActionGroupChange", "throttle": null, "summary": false } } ], "alert_delay": { "active": 1 } } ``` 5. You can test the same behaviors when trying to add a SentinelOne connector to existing rules. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> (cherry picked from commit 23a5c6d2db1cd8d501e94c1af872d4ce7792e9ee) # Conflicts: # x-pack/platform/plugins/shared/actions/server/application/connector/methods/execute/execute.ts # x-pack/platform/plugins/shared/actions/server/authorization/actions_authorization.test.ts # x-pack/platform/plugins/shared/actions/server/authorization/actions_authorization.ts --- .../shared/kbn-actions-types/action_types.ts | 6 + ...transform_connector_types_response.test.ts | 2 + .../transform_connector_types_response.ts | 2 + .../src/common/types/action_types.ts | 2 + .../impl/mock/connectors.ts | 3 + .../common/connector_feature_config.ts | 15 + .../routes/connector/response/schemas/v1.ts | 7 + .../routes/connector/response/types/v1.ts | 1 + .../plugins/shared/actions/common/types.ts | 5 + .../server/action_type_registry.mock.ts | 3 +- .../server/action_type_registry.test.ts | 159 ++++++++++- .../actions/server/action_type_registry.ts | 69 +++-- .../connector/methods/execute/execute.ts | 9 +- .../schemas/connector_type_schema.ts | 1 + .../connector/types/connector_type.ts | 1 + .../actions_authorization.test.ts | 62 +---- .../authorization/actions_authorization.ts | 11 +- .../plugins/shared/actions/server/feature.ts | 49 +++- .../plugins/shared/actions/server/index.ts | 1 + .../server/lib/action_executor.test.ts | 258 ++++++++++++++---- .../actions/server/lib/action_executor.ts | 23 +- .../server/lib/bidirectional_connectors.ts | 15 - .../lib/get_action_kibana_privileges.ts | 26 ++ .../get_system_action_kibana_privileges.ts | 28 -- .../connector/list_types/list_types.test.ts | 5 + .../transform_list_types_response/v1.ts | 2 + .../list_types_system.test.ts | 5 + .../sub_action_framework/register.test.ts | 24 +- .../server/sub_action_framework/register.ts | 1 + .../server/sub_action_framework/types.ts | 4 + .../plugins/shared/actions/server/types.ts | 10 +- .../methods/bulk_edit/bulk_edit_rules.test.ts | 2 + .../rule/methods/create/create_rule.test.ts | 2 + .../rule/methods/update/update_rule.test.ts | 2 + .../rules_client/lib/validate_actions.test.ts | 24 ++ .../rules_client/lib/validate_actions.ts | 20 ++ .../server/plugin.ts | 11 +- .../connector_types/crowdstrike/index.ts | 27 +- .../connector_types/sentinelone/index.ts | 23 +- .../public/application/lib/capabilities.ts | 6 +- .../edit_connector_flyout/header.tsx | 7 +- .../edit_connector_flyout/index.tsx | 2 +- .../components/actions_connectors_list.tsx | 7 +- .../public/legacy_uptime/state/api/alerts.ts | 2 + .../group1/tests/alerting/create.ts | 72 +++++ .../actions/connector_types/crowdstrike.ts | 28 +- .../actions/connector_types/sentinelone.ts | 28 +- .../apis/security/privileges.ts | 2 +- .../apis/security/privileges_basic.ts | 2 +- 49 files changed, 790 insertions(+), 286 deletions(-) delete mode 100644 x-pack/platform/plugins/shared/actions/server/lib/bidirectional_connectors.ts create mode 100644 x-pack/platform/plugins/shared/actions/server/lib/get_action_kibana_privileges.ts delete mode 100644 x-pack/platform/plugins/shared/actions/server/lib/get_system_action_kibana_privileges.ts 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 f9922e0b61a8d..c0220c491a729 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 { @@ -30,7 +30,6 @@ export async function execute( (await getAuthorizationModeBySource(context.unsecuredSavedObjectsClient, source)) === AuthorizationMode.RBAC ) { - const additionalPrivileges = getSystemActionKibanaPrivileges(context, actionId, params); let actionTypeId: string | undefined; try { @@ -53,6 +52,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 41eab4fbc2e43..8ab8225e9897e 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 @@ -13,17 +13,10 @@ import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, } from '../constants/saved_objects'; import { AuthorizationMode } from './get_authorization_mode_by_source'; -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(); @@ -88,7 +81,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')], }); }); @@ -128,7 +121,6 @@ describe('ensureAuthorized', () => { kibana: [ mockAuthorizationAction(ACTION_SAVED_OBJECT_TYPE, 'get'), mockAuthorizationAction(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'), - BASIC_EXECUTE_AUTHZ, ], }); }); @@ -226,59 +218,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 5739af64050ee..a17b23c828afc 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'; import { AuthorizationMode } from './get_authorization_mode_by_source'; export interface ConstructorOptions { @@ -74,15 +73,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 1d5aa22ba07cf..04f1de476f5ae 100644 --- a/x-pack/platform/plugins/shared/actions/server/index.ts +++ b/x-pack/platform/plugins/shared/actions/server/index.ts @@ -151,3 +151,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 ad0d78aa9f2b3..232c82fc36c4c 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; @@ -171,6 +170,7 @@ export class ActionExecutor { actionTypeId: connectorTypeId, actionTypeRegistry, authorization, + source: source?.type, }); }, executeLabel: `execute_action`, @@ -724,6 +724,7 @@ interface EnsureAuthorizedToExecuteOpts { params: Record; actionTypeRegistry: ActionTypeRegistryContract; authorization: ActionsAuthorization; + source?: ActionExecutionSourceType; } const ensureAuthorizedToExecute = async ({ @@ -732,12 +733,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({ @@ -745,13 +751,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 e7370c7638a89..ec325f932d591 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 '../../legacy/_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 07d2d3adcd4f3..c78ab7cbe7caa 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 '../../legacy/_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 241f29c503a8a..b31e1b12c836b 100644 --- a/x-pack/platform/plugins/shared/actions/server/types.ts +++ b/x-pack/platform/plugins/shared/actions/server/types.ts @@ -24,7 +24,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'; @@ -41,7 +41,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'; @@ -199,6 +199,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. @@ -210,7 +211,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/group1/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts index d0716df4e2db9..47af7e546e5b7 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/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/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/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index e2c1536039da7..2a1daedccba6e 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 19fa3b3f5f1e5..dc2007eff54fd 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: [