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

Merged
merged 13 commits into from
Jan 7, 2025
Merged
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.',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be in i18n?

},
});
}

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.',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And this?

},
});
}

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