mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[8.12] [ResponseOps] Fix Actions authz for SentinelOne to ensure that the user explicitly has ALL
privilege (#172528) (#173089)
# Backport This will backport the following commits from `main` to `8.12`: - [[ResponseOps] Fix Actions authz for SentinelOne to ensure that the user explicitly has `ALL` privilege (#172528)](https://github.com/elastic/kibana/pull/172528) <!--- Backport version: 8.9.7 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Paul Tavares","email":"56442535+paul-tavares@users.noreply.github.com"},"sourceCommit":{"committedDate":"2023-12-11T17:31:25Z","message":"[ResponseOps] Fix Actions authz for SentinelOne to ensure that the user explicitly has `ALL` privilege (#172528)\n\n## Summary\r\n\r\n- Fixes the Actions plugin sub-actions execution for SentinelOne\r\nconnector to ensure that a user must have `ALL` privilege to \"Action and\r\nConnectors\"","sha":"88ea8498b417d0b0f22364b434d57764ad958030","branchLabelMapping":{"^v8.13.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:Defend Workflows","v8.12.0","v8.13.0"],"number":172528,"url":"https://github.com/elastic/kibana/pull/172528","mergeCommit":{"message":"[ResponseOps] Fix Actions authz for SentinelOne to ensure that the user explicitly has `ALL` privilege (#172528)\n\n## Summary\r\n\r\n- Fixes the Actions plugin sub-actions execution for SentinelOne\r\nconnector to ensure that a user must have `ALL` privilege to \"Action and\r\nConnectors\"","sha":"88ea8498b417d0b0f22364b434d57764ad958030"}},"sourceBranch":"main","suggestedTargetBranches":["8.12"],"targetPullRequestStates":[{"branch":"8.12","label":"v8.12.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.13.0","labelRegex":"^v8.13.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/172528","number":172528,"mergeCommit":{"message":"[ResponseOps] Fix Actions authz for SentinelOne to ensure that the user explicitly has `ALL` privilege (#172528)\n\n## Summary\r\n\r\n- Fixes the Actions plugin sub-actions execution for SentinelOne\r\nconnector to ensure that a user must have `ALL` privilege to \"Action and\r\nConnectors\"","sha":"88ea8498b417d0b0f22364b434d57764ad958030"}}]}] BACKPORT--> Co-authored-by: Paul Tavares <56442535+paul-tavares@users.noreply.github.com>
This commit is contained in:
parent
f578d469a2
commit
fc3de170dc
17 changed files with 483 additions and 29 deletions
|
@ -43,7 +43,7 @@ import { actionsAuthorizationMock } from '../authorization/actions_authorization
|
|||
import { trackLegacyRBACExemption } from '../lib/track_legacy_rbac_exemption';
|
||||
import { ConnectorTokenClient } from '../lib/connector_token_client';
|
||||
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { Logger, SavedObject } from '@kbn/core/server';
|
||||
import { connectorTokenClientMock } from '../lib/connector_token_client.mock';
|
||||
import { inMemoryMetricsMock } from '../monitoring/in_memory_metrics.mock';
|
||||
import { getOAuthJwtAccessToken } from '../lib/get_oauth_jwt_access_token';
|
||||
|
@ -120,6 +120,14 @@ const executor: ExecutorType<{}, {}, {}, void> = async (options) => {
|
|||
const connectorTokenClient = connectorTokenClientMock.create();
|
||||
const inMemoryMetrics = inMemoryMetricsMock.create();
|
||||
|
||||
const actionTypeIdFromSavedObjectMock = (actionTypeId: string = 'my-action-type') => {
|
||||
return {
|
||||
attributes: {
|
||||
actionTypeId,
|
||||
},
|
||||
} as SavedObject;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
mockedLicenseState = licenseStateMock.create();
|
||||
|
@ -2664,6 +2672,7 @@ describe('execute()', () => {
|
|||
(getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => {
|
||||
return AuthorizationMode.RBAC;
|
||||
});
|
||||
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(actionTypeIdFromSavedObjectMock());
|
||||
await actionsClient.execute({
|
||||
actionId: 'action-id',
|
||||
params: {
|
||||
|
@ -2672,6 +2681,7 @@ describe('execute()', () => {
|
|||
source: asHttpRequestExecutionSource(request),
|
||||
});
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
actionTypeId: 'my-action-type',
|
||||
operation: 'execute',
|
||||
additionalPrivileges: [],
|
||||
});
|
||||
|
@ -2685,6 +2695,8 @@ describe('execute()', () => {
|
|||
new Error(`Unauthorized to execute all actions`)
|
||||
);
|
||||
|
||||
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(actionTypeIdFromSavedObjectMock());
|
||||
|
||||
await expect(
|
||||
actionsClient.execute({
|
||||
actionId: 'action-id',
|
||||
|
@ -2696,6 +2708,7 @@ describe('execute()', () => {
|
|||
).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to execute all actions]`);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
actionTypeId: 'my-action-type',
|
||||
operation: 'execute',
|
||||
additionalPrivileges: [],
|
||||
});
|
||||
|
@ -2768,12 +2781,15 @@ describe('execute()', () => {
|
|||
executor,
|
||||
});
|
||||
|
||||
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(actionTypeIdFromSavedObjectMock());
|
||||
|
||||
await actionsClient.execute({
|
||||
actionId: 'system-connector-.cases',
|
||||
params: {},
|
||||
});
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
actionTypeId: 'my-action-type',
|
||||
operation: 'execute',
|
||||
additionalPrivileges: ['test/create'],
|
||||
});
|
||||
|
@ -2832,12 +2848,15 @@ describe('execute()', () => {
|
|||
executor,
|
||||
});
|
||||
|
||||
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(actionTypeIdFromSavedObjectMock());
|
||||
|
||||
await actionsClient.execute({
|
||||
actionId: 'testPreconfigured',
|
||||
params: {},
|
||||
});
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
actionTypeId: 'my-action-type',
|
||||
operation: 'execute',
|
||||
additionalPrivileges: [],
|
||||
});
|
||||
|
@ -2895,12 +2914,15 @@ describe('execute()', () => {
|
|||
executor,
|
||||
});
|
||||
|
||||
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(actionTypeIdFromSavedObjectMock());
|
||||
|
||||
await actionsClient.execute({
|
||||
actionId: 'system-connector-.cases',
|
||||
params: { foo: 'bar' },
|
||||
});
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
actionTypeId: 'my-action-type',
|
||||
operation: 'execute',
|
||||
additionalPrivileges: ['test/create'],
|
||||
});
|
||||
|
@ -3032,6 +3054,7 @@ describe('bulkEnqueueExecution()', () => {
|
|||
},
|
||||
]);
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
actionTypeId: 'my-action-type',
|
||||
operation: 'execute',
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,7 +11,7 @@ import url from 'url';
|
|||
import { UsageCounter } from '@kbn/usage-collection-plugin/server';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { omitBy, isUndefined, compact } from 'lodash';
|
||||
import { omitBy, isUndefined, compact, uniq } from 'lodash';
|
||||
import {
|
||||
IScopedClusterClient,
|
||||
SavedObjectsClientContract,
|
||||
|
@ -681,14 +681,39 @@ export class ActionsClient {
|
|||
}: Omit<ExecuteOptions, 'request' | 'actionExecutionId'>): Promise<
|
||||
ActionTypeExecutorResult<unknown>
|
||||
> {
|
||||
const log = this.context.logger;
|
||||
|
||||
if (
|
||||
(await getAuthorizationModeBySource(this.context.unsecuredSavedObjectsClient, source)) ===
|
||||
AuthorizationMode.RBAC
|
||||
) {
|
||||
const additionalPrivileges = this.getSystemActionKibanaPrivileges(actionId, params);
|
||||
let actionTypeId: string | undefined;
|
||||
|
||||
try {
|
||||
if (this.isPreconfigured(actionId)) {
|
||||
const connector = this.context.inMemoryConnectors.find(
|
||||
(inMemoryConnector) => inMemoryConnector.id === actionId
|
||||
);
|
||||
|
||||
actionTypeId = connector?.actionTypeId;
|
||||
} else {
|
||||
// TODO: Optimize so we don't do another get on top of getAuthorizationModeBySource and within the actionExecutor.execute
|
||||
const { attributes } = await this.context.unsecuredSavedObjectsClient.get<RawAction>(
|
||||
'action',
|
||||
actionId
|
||||
);
|
||||
|
||||
actionTypeId = attributes.actionTypeId;
|
||||
}
|
||||
} catch (err) {
|
||||
log.debug(`Failed to retrieve actionTypeId for action [${actionId}]`, err);
|
||||
}
|
||||
|
||||
await this.context.authorization.ensureAuthorized({
|
||||
operation: 'execute',
|
||||
additionalPrivileges,
|
||||
actionTypeId,
|
||||
});
|
||||
} else {
|
||||
trackLegacyRBACExemption('execute', this.context.usageCounter);
|
||||
|
@ -723,6 +748,11 @@ export class ActionsClient {
|
|||
* inside the ActionExecutor at execution time
|
||||
*/
|
||||
await this.context.authorization.ensureAuthorized({ operation: 'execute' });
|
||||
await Promise.all(
|
||||
uniq(options.map((o) => o.actionTypeId)).map((actionTypeId) =>
|
||||
this.context.authorization.ensureAuthorized({ operation: 'execute', actionTypeId })
|
||||
)
|
||||
);
|
||||
}
|
||||
if (authModes[AuthorizationMode.Legacy] > 0) {
|
||||
trackLegacyRBACExemption(
|
||||
|
@ -740,7 +770,10 @@ export class ActionsClient {
|
|||
(await getAuthorizationModeBySource(this.context.unsecuredSavedObjectsClient, source)) ===
|
||||
AuthorizationMode.RBAC
|
||||
) {
|
||||
await this.context.authorization.ensureAuthorized({ operation: 'execute' });
|
||||
await this.context.authorization.ensureAuthorized({
|
||||
operation: 'execute',
|
||||
actionTypeId: options.actionTypeId,
|
||||
});
|
||||
} else {
|
||||
trackLegacyRBACExemption('ephemeralEnqueuedExecution', this.context.usageCounter);
|
||||
}
|
||||
|
|
|
@ -14,10 +14,16 @@ import {
|
|||
} from '../constants/saved_objects';
|
||||
import { AuthenticatedUser } from '@kbn/security-plugin/server';
|
||||
import { AuthorizationMode } from './get_authorization_mode_by_source';
|
||||
import {
|
||||
CONNECTORS_ADVANCED_EXECUTE_PRIVILEGE_API_TAG,
|
||||
CONNECTORS_BASIC_EXECUTE_PRIVILEGE_API_TAG,
|
||||
} from '../feature';
|
||||
|
||||
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();
|
||||
|
@ -83,7 +89,7 @@ describe('ensureAuthorized', () => {
|
|||
|
||||
expect(authorization.actions.savedObject.get).toHaveBeenCalledWith('action', 'create');
|
||||
expect(checkPrivileges).toHaveBeenCalledWith({
|
||||
kibana: [mockAuthorizationAction('action', 'create')],
|
||||
kibana: [mockAuthorizationAction('action', 'create'), BASIC_EXECUTE_AUTHZ],
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -123,6 +129,7 @@ describe('ensureAuthorized', () => {
|
|||
kibana: [
|
||||
mockAuthorizationAction(ACTION_SAVED_OBJECT_TYPE, 'get'),
|
||||
mockAuthorizationAction(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'),
|
||||
BASIC_EXECUTE_AUTHZ,
|
||||
],
|
||||
});
|
||||
});
|
||||
|
@ -225,6 +232,54 @@ describe('ensureAuthorized', () => {
|
|||
mockAuthorizationAction(ACTION_SAVED_OBJECT_TYPE, 'get'),
|
||||
mockAuthorizationAction(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'),
|
||||
'test/create',
|
||||
BASIC_EXECUTE_AUTHZ,
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('checks SentinelOne 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: '.sentinelone',
|
||||
});
|
||||
|
||||
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,
|
||||
],
|
||||
});
|
||||
});
|
||||
|
|
|
@ -74,7 +74,15 @@ export class ActionsAuthorization {
|
|||
: [authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, operation)];
|
||||
|
||||
const { hasAllRequested } = await checkPrivileges({
|
||||
kibana: [...privileges, ...additionalPrivileges],
|
||||
kibana: [
|
||||
...privileges,
|
||||
...additionalPrivileges,
|
||||
// SentinelOne sub-actions require that a user have `all` privilege to Actions and Connectors.
|
||||
// This is a temporary solution until a more robust RBAC approach can be implemented for sub-actions
|
||||
actionTypeId === '.sentinelone'
|
||||
? 'api:actions:execute-advanced-connectors'
|
||||
: 'api:actions:execute-basic-connectors',
|
||||
],
|
||||
});
|
||||
if (!hasAllRequested) {
|
||||
throw Boom.forbidden(
|
||||
|
|
|
@ -13,6 +13,9 @@ import {
|
|||
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';
|
||||
|
||||
/**
|
||||
* The order of appearance in the feature privilege page
|
||||
* under the management section.
|
||||
|
@ -33,7 +36,10 @@ export const ACTIONS_FEATURE = {
|
|||
privileges: {
|
||||
all: {
|
||||
app: [],
|
||||
api: [],
|
||||
api: [
|
||||
CONNECTORS_ADVANCED_EXECUTE_PRIVILEGE_API_TAG,
|
||||
CONNECTORS_BASIC_EXECUTE_PRIVILEGE_API_TAG,
|
||||
],
|
||||
catalogue: [],
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions', 'triggersActionsConnectors'],
|
||||
|
@ -50,7 +56,7 @@ export const ACTIONS_FEATURE = {
|
|||
},
|
||||
read: {
|
||||
app: [],
|
||||
api: [],
|
||||
api: [CONNECTORS_BASIC_EXECUTE_PRIVILEGE_API_TAG],
|
||||
catalogue: [],
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions', 'triggersActionsConnectors'],
|
||||
|
|
|
@ -835,6 +835,7 @@ test('successfully authorize system actions', async () => {
|
|||
await actionExecutor.execute({ ...executeParams, actionId: 'system-connector-.cases' });
|
||||
|
||||
expect(authorizationMock.ensureAuthorized).toBeCalledWith({
|
||||
actionTypeId: '.cases',
|
||||
operation: 'execute',
|
||||
additionalPrivileges: ['test/create'],
|
||||
});
|
||||
|
@ -875,7 +876,10 @@ test('Execute of SentinelOne sub-actions require create privilege', async () =>
|
|||
|
||||
await actionExecutor.execute({ ...executeParams, actionId: 'sentinel-one-connector-authz' });
|
||||
|
||||
expect(authorizationMock.ensureAuthorized).toHaveBeenCalledWith({ operation: 'create' });
|
||||
expect(authorizationMock.ensureAuthorized).toHaveBeenCalledWith({
|
||||
operation: 'execute',
|
||||
actionTypeId: '.sentinelone',
|
||||
});
|
||||
});
|
||||
|
||||
test('pass the params to the actionTypeRegistry when authorizing system actions', async () => {
|
||||
|
@ -909,6 +913,7 @@ test('pass the params to the actionTypeRegistry when authorizing system actions'
|
|||
});
|
||||
|
||||
expect(authorizationMock.ensureAuthorized).toBeCalledWith({
|
||||
actionTypeId: '.cases',
|
||||
operation: 'execute',
|
||||
additionalPrivileges: ['test/create'],
|
||||
});
|
||||
|
|
|
@ -565,14 +565,17 @@ const ensureAuthorizedToExecute = async ({
|
|||
params
|
||||
);
|
||||
|
||||
await authorization.ensureAuthorized({ operation: 'execute', additionalPrivileges });
|
||||
}
|
||||
|
||||
// SentinelOne 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
|
||||
if (actionTypeId === '.sentinelone') {
|
||||
await authorization.ensureAuthorized({
|
||||
operation: 'create',
|
||||
operation: 'execute',
|
||||
additionalPrivileges,
|
||||
actionTypeId,
|
||||
});
|
||||
} else if (actionTypeId === '.sentinelone') {
|
||||
// SentinelOne 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) {
|
||||
|
|
|
@ -16,7 +16,7 @@ export const allowedExperimentalValues = Object.freeze({
|
|||
sentinelOneConnectorOn: false,
|
||||
});
|
||||
|
||||
type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
|
||||
export type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
|
||||
type Mutable<T> = { -readonly [P in keyof T]: T[P] };
|
||||
|
||||
const allowedKeys = Object.keys(allowedExperimentalValues) as Readonly<ExperimentalConfigKeys>;
|
||||
|
|
|
@ -10,6 +10,8 @@ import getPort from 'get-port';
|
|||
import { CA_CERT_PATH } from '@kbn/dev-utils';
|
||||
import { FtrConfigProviderContext, findTestPluginPaths } from '@kbn/test';
|
||||
import { getAllExternalServiceSimulatorPaths } from '@kbn/actions-simulators-plugin/server/plugin';
|
||||
import { ExperimentalConfigKeys } from '@kbn/stack-connectors-plugin/common/experimental_features';
|
||||
import { SENTINELONE_CONNECTOR_ID } from '@kbn/stack-connectors-plugin/common/sentinelone/constants';
|
||||
import { services } from './services';
|
||||
import { getTlsWebhookServerUrls } from './lib/get_tls_webhook_servers';
|
||||
|
||||
|
@ -29,6 +31,7 @@ interface CreateTestConfigOptions {
|
|||
useDedicatedTaskRunner: boolean;
|
||||
enableFooterInEmail?: boolean;
|
||||
maxScheduledPerMinute?: number;
|
||||
experimentalFeatures?: ExperimentalConfigKeys;
|
||||
}
|
||||
|
||||
// test.not-enabled is specifically not enabled
|
||||
|
@ -48,6 +51,7 @@ const enabledActionTypes = [
|
|||
'.resilient',
|
||||
'.gen-ai',
|
||||
'.d3security',
|
||||
SENTINELONE_CONNECTOR_ID,
|
||||
'.slack',
|
||||
'.slack_api',
|
||||
'.tines',
|
||||
|
@ -85,6 +89,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
|
|||
useDedicatedTaskRunner,
|
||||
enableFooterInEmail = true,
|
||||
maxScheduledPerMinute,
|
||||
experimentalFeatures = [],
|
||||
} = options;
|
||||
|
||||
return async ({ readConfigFile }: FtrConfigProviderContext) => {
|
||||
|
@ -344,6 +349,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
|
|||
'--xpack.task_manager.allow_reading_invalid_state=false',
|
||||
'--xpack.task_manager.requeue_invalid_tasks.enabled=true',
|
||||
'--xpack.actions.queued.max=500',
|
||||
`--xpack.stack_connectors.enableExperimental=${JSON.stringify(experimentalFeatures)}`,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 SuperTest from 'supertest';
|
||||
import { ToolingLog } from '@kbn/tooling-log';
|
||||
|
||||
export interface LogErrorDetailsInterface {
|
||||
(this: SuperTest.Test, err: Error & { response?: any }): SuperTest.Test;
|
||||
ignoreCodes: (
|
||||
codes: number[]
|
||||
) => (this: SuperTest.Test, err: Error & { response?: SuperTest.Response }) => SuperTest.Test;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a logger that can be used with `supertest` to log details around errors
|
||||
*
|
||||
* @param log
|
||||
*
|
||||
* @example
|
||||
* const errorLogger = createSupertestErrorLogger(log);
|
||||
* supertestWithoutAuth
|
||||
* .post(`some/url`)
|
||||
* .on('error', errorLogger) //<< Add logger to `error` event
|
||||
* .send({})
|
||||
*/
|
||||
export const createSupertestErrorLogger = (log: ToolingLog): LogErrorDetailsInterface => {
|
||||
/**
|
||||
* Utility for use with `supertest` that logs errors with details returned by the API
|
||||
* @param err
|
||||
*/
|
||||
const logErrorDetails: LogErrorDetailsInterface = function (err) {
|
||||
if (err.response && (err.response.body || err.response.text)) {
|
||||
let outputData =
|
||||
'RESPONSE:\n' + err.response.body
|
||||
? JSON.stringify(err.response.body, null, 2)
|
||||
: err.response.text;
|
||||
|
||||
if (err.response.request) {
|
||||
const { url = '', method = '', _data = '' } = err.response.request;
|
||||
|
||||
outputData += `\nREQUEST:
|
||||
${method} ${url}
|
||||
${JSON.stringify(_data, null, 2)}
|
||||
`;
|
||||
}
|
||||
|
||||
log.error(outputData);
|
||||
}
|
||||
|
||||
return this ?? err;
|
||||
};
|
||||
logErrorDetails.ignoreCodes = (codes) => {
|
||||
return function (err) {
|
||||
if (err.response && err.response.status && !codes.includes(err.response.status)) {
|
||||
return logErrorDetails.call(this, err);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
};
|
||||
|
||||
return logErrorDetails;
|
||||
};
|
|
@ -16,4 +16,5 @@ export default createTestConfig('security_and_spaces', {
|
|||
publicBaseUrl: true,
|
||||
testFiles: [require.resolve('./tests')],
|
||||
useDedicatedTaskRunner: true,
|
||||
experimentalFeatures: ['sentinelOneConnectorOn'],
|
||||
});
|
||||
|
|
|
@ -16,4 +16,5 @@ export default createTestConfig('security_and_spaces', {
|
|||
publicBaseUrl: true,
|
||||
testFiles: [require.resolve('./tests')],
|
||||
useDedicatedTaskRunner: false,
|
||||
experimentalFeatures: ['sentinelOneConnectorOn'],
|
||||
});
|
||||
|
|
|
@ -133,7 +133,8 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
connectorId,
|
||||
outcome: 'failure',
|
||||
message: `action execution failure: test.system-action-kibana-privileges:${connectorId}: ${name}`,
|
||||
errorMessage: 'Unauthorized to execute actions',
|
||||
errorMessage:
|
||||
'Unauthorized to execute a "test.system-action-kibana-privileges" action',
|
||||
startDate,
|
||||
});
|
||||
break;
|
||||
|
|
|
@ -16,4 +16,5 @@ export default createTestConfig('security_and_spaces', {
|
|||
publicBaseUrl: true,
|
||||
testFiles: [require.resolve('.')],
|
||||
useDedicatedTaskRunner: true,
|
||||
experimentalFeatures: ['sentinelOneConnectorOn'],
|
||||
});
|
||||
|
|
|
@ -0,0 +1,240 @@
|
|||
/*
|
||||
* 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 {
|
||||
SENTINELONE_CONNECTOR_ID,
|
||||
SUB_ACTION,
|
||||
} from '@kbn/stack-connectors-plugin/common/sentinelone/constants';
|
||||
import { FeaturesPrivileges, Role } from '@kbn/security-plugin/common';
|
||||
import SuperTest from 'supertest';
|
||||
import expect from '@kbn/expect';
|
||||
import { getUrlPrefix } from '../../../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
||||
import { createSupertestErrorLogger } from '../../../../../common/lib/log_supertest_errors';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function createSentinelOneTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const securityService = getService('security');
|
||||
const log = getService('log');
|
||||
const logErrorDetails = createSupertestErrorLogger(log);
|
||||
|
||||
describe('SentinelOne', () => {
|
||||
describe('sub-actions authz', () => {
|
||||
interface CreatedUser {
|
||||
username: string;
|
||||
password: string;
|
||||
deleteUser: () => Promise<void>;
|
||||
}
|
||||
|
||||
// SentinelOne supported sub-actions
|
||||
const s1SubActions = [
|
||||
SUB_ACTION.KILL_PROCESS,
|
||||
SUB_ACTION.GET_AGENTS,
|
||||
SUB_ACTION.ISOLATE_HOST,
|
||||
SUB_ACTION.RELEASE_HOST,
|
||||
SUB_ACTION.GET_REMOTE_SCRIPT_STATUS,
|
||||
SUB_ACTION.GET_REMOTE_SCRIPT_RESULTS,
|
||||
];
|
||||
|
||||
let connectorId: string;
|
||||
|
||||
const createUser = async ({
|
||||
username,
|
||||
password = 'changeme',
|
||||
kibanaFeatures = { actions: ['all'] },
|
||||
}: {
|
||||
username: string;
|
||||
password?: string;
|
||||
kibanaFeatures?: FeaturesPrivileges;
|
||||
}): Promise<CreatedUser> => {
|
||||
const role: Role = {
|
||||
name: username,
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
indices: [],
|
||||
run_as: [],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
feature: {
|
||||
// Important: Saved Objects Managemnt should be set to `all` to ensure that authz
|
||||
// is not defaulted to the check done against SO's for SentinelOne
|
||||
savedObjectsManagement: ['all'],
|
||||
...kibanaFeatures,
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await securityService.role.create(role.name, {
|
||||
kibana: role.kibana,
|
||||
elasticsearch: role.elasticsearch,
|
||||
});
|
||||
|
||||
await securityService.user.create(username, {
|
||||
password: 'changeme',
|
||||
full_name: role.name,
|
||||
roles: [role.name],
|
||||
});
|
||||
|
||||
return {
|
||||
username,
|
||||
password,
|
||||
deleteUser: async () => {
|
||||
await securityService.user.delete(role.name);
|
||||
await securityService.role.delete(role.name);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
before(async () => {
|
||||
const response = await supertest
|
||||
.post(`${getUrlPrefix('default')}/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.on('error', logErrorDetails)
|
||||
.send({
|
||||
name: 'My sub connector',
|
||||
connector_type_id: SENTINELONE_CONNECTOR_ID,
|
||||
config: { url: 'https://some.non.existent.com' },
|
||||
secrets: { token: 'abc-123' },
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
connectorId = response.body.id;
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
if (connectorId) {
|
||||
await supertest
|
||||
.delete(`${getUrlPrefix('default')}/api/actions/connector/${connectorId}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(({ ok, status }) => {
|
||||
// Should cover all success codes (ex. 204 (no content), 200, etc...)
|
||||
if (!ok) {
|
||||
throw new Error(
|
||||
`Expected delete to return a status code in the 200, but got ${status}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
connectorId = '';
|
||||
}
|
||||
});
|
||||
|
||||
const executeSubAction = async ({
|
||||
subAction,
|
||||
subActionParams,
|
||||
expectedHttpCode = 200,
|
||||
username = 'elastic',
|
||||
password = 'changeme',
|
||||
errorLogger = logErrorDetails,
|
||||
}: {
|
||||
supertest: SuperTest.SuperTest<SuperTest.Test>;
|
||||
subAction: string;
|
||||
subActionParams: Record<string, unknown>;
|
||||
expectedHttpCode?: number;
|
||||
username?: string;
|
||||
password?: string;
|
||||
errorLogger?: (err: any) => void;
|
||||
}) => {
|
||||
return supertestWithoutAuth
|
||||
.post(`${getUrlPrefix('default')}/api/actions/connector/${connectorId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.on('error', errorLogger)
|
||||
.auth(username, password)
|
||||
.send({
|
||||
params: {
|
||||
subAction,
|
||||
subActionParams,
|
||||
},
|
||||
})
|
||||
.expect(expectedHttpCode);
|
||||
};
|
||||
|
||||
describe('and user has NO privileges', () => {
|
||||
let user: CreatedUser;
|
||||
|
||||
before(async () => {
|
||||
user = await createUser({
|
||||
username: 'read_access_user',
|
||||
kibanaFeatures: { actions: ['read'] },
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
if (user) {
|
||||
await user.deleteUser();
|
||||
}
|
||||
});
|
||||
|
||||
for (const s1SubAction of s1SubActions) {
|
||||
it(`should deny execute of ${s1SubAction}`, async () => {
|
||||
const execRes = await executeSubAction({
|
||||
supertest: supertestWithoutAuth,
|
||||
subAction: s1SubAction,
|
||||
subActionParams: {},
|
||||
username: user.username,
|
||||
password: user.password,
|
||||
expectedHttpCode: 403,
|
||||
errorLogger: logErrorDetails.ignoreCodes([403]),
|
||||
});
|
||||
|
||||
expect(execRes.body).to.eql({
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: 'Unauthorized to execute a ".sentinelone" action',
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('and user has proper privileges', () => {
|
||||
let user: CreatedUser;
|
||||
|
||||
before(async () => {
|
||||
user = await createUser({
|
||||
username: 'all_access_user',
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
if (user) {
|
||||
await user.deleteUser();
|
||||
// @ts-expect-error
|
||||
user = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
for (const s1SubAction of s1SubActions) {
|
||||
it(`should allow execute of ${s1SubAction}`, async () => {
|
||||
const {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
body: { status, message, connector_id },
|
||||
} = await executeSubAction({
|
||||
supertest: supertestWithoutAuth,
|
||||
subAction: s1SubAction,
|
||||
subActionParams: {},
|
||||
username: user.username,
|
||||
password: user.password,
|
||||
});
|
||||
|
||||
expect({ status, message, connector_id }).to.eql({
|
||||
status: 'error',
|
||||
message: 'an error occurred while running the action',
|
||||
connector_id: connectorId,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -41,12 +41,13 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
const { user, space } = scenario;
|
||||
describe(scenario.id, () => {
|
||||
it('should handle execute request appropriately', async () => {
|
||||
const connectorTypeId = 'test.index-record';
|
||||
const { body: createdAction } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'My action',
|
||||
connector_type_id: 'test.index-record',
|
||||
connector_type_id: connectorTypeId,
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
|
@ -78,7 +79,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(response.body).to.eql({
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: 'Unauthorized to execute actions',
|
||||
message: `Unauthorized to execute a "${connectorTypeId}" action`,
|
||||
});
|
||||
break;
|
||||
case 'global_read at space1':
|
||||
|
@ -161,7 +162,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
case 'space_1_all at space2':
|
||||
case 'space_1_all at space1':
|
||||
case 'space_1_all_with_restricted_fixture at space1':
|
||||
expect(response.statusCode).to.eql(403);
|
||||
expect(response.statusCode).to.eql(403, response.text);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
|
@ -184,12 +185,13 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should handle execute request appropriately after action is updated', async () => {
|
||||
const connectorTypeId = 'test.index-record';
|
||||
const { body: createdAction } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'My action',
|
||||
connector_type_id: 'test.index-record',
|
||||
connector_type_id: connectorTypeId,
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
|
@ -235,7 +237,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(response.body).to.eql({
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: 'Unauthorized to execute actions',
|
||||
message: `Unauthorized to execute a "${connectorTypeId}" action`,
|
||||
});
|
||||
break;
|
||||
case 'global_read at space1':
|
||||
|
@ -286,7 +288,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
case 'no_kibana_privileges at space1':
|
||||
case 'space_1_all_alerts_none_actions at space1':
|
||||
case 'space_1_all at space2':
|
||||
expect(response.statusCode).to.eql(403);
|
||||
expect(response.statusCode).to.eql(403, response.text);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
|
@ -340,12 +342,13 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should handle execute request appropriately after changing config properties', async () => {
|
||||
const connectorTypeId = '.email';
|
||||
const { body: createdAction } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'test email action',
|
||||
connector_type_id: '.email',
|
||||
connector_type_id: connectorTypeId,
|
||||
config: {
|
||||
from: 'email-from-1@example.com',
|
||||
// this host is specifically added to allowedHosts in:
|
||||
|
@ -397,7 +400,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(response.body).to.eql({
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: 'Unauthorized to execute actions',
|
||||
message: `Unauthorized to execute a "${connectorTypeId}" action`,
|
||||
});
|
||||
break;
|
||||
case 'global_read at space1':
|
||||
|
@ -416,12 +419,13 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
let indexedRecord: any;
|
||||
let searchResult: any;
|
||||
const reference = `actions-execute-3:${user.username}`;
|
||||
const connectorTypeId = 'test.authorization';
|
||||
const { body: createdAction } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'My action',
|
||||
connector_type_id: 'test.authorization',
|
||||
connector_type_id: connectorTypeId,
|
||||
})
|
||||
.expect(200);
|
||||
objectRemover.add(space.id, createdAction.id, 'action', 'actions');
|
||||
|
@ -448,7 +452,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(response.body).to.eql({
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: 'Unauthorized to execute actions',
|
||||
message: `Unauthorized to execute a "${connectorTypeId}" action`,
|
||||
});
|
||||
break;
|
||||
case 'global_read at space1':
|
||||
|
@ -528,7 +532,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
case 'global_read at space1':
|
||||
case 'space_1_all at space1':
|
||||
case 'space_1_all_with_restricted_fixture at space1':
|
||||
expect(response.statusCode).to.eql(403);
|
||||
expect(response.statusCode).to.eql(403, response.text);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
|
@ -542,7 +546,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
*/
|
||||
case 'superuser at space1':
|
||||
case 'system_actions at space1':
|
||||
expect(response.statusCode).to.eql(200);
|
||||
expect(response.statusCode).to.eql(200, response.text);
|
||||
|
||||
await validateSystemEventLog({
|
||||
spaceId: space.id,
|
||||
|
|
|
@ -32,6 +32,7 @@ export default function connectorsTests({ loadTestFile, getService }: FtrProvide
|
|||
loadTestFile(require.resolve('./connector_types/es_index_preconfigured'));
|
||||
loadTestFile(require.resolve('./connector_types/opsgenie'));
|
||||
loadTestFile(require.resolve('./connector_types/pagerduty'));
|
||||
loadTestFile(require.resolve('./connector_types/sentinelone'));
|
||||
loadTestFile(require.resolve('./connector_types/server_log'));
|
||||
loadTestFile(require.resolve('./connector_types/slack_webhook'));
|
||||
loadTestFile(require.resolve('./connector_types/slack_api'));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue