Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EDR Workflows] Workflow Insights - RBAC #205088

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ t3_analyst:
- feature_siem.actions_log_management_all # Response actions history
- feature_siem.file_operations_all
- feature_siem.scan_operations_all
- feature_siem.workflow_insights_all
- feature_securitySolutionCasesV2.all
- feature_securitySolutionAssistant.all
- feature_securitySolutionAttackDiscovery.all
Expand Down Expand Up @@ -433,6 +434,7 @@ rule_author:
- feature_siem.host_isolation_exceptions_read
- feature_siem.blocklist_all # Elastic Defend Policy Management
- feature_siem.actions_log_management_read
- feature_siem.workflow_insights_all
- feature_securitySolutionCasesV2.all
- feature_securitySolutionAssistant.all
- feature_securitySolutionAttackDiscovery.all
Expand Down Expand Up @@ -506,6 +508,7 @@ soc_manager:
- feature_siem.file_operations_all
- feature_siem.execute_operations_all
- feature_siem.scan_operations_all
- feature_siem.workflow_insights_all
- feature_securitySolutionCasesV2.all
- feature_securitySolutionAssistant.all
- feature_securitySolutionAttackDiscovery.all
Expand Down Expand Up @@ -625,6 +628,7 @@ platform_engineer:
- feature_siem.host_isolation_exceptions_all
- feature_siem.blocklist_all # Elastic Defend Policy Management
- feature_siem.actions_log_management_read
- feature_siem.workflow_insights_all
- feature_securitySolutionCasesV2.all
- feature_securitySolutionAssistant.all
- feature_securitySolutionAttackDiscovery.all
Expand Down Expand Up @@ -698,6 +702,7 @@ endpoint_operations_analyst:
- feature_siem.file_operations_all
- feature_siem.execute_operations_all
- feature_siem.scan_operations_all
- feature_siem.workflow_insights_all
- feature_securitySolutionCasesV2.all
- feature_securitySolutionAssistant.all
- feature_securitySolutionAttackDiscovery.all
Expand Down Expand Up @@ -773,6 +778,7 @@ endpoint_policy_manager:
- feature_siem.event_filters_all
- feature_siem.host_isolation_exceptions_all
- feature_siem.blocklist_all # Elastic Defend Policy Management
- feature_siem.workflow_insights_all
- feature_securitySolutionCasesV2.all
- feature_securitySolutionAssistant.all
- feature_securitySolutionAttackDiscovery.all
Expand Down
12 changes: 12 additions & 0 deletions x-pack/plugins/fleet/common/constants/authz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,18 @@ export const ENDPOINT_PRIVILEGES: Record<string, PrivilegeMapObject> = deepFreez
privilegeType: 'api',
privilegeName: 'writeScanOperations',
},
writeWorkflowInsights: {
appId: DEFAULT_APP_CATEGORIES.security.id,
privilegeSplit: '-',
privilegeType: 'api',
privilegeName: 'writeWorkflowInsights',
},
readWorkflowInsights: {
appId: DEFAULT_APP_CATEGORIES.security.id,
privilegeSplit: '-',
privilegeType: 'api',
privilegeName: 'readWorkflowInsights',
},
});

export const ENDPOINT_EXCEPTIONS_PRIVILEGES: Record<string, PrivilegeMapObject> = deepFreeze({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ export enum ProductFeatureSecurityKey {
* enables the integration assistant
*/
integrationAssistant = 'integration_assistant',

/** Enables Endpoint Workflow Insights */
securityWorkflowInsights = 'security_workflow_insights',
}

export enum ProductFeatureCasesKey {
Expand Down Expand Up @@ -137,6 +140,7 @@ export enum SecuritySubFeatureId {
eventFilters = 'eventFiltersSubFeature',
policyManagement = 'policyManagementSubFeature',
responseActionsHistory = 'responseActionsHistorySubFeature',
workflowInsights = 'workflowInsightsSubFeature',
hostIsolation = 'hostIsolationSubFeature',
processOperations = 'processOperationsSubFeature',
fileOperations = 'fileOperationsSubFeature',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,58 @@ const scanActionSubFeature = (): SubFeatureConfig => ({
],
});

const workflowInsightsSubFeature = (): SubFeatureConfig => ({
requireAllSpaces: true,
privilegesTooltip: i18n.translate(
'securitySolutionPackages.features.featureRegistry.subFeatures.workflowInsights.privilegesTooltip',
{
defaultMessage: 'All Spaces is required for Endpoint Insights access.',
}
),
name: i18n.translate(
'securitySolutionPackages.features.featureRegistry.subFeatures.workflowInsights',
{
defaultMessage: 'Endpoint Insights',
}
),
description: i18n.translate(
'securitySolutionPackages.features.featureRegistry.subFeatures.workflowInsights.description',
{
defaultMessage: 'Access the endpoint insights.',
}
),

privilegeGroups: [
{
groupType: 'mutually_exclusive',
privileges: [
{
api: [`${APP_ID}-writeWorkflowInsights`, `${APP_ID}-readWorkflowInsights`],
id: 'workflow_insights_all',
includeIn: 'none',
name: 'All',
savedObject: {
all: [],
read: [],
},
ui: ['writeWorkflowInsights', 'readWorkflowInsights'],
},
{
api: [`${APP_ID}-readWorkflowInsights`],
id: 'workflow_insights_read',
includeIn: 'none',
name: 'Read',
savedObject: {
all: [],
read: [],
},
ui: ['readWorkflowInsights'],
},
],
},
],
});

const endpointExceptionsSubFeature = (): SubFeatureConfig => ({
requireAllSpaces: true,
privilegesTooltip: i18n.translate(
Expand Down Expand Up @@ -709,6 +761,14 @@ export const getSecuritySubFeaturesMap = ({
// securitySubFeaturesList.push([SecuritySubFeatureId.featureId, featureSubFeature]);
// }

if (experimentalFeatures.defendInsights) {
// place with other All/Read/None options
securitySubFeaturesList.splice(1, 0, [
SecuritySubFeatureId.workflowInsights,
enableSpaceAwarenessIfNeeded(workflowInsightsSubFeature()),
]);
}

const securitySubFeaturesMap = new Map<SecuritySubFeatureId, SubFeatureConfig>(
securitySubFeaturesList
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ export const securityDefaultProductFeaturesConfig: DefaultSecurityProductFeature
],
},

[ProductFeatureSecurityKey.securityWorkflowInsights]: {
subFeatureIds: [SecuritySubFeatureId.workflowInsights],
},
// Product features without RBAC
// Endpoint/Osquery PLIs
[ProductFeatureSecurityKey.osqueryAutomatedResponseActions]: {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
import { serverMock } from '../../__mocks__/server';
import { isDefendInsightsEnabled, updateDefendInsightLastViewedAt } from './helpers';
import { getDefendInsightRoute } from './get_defend_insight';
import { licensingMock } from '@kbn/licensing-plugin/public/mocks';

jest.mock('./helpers');

Expand Down Expand Up @@ -73,6 +74,20 @@ describe('getDefendInsightRoute', () => {
jest.clearAllMocks();
});

it('Insufficient license', async () => {
const insufficientLicense = licensingMock.createLicense({ license: { type: 'basic' } });
const tools = requestContextMock.createTools();
tools.context.licensing.license = insufficientLicense;
jest.spyOn(insufficientLicense, 'hasAtLeast').mockReturnValue(false);

await expect(
server.inject(
getDefendInsightRequest('insight-id1'),
requestContextMock.convertContext(tools.context)
)
).rejects.toThrowError('Encountered unexpected call to response.forbidden');
});

it('should handle successful request', async () => {
const response = await server.inject(
getDefendInsightRequest('insight-id1'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const getDefendInsightRoute = (router: IRouter<ElasticAssistantRequestHan
path: DEFEND_INSIGHTS_BY_ID,
security: {
authz: {
requiredPrivileges: ['elasticAssistant'],
requiredPrivileges: ['securitySolution-readWorkflowInsights'],
},
},
})
Expand All @@ -48,7 +48,9 @@ export const getDefendInsightRoute = (router: IRouter<ElasticAssistantRequestHan
},
async (context, request, response): Promise<IKibanaResponse<DefendInsightGetResponse>> => {
const resp = buildResponse(response);
const assistantContext = await context.elasticAssistant;
const ctx = await context.resolve(['licensing', 'elasticAssistant']);

const assistantContext = ctx.elasticAssistant;
const logger: Logger = assistantContext.logger;
try {
const isEnabled = isDefendInsightsEnabled({
Expand All @@ -60,6 +62,15 @@ export const getDefendInsightRoute = (router: IRouter<ElasticAssistantRequestHan
return response.notFound();
}

if (!ctx.licensing.license.hasAtLeast('enterprise')) {
return response.forbidden({
body: {
message:
'Your license does not support Defend Workflows. Please upgrade your license.',
},
});
}

const dataClient = await assistantContext.getDefendInsightsDataClient();
const authenticatedUser = assistantContext.getCurrentUser();
if (authenticatedUser == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
import { serverMock } from '../../__mocks__/server';
import { isDefendInsightsEnabled, updateDefendInsightsLastViewedAt } from './helpers';
import { getDefendInsightsRoute } from './get_defend_insights';
import { licensingMock } from '@kbn/licensing-plugin/public/mocks';

jest.mock('./helpers');

Expand Down Expand Up @@ -73,6 +74,20 @@ describe('getDefendInsightsRoute', () => {
jest.clearAllMocks();
});

it('Insufficient license', async () => {
const insufficientLicense = licensingMock.createLicense({ license: { type: 'basic' } });
const tools = requestContextMock.createTools();
tools.context.licensing.license = insufficientLicense;
jest.spyOn(insufficientLicense, 'hasAtLeast').mockReturnValue(false);

await expect(
server.inject(
getDefendInsightsRequest({ connector_id: 'connector-id1' }),
requestContextMock.convertContext(tools.context)
)
).rejects.toThrowError('Encountered unexpected call to response.forbidden');
});

it('should handle successful request', async () => {
const response = await server.inject(
getDefendInsightsRequest({ connector_id: 'connector-id1' }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const getDefendInsightsRoute = (router: IRouter<ElasticAssistantRequestHa
path: DEFEND_INSIGHTS,
security: {
authz: {
requiredPrivileges: ['elasticAssistant'],
requiredPrivileges: ['securitySolution-readWorkflowInsights'],
},
},
})
Expand All @@ -48,8 +48,12 @@ export const getDefendInsightsRoute = (router: IRouter<ElasticAssistantRequestHa
},
async (context, request, response): Promise<IKibanaResponse<DefendInsightsGetResponse>> => {
const resp = buildResponse(response);
const assistantContext = await context.elasticAssistant;

const ctx = await context.resolve(['licensing', 'elasticAssistant']);

const assistantContext = ctx.elasticAssistant;
const logger: Logger = assistantContext.logger;

try {
const isEnabled = isDefendInsightsEnabled({
request,
Expand All @@ -60,6 +64,15 @@ export const getDefendInsightsRoute = (router: IRouter<ElasticAssistantRequestHa
return response.notFound();
}

if (!ctx.licensing.license.hasAtLeast('enterprise')) {
return response.forbidden({
body: {
message:
'Your license does not support Defend Workflows. Please upgrade your license.',
},
});
}

const dataClient = await assistantContext.getDefendInsightsDataClient();

const authenticatedUser = assistantContext.getCurrentUser();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { getDefendInsightsSearchEsMock } from '../../__mocks__/defend_insights_s
import { postDefendInsightsRequest } from '../../__mocks__/request';
import { getAssistantTool, createDefendInsight, isDefendInsightsEnabled } from './helpers';
import { postDefendInsightsRoute } from './post_defend_insights';
import { licensingMock } from '@kbn/licensing-plugin/public/mocks';

jest.mock('./helpers');

Expand Down Expand Up @@ -111,6 +112,20 @@ describe('postDefendInsightsRoute', () => {
jest.clearAllMocks();
});

it('Insufficient license', async () => {
const insufficientLicense = licensingMock.createLicense({ license: { type: 'basic' } });
const tools = requestContextMock.createTools();
tools.context.licensing.license = insufficientLicense;
jest.spyOn(insufficientLicense, 'hasAtLeast').mockReturnValue(false);

await expect(
server.inject(
postDefendInsightsRequest(mockRequestBody),
requestContextMock.convertContext(tools.context)
)
).rejects.toThrowError('Encountered unexpected call to response.forbidden');
});

it('should handle successful request', async () => {
const response = await server.inject(
postDefendInsightsRequest(mockRequestBody),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const postDefendInsightsRoute = (router: IRouter<ElasticAssistantRequestH
},
security: {
authz: {
requiredPrivileges: ['elasticAssistant'],
requiredPrivileges: ['securitySolution-writeWorkflowInsights'],
},
},
})
Expand All @@ -69,7 +69,11 @@ export const postDefendInsightsRoute = (router: IRouter<ElasticAssistantRequestH
async (context, request, response): Promise<IKibanaResponse<DefendInsightsPostResponse>> => {
const startTime = moment(); // start timing the generation
const resp = buildResponse(response);
const assistantContext = await context.elasticAssistant;

const ctx = await context.resolve(['licensing', 'elasticAssistant']);

const assistantContext = ctx.elasticAssistant;

const logger: Logger = assistantContext.logger;
const telemetry = assistantContext.telemetry;

Expand All @@ -83,6 +87,15 @@ export const postDefendInsightsRoute = (router: IRouter<ElasticAssistantRequestH
return response.notFound();
}

if (!ctx.licensing.license.hasAtLeast('enterprise')) {
return response.forbidden({
body: {
message:
'Your license does not support Defend Workflows. Please upgrade your license.',
},
});
}

const actions = assistantContext.actions;
const actionsClient = await actions.getActionsClientWithRequest(request);
const dataClient = await assistantContext.getDefendInsightsDataClient();
Expand Down
Loading