mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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>
This commit is contained in:
parent
540963148d
commit
23a5c6d2db
49 changed files with 790 additions and 286 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -15,6 +15,7 @@ const transformConnectorType: RewriteRequestCase<ActionType> = ({
|
|||
minimum_license_required: minimumLicenseRequired,
|
||||
supported_feature_ids: supportedFeatureIds,
|
||||
is_system_action_type: isSystemActionType,
|
||||
sub_feature: subFeature,
|
||||
...res
|
||||
}: AsApiContract<ActionType>) => ({
|
||||
enabledInConfig,
|
||||
|
@ -22,6 +23,7 @@ const transformConnectorType: RewriteRequestCase<ActionType> = ({
|
|||
minimumLicenseRequired,
|
||||
supportedFeatureIds,
|
||||
isSystemActionType,
|
||||
subFeature,
|
||||
...res,
|
||||
});
|
||||
|
||||
|
|
|
@ -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<ActionConfig = any, ActionSecrets = any, Action
|
|||
hideInUi?: boolean;
|
||||
modalWidth?: number;
|
||||
isSystemActionType?: boolean;
|
||||
subFeature?: SubFeature;
|
||||
}
|
||||
|
||||
export type ActionTypeRegistryContract<Connector = unknown, Params = unknown> = PublicMethodsOf<
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
|
||||
|
|
|
@ -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[]) {
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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<typeof connectorExecuteResponseSchema>;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<Params extends ActionTypeParams = ActionTypeParams>(
|
||||
public hasSubFeature = (actionTypeId: string): boolean =>
|
||||
Boolean(this.actionTypes.get(actionTypeId)?.subFeature);
|
||||
|
||||
/**
|
||||
* Returns the kibana privileges
|
||||
*/
|
||||
public getActionKibanaPrivileges<Params extends ActionTypeParams = ActionTypeParams>(
|
||||
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,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { RawAction, ActionTypeExecutorResult } from '../../../../types';
|
||||
import { getSystemActionKibanaPrivileges } from '../../../../lib/get_system_action_kibana_privileges';
|
||||
import { getActionKibanaPrivileges } from '../../../../lib/get_action_kibana_privileges';
|
||||
import { isPreconfigured } from '../../../../lib/is_preconfigured';
|
||||
import { isSystemAction } from '../../../../lib/is_system_action';
|
||||
import { ConnectorExecuteParams } from './types';
|
||||
|
@ -20,7 +20,6 @@ export async function execute(
|
|||
): Promise<ActionTypeExecutorResult<unknown>> {
|
||||
const log = context.logger;
|
||||
const { actionId, params, source, relatedSavedObjects } = connectorExecuteParams;
|
||||
const additionalPrivileges = getSystemActionKibanaPrivileges(context, actionId, params);
|
||||
let actionTypeId: string | undefined;
|
||||
|
||||
try {
|
||||
|
@ -42,6 +41,12 @@ export async function execute(
|
|||
log.debug(`Failed to retrieve actionTypeId for action [${actionId}]`, err);
|
||||
}
|
||||
|
||||
const additionalPrivileges = getActionKibanaPrivileges(
|
||||
context,
|
||||
actionTypeId,
|
||||
params,
|
||||
source?.type
|
||||
);
|
||||
await context.authorization.ensureAuthorized({
|
||||
operation: 'execute',
|
||||
additionalPrivileges,
|
||||
|
|
|
@ -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')])),
|
||||
});
|
||||
|
|
|
@ -19,4 +19,5 @@ export interface ConnectorType {
|
|||
minimumLicenseRequired: ConnectorTypeSchemaType['minimumLicenseRequired'];
|
||||
supportedFeatureIds: ConnectorTypeSchemaType['supportedFeatureIds'];
|
||||
isSystemActionType: ConnectorTypeSchemaType['isSystemActionType'];
|
||||
subFeature?: ConnectorTypeSchemaType['subFeature'];
|
||||
}
|
||||
|
|
|
@ -12,17 +12,10 @@ import {
|
|||
ACTION_SAVED_OBJECT_TYPE,
|
||||
ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
|
||||
} from '../constants/saved_objects';
|
||||
import {
|
||||
CONNECTORS_ADVANCED_EXECUTE_PRIVILEGE_API_TAG,
|
||||
CONNECTORS_BASIC_EXECUTE_PRIVILEGE_API_TAG,
|
||||
} from '../feature';
|
||||
import { forEach } from 'lodash';
|
||||
|
||||
const request = {} as KibanaRequest;
|
||||
|
||||
const mockAuthorizationAction = (type: string, operation: string) => `${type}/${operation}`;
|
||||
const BASIC_EXECUTE_AUTHZ = `api:${CONNECTORS_BASIC_EXECUTE_PRIVILEGE_API_TAG}`;
|
||||
const ADVANCED_EXECUTE_AUTHZ = `api:${CONNECTORS_ADVANCED_EXECUTE_PRIVILEGE_API_TAG}`;
|
||||
|
||||
function mockSecurity() {
|
||||
const security = securityMock.createSetup();
|
||||
|
@ -87,7 +80,7 @@ describe('ensureAuthorized', () => {
|
|||
|
||||
expect(authorization.actions.savedObject.get).toHaveBeenCalledWith('action', 'create');
|
||||
expect(checkPrivileges).toHaveBeenCalledWith({
|
||||
kibana: [mockAuthorizationAction('action', 'create'), BASIC_EXECUTE_AUTHZ],
|
||||
kibana: [mockAuthorizationAction('action', 'create')],
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -127,7 +120,6 @@ describe('ensureAuthorized', () => {
|
|||
kibana: [
|
||||
mockAuthorizationAction(ACTION_SAVED_OBJECT_TYPE, 'get'),
|
||||
mockAuthorizationAction(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'),
|
||||
BASIC_EXECUTE_AUTHZ,
|
||||
],
|
||||
});
|
||||
});
|
||||
|
@ -207,59 +199,7 @@ describe('ensureAuthorized', () => {
|
|||
mockAuthorizationAction(ACTION_SAVED_OBJECT_TYPE, 'get'),
|
||||
mockAuthorizationAction(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'),
|
||||
'test/create',
|
||||
BASIC_EXECUTE_AUTHZ,
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bi-directional connectors', () => {
|
||||
forEach(['.sentinelone', '.crowdstrike'], (actionTypeId) => {
|
||||
test(`checks ${actionTypeId} connector privileges correctly`, async () => {
|
||||
const { authorization } = mockSecurity();
|
||||
const checkPrivileges: jest.MockedFunction<
|
||||
ReturnType<typeof authorization.checkPrivilegesDynamicallyWithRequest>
|
||||
> = 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,
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,7 +12,6 @@ import {
|
|||
ACTION_SAVED_OBJECT_TYPE,
|
||||
ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
|
||||
} from '../constants/saved_objects';
|
||||
import { isBidirectionalConnectorType } from '../lib/bidirectional_connectors';
|
||||
|
||||
export interface ConstructorOptions {
|
||||
request: KibanaRequest;
|
||||
|
@ -56,15 +55,7 @@ export class ActionsAuthorization {
|
|||
: [authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, operation)];
|
||||
|
||||
const { hasAllRequested } = await checkPrivileges({
|
||||
kibana: [
|
||||
...privileges,
|
||||
...additionalPrivileges,
|
||||
// SentinelOne and Crowdstrike sub-actions require that a user have `all` privilege to Actions and Connectors.
|
||||
// This is a temporary solution until a more robust RBAC approach can be implemented for sub-actions
|
||||
isBidirectionalConnectorType(actionTypeId)
|
||||
? 'api:actions:execute-advanced-connectors'
|
||||
: 'api:actions:execute-basic-connectors',
|
||||
],
|
||||
kibana: [...privileges, ...additionalPrivileges],
|
||||
});
|
||||
if (!hasAllRequested) {
|
||||
throw Boom.forbidden(
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -53,3 +53,4 @@ export const config: PluginConfigDescriptor<ActionsConfig> = {
|
|||
};
|
||||
|
||||
export { urlAllowListValidator } from './sub_action_framework/helpers';
|
||||
export { ActionExecutionSourceType } from './lib/action_execution_source';
|
||||
|
|
|
@ -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<ConnectorType> = {
|
|||
executor: jest.fn(),
|
||||
};
|
||||
|
||||
const subFeatureConnectorType: jest.Mocked<ConnectorType> = {
|
||||
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<ConnectorType> = {
|
||||
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,
|
||||
},
|
||||
|
|
|
@ -48,12 +48,11 @@ import {
|
|||
ValidatorServices,
|
||||
} from '../types';
|
||||
import { EVENT_LOG_ACTIONS } from '../constants/event_log';
|
||||
import { ActionExecutionSource } from './action_execution_source';
|
||||
import { ActionExecutionSource, ActionExecutionSourceType } from './action_execution_source';
|
||||
import { RelatedSavedObjects } from './related_saved_objects';
|
||||
import { createActionEventLogRecordObject } from './create_action_event_log_record_object';
|
||||
import { ActionExecutionError, ActionExecutionErrorReason } from './errors/action_execution_error';
|
||||
import type { ActionsAuthorization } from '../authorization/actions_authorization';
|
||||
import { isBidirectionalConnectorType } from './bidirectional_connectors';
|
||||
|
||||
// 1,000,000 nanoseconds in 1 millisecond
|
||||
const Millis2Nanos = 1000 * 1000;
|
||||
|
@ -169,6 +168,7 @@ export class ActionExecutor {
|
|||
actionTypeId: connectorTypeId,
|
||||
actionTypeRegistry,
|
||||
authorization,
|
||||
source: source?.type,
|
||||
});
|
||||
},
|
||||
executeLabel: `execute_action`,
|
||||
|
@ -719,6 +719,7 @@ interface EnsureAuthorizedToExecuteOpts {
|
|||
params: Record<string, unknown>;
|
||||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
authorization: ActionsAuthorization;
|
||||
source?: ActionExecutionSourceType;
|
||||
}
|
||||
|
||||
const ensureAuthorizedToExecute = async ({
|
||||
|
@ -727,12 +728,17 @@ const ensureAuthorizedToExecute = async ({
|
|||
params,
|
||||
actionTypeRegistry,
|
||||
authorization,
|
||||
source,
|
||||
}: EnsureAuthorizedToExecuteOpts) => {
|
||||
try {
|
||||
if (actionTypeRegistry.isSystemActionType(actionTypeId)) {
|
||||
const additionalPrivileges = actionTypeRegistry.getSystemActionKibanaPrivileges(
|
||||
if (
|
||||
actionTypeRegistry.isSystemActionType(actionTypeId) ||
|
||||
actionTypeRegistry.hasSubFeature(actionTypeId)
|
||||
) {
|
||||
const additionalPrivileges = actionTypeRegistry.getActionKibanaPrivileges(
|
||||
actionTypeId,
|
||||
params
|
||||
params,
|
||||
source
|
||||
);
|
||||
|
||||
await authorization.ensureAuthorized({
|
||||
|
@ -740,13 +746,6 @@ const ensureAuthorizedToExecute = async ({
|
|||
additionalPrivileges,
|
||||
actionTypeId,
|
||||
});
|
||||
} else if (isBidirectionalConnectorType(actionTypeId)) {
|
||||
// SentinelOne and Crowdstrike sub-actions require that a user have `all` privilege to Actions and Connectors.
|
||||
// This is a temporary solution until a more robust RBAC approach can be implemented for sub-actions
|
||||
await authorization.ensureAuthorized({
|
||||
operation: 'execute',
|
||||
actionTypeId,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
throw new ActionExecutionError(error.message, ActionExecutionErrorReason.Authorization, {
|
||||
|
|
|
@ -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);
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -12,6 +12,7 @@ import { mockHandlerArguments } from '../../_mock_handler_arguments';
|
|||
import { listTypesRoute } from './list_types';
|
||||
import { verifyAccessAndContext } from '../../verify_access_and_context';
|
||||
import { actionsClientMock } from '../../../mocks';
|
||||
import { SubFeature } from '../../../../common';
|
||||
|
||||
jest.mock('../../verify_access_and_context', () => ({
|
||||
verifyAccessAndContext: jest.fn(),
|
||||
|
@ -43,6 +44,7 @@ describe('listTypesRoute', () => {
|
|||
minimumLicenseRequired: 'gold' as LicenseType,
|
||||
supportedFeatureIds: ['alerting'],
|
||||
isSystemActionType: false,
|
||||
subFeature: 'endpointSecurity' as SubFeature,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -61,6 +63,7 @@ describe('listTypesRoute', () => {
|
|||
"is_system_action_type": false,
|
||||
"minimum_license_required": "gold",
|
||||
"name": "name",
|
||||
"sub_feature": "endpointSecurity",
|
||||
"supported_feature_ids": Array [
|
||||
"alerting",
|
||||
],
|
||||
|
@ -80,6 +83,7 @@ describe('listTypesRoute', () => {
|
|||
supported_feature_ids: ['alerting'],
|
||||
minimum_license_required: 'gold',
|
||||
is_system_action_type: false,
|
||||
sub_feature: 'endpointSecurity',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -131,6 +135,7 @@ describe('listTypesRoute', () => {
|
|||
"is_system_action_type": false,
|
||||
"minimum_license_required": "gold",
|
||||
"name": "name",
|
||||
"sub_feature": undefined,
|
||||
"supported_feature_ids": Array [
|
||||
"alerting",
|
||||
],
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@ import { mockHandlerArguments } from '../../_mock_handler_arguments';
|
|||
import { listTypesWithSystemRoute } from './list_types_system';
|
||||
import { verifyAccessAndContext } from '../../verify_access_and_context';
|
||||
import { actionsClientMock } from '../../../mocks';
|
||||
import { SubFeature } from '../../../../common';
|
||||
|
||||
jest.mock('../../verify_access_and_context', () => ({
|
||||
verifyAccessAndContext: jest.fn(),
|
||||
|
@ -43,6 +44,7 @@ describe('listTypesWithSystemRoute', () => {
|
|||
minimumLicenseRequired: 'gold' as LicenseType,
|
||||
supportedFeatureIds: ['alerting'],
|
||||
isSystemActionType: true,
|
||||
subFeature: 'endpointSecurity' as SubFeature,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -61,6 +63,7 @@ describe('listTypesWithSystemRoute', () => {
|
|||
"is_system_action_type": true,
|
||||
"minimum_license_required": "gold",
|
||||
"name": "name",
|
||||
"sub_feature": "endpointSecurity",
|
||||
"supported_feature_ids": Array [
|
||||
"alerting",
|
||||
],
|
||||
|
@ -80,6 +83,7 @@ describe('listTypesWithSystemRoute', () => {
|
|||
supported_feature_ids: ['alerting'],
|
||||
minimum_license_required: 'gold',
|
||||
is_system_action_type: true,
|
||||
sub_feature: 'endpointSecurity',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -131,6 +135,7 @@ describe('listTypesWithSystemRoute', () => {
|
|||
"is_system_action_type": false,
|
||||
"minimum_license_required": "gold",
|
||||
"name": "name",
|
||||
"sub_feature": undefined,
|
||||
"supported_feature_ids": Array [
|
||||
"alerting",
|
||||
],
|
||||
|
|
|
@ -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<TestConfig, TestSecrets>({
|
||||
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<TestConfig, TestSecrets>({
|
||||
|
|
|
@ -41,6 +41,7 @@ export const register = <Config extends ActionTypeConfig, Secrets extends Action
|
|||
executor,
|
||||
renderParameterTemplates: connector.renderParameterTemplates,
|
||||
isSystemActionType: connector.isSystemActionType,
|
||||
subFeature: connector.subFeature,
|
||||
getService: connector.getService,
|
||||
getKibanaPrivileges: connector.getKibanaPrivileges,
|
||||
preSaveHook: connector.preSaveHook,
|
||||
|
|
|
@ -18,8 +18,10 @@ import type {
|
|||
Services,
|
||||
ValidatorType as ValidationSchema,
|
||||
} from '../types';
|
||||
import { SubFeature } from '../../common';
|
||||
import type { SubActionConnector } from './sub_action_connector';
|
||||
import type { HookServices } from '../types';
|
||||
import { ActionExecutionSourceType } from '../lib';
|
||||
|
||||
export interface ServiceParams<Config, Secrets> {
|
||||
/**
|
||||
|
@ -119,8 +121,10 @@ export interface SubActionConnectorType<Config, Secrets> {
|
|||
getService: (params: ServiceParams<Config, Secrets>) => SubActionConnector<Config, Secrets>;
|
||||
renderParameterTemplates?: RenderParameterTemplates<ExecutorParams>;
|
||||
isSystemActionType?: boolean;
|
||||
subFeature?: SubFeature;
|
||||
getKibanaPrivileges?: (args?: {
|
||||
params?: { subAction: string; subActionParams: Record<string, unknown> };
|
||||
source?: ActionExecutionSourceType;
|
||||
}) => string[];
|
||||
preSaveHook?: (params: PreSaveConnectorHookParams<Config, Secrets>) => Promise<void>;
|
||||
postSaveHook?: (params: PostSaveConnectorHookParams<Config, Secrets>) => Promise<void>;
|
||||
|
|
|
@ -23,7 +23,7 @@ import { ServiceParams } from './sub_action_framework/types';
|
|||
import { ActionTypeRegistry } from './action_type_registry';
|
||||
import { PluginSetupContract, PluginStartContract } from './plugin';
|
||||
import { ActionsClient } from './actions_client';
|
||||
import { ActionTypeExecutorResult } from '../common';
|
||||
import { ActionTypeExecutorResult, SubFeature } from '../common';
|
||||
import { TaskInfo } from './lib/action_executor';
|
||||
import { ConnectorTokenClient } from './lib/connector_token_client';
|
||||
import { ActionsConfigurationUtilities } from './actions_config';
|
||||
|
@ -40,7 +40,7 @@ export type ActionTypeParams = Record<string, unknown>;
|
|||
export type ConnectorTokenClientContract = PublicMethodsOf<ConnectorTokenClient>;
|
||||
|
||||
import { Connector, ConnectorWithExtraFindData } from './application/connector/types';
|
||||
import type { ActionExecutionSource } from './lib';
|
||||
import type { ActionExecutionSource, ActionExecutionSourceType } from './lib';
|
||||
export { ActionExecutionSourceType } from './lib';
|
||||
import { ConnectorUsageCollector } from './usage';
|
||||
export { ConnectorUsageCollector } from './usage';
|
||||
|
@ -197,6 +197,7 @@ export interface ActionType<
|
|||
connector?: (config: Config, secrets: Secrets) => string | null;
|
||||
};
|
||||
isSystemActionType?: boolean;
|
||||
subFeature?: SubFeature;
|
||||
/**
|
||||
* Additional Kibana privileges to be checked by the actions framework.
|
||||
* Use it if you want to perform extra authorization checks based on a Kibana feature.
|
||||
|
@ -208,7 +209,10 @@ export interface ActionType<
|
|||
* It only works with system actions and only when executing an action.
|
||||
* For all other scenarios they will be ignored
|
||||
*/
|
||||
getKibanaPrivileges?: (args?: { params?: Params }) => string[];
|
||||
getKibanaPrivileges?: (args?: {
|
||||
params?: Params;
|
||||
source?: ActionExecutionSourceType;
|
||||
}) => string[];
|
||||
renderParameterTemplates?: RenderParameterTemplates<Params>;
|
||||
executor: ExecutorType<Config, Secrets, Params, ExecutorResultData>;
|
||||
getService?: (params: ServiceParams<Config, Secrets>) => SubActionConnector<Config, Secrets>;
|
||||
|
|
|
@ -207,6 +207,8 @@ describe('bulkEdit()', () => {
|
|||
isDeprecated: false,
|
||||
},
|
||||
]);
|
||||
actionsClient.listTypes.mockReset();
|
||||
actionsClient.listTypes.mockResolvedValue([]);
|
||||
rulesClientParams.getActionsClient.mockResolvedValue(actionsClient);
|
||||
|
||||
authorization.getFindAuthorizationFilter.mockResolvedValue({
|
||||
|
|
|
@ -162,6 +162,8 @@ describe('create()', () => {
|
|||
isSystemAction: false,
|
||||
},
|
||||
]);
|
||||
actionsClient.listTypes.mockReset();
|
||||
actionsClient.listTypes.mockResolvedValue([]);
|
||||
|
||||
actionsClient.isSystemAction.mockImplementation((id: string) => id === 'system_action-id');
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<UntypedNormalizedRuleType> = {
|
||||
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"'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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<string, any>;
|
|||
|
||||
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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -346,12 +346,12 @@ const EditConnectorFlyoutComponent: React.FC<EditConnectorFlyoutProps> = ({
|
|||
<FlyoutHeader
|
||||
isPreconfigured={connector.isPreconfigured}
|
||||
connectorName={connector.name}
|
||||
connectorTypeId={connector.actionTypeId}
|
||||
connectorTypeDesc={actionTypeModel?.selectMessage}
|
||||
setTab={handleSetTab}
|
||||
selectedTab={selectedTab}
|
||||
icon={actionTypeModel?.iconClass}
|
||||
isExperimental={actionTypeModel?.isExperimental}
|
||||
subFeature={actionTypeModel?.subFeature}
|
||||
/>
|
||||
<EuiFlyoutBody>
|
||||
{selectedTab === EditConnectorTabs.Configuration && renderConfigurationTab()}
|
||||
|
|
|
@ -339,9 +339,12 @@ const ActionsConnectorsList = ({
|
|||
<RunOperation
|
||||
canExecute={
|
||||
!!(
|
||||
hasExecuteActionsCapability(capabilities, item.actionTypeId) &&
|
||||
actionTypesIndex &&
|
||||
actionTypesIndex[item.actionTypeId]
|
||||
actionTypesIndex[item.actionTypeId] &&
|
||||
hasExecuteActionsCapability(
|
||||
capabilities,
|
||||
actionTypesIndex[item.actionTypeId].subFeature
|
||||
)
|
||||
)
|
||||
}
|
||||
item={item}
|
||||
|
|
|
@ -172,6 +172,7 @@ export const fetchActionTypes = async (): Promise<ActionType[]> => {
|
|||
minimum_license_required: minimumLicenseRequired,
|
||||
supported_feature_ids: supportedFeatureIds,
|
||||
is_system_action_type: isSystemActionType,
|
||||
sub_feature: subFeature,
|
||||
...res
|
||||
}: AsApiContract<ActionType>) => ({
|
||||
...res,
|
||||
|
@ -180,6 +181,7 @@ export const fetchActionTypes = async (): Promise<ActionType[]> => {
|
|||
minimumLicenseRequired,
|
||||
supportedFeatureIds,
|
||||
isSystemActionType,
|
||||
subFeature,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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: [
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue