[EDR Workflows] Workflow Insights - RBAC (#205088)

## Access Control for Endpoint Workflow Insights

This PR adds access control to the Endpoint Workflow Insights
functionality. Both the UI and API are gated based on the following
conditions. If these conditions are not met, the content will not
render, and direct API calls will return errors.

Access Conditions
```
1. Serverless: Requires the Endpoint Complete Tier.
2. ESS: Requires an Enterprise License.
3. User Privileges:
    3.1 Endpoint Insights Privilege must be enabled:
        3.1.1 Endpoint Insights All: Grants full access.
	3.1.2 Endpoint Insights Read:
	    3.1.2.1 Allows users to view generated insights but prevents triggering new scans.
	    3.1.2.2 With Trusted Applications privilege: Users can remediate already generated insights.
	    3.1.2.3 Without Trusted Applications privilege: No actions can be taken.
	3.1.3Endpoint Insights None: The section is not rendered.
```

Predefined serverless roles that should include endpoint insights
privilege(as defined
[here](https://github.com/elastic/security-team/issues/11460)):
- Tier 3 analyst
- Rule Author
- SOC Manager
- Endpoint Operations Analyst
- Endpoint Policy Manager
- Platform Engineer

Once this PR is merged and changes make it to canary release, [this
follow-up
PR](https://github.com/elastic/elasticsearch-controller/pull/816) should
be merged.

Note on Testing and Local Setup

To test these changes locally, the `defendInsights` assistant feature
must be enabled. You can do this by updating the following line in the
code: [Enable defendInsights
here](2ae68bdaac/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/capabilities/index.ts (L23)).

Cypress Tests

Cypress tests in this PR are currently skipped because the
`defendInsights` feature is not enabled by default. These tests should
be enabled once the feature is turned on in the main branch. Successful
run with all cypress tests enabled can be found
[here](https://buildkite.com/elastic/kibana-pull-request/builds/262774#0193f3c2-eddd-48b6-9103-fb7338304f15).

<details>
<summary>Screenshots</summary>


![396870292-b5bbedad-330f-4ef5-8281-29699cf01a98](https://github.com/user-attachments/assets/586745d4-9e8d-42b4-8d70-e32737285f5c)

![b](https://github.com/user-attachments/assets/0926b696-37ef-48e8-9dfb-d6f735033583)

![a](https://github.com/user-attachments/assets/afa799bd-7e07-4a1a-b63e-6448ae56b21a)

![c](https://github.com/user-attachments/assets/cc3a2e07-0955-4348-a954-1914c5a85e81)
![Screenshot 2024-12-23 at 13 32
57](https://github.com/user-attachments/assets/fbb28bce-eedd-4a6e-85c3-b2a07f40ab27)
</details>

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Konrad Szwarc 2025-01-07 10:33:25 +01:00 committed by GitHub
parent 40f6628c22
commit 2f61892e84
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 691 additions and 80 deletions

View file

@ -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
@ -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
@ -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
@ -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
@ -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
@ -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

View file

@ -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({

View file

@ -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 {
@ -137,6 +140,7 @@ export enum SecuritySubFeatureId {
eventFilters = 'eventFiltersSubFeature',
policyManagement = 'policyManagementSubFeature',
responseActionsHistory = 'responseActionsHistorySubFeature',
workflowInsights = 'workflowInsightsSubFeature',
hostIsolation = 'hostIsolationSubFeature',
processOperations = 'processOperationsSubFeature',
fileOperations = 'fileOperationsSubFeature',

View file

@ -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(
@ -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
);

View file

@ -121,6 +121,9 @@ export const securityDefaultProductFeaturesConfig: DefaultSecurityProductFeature
],
},
[ProductFeatureSecurityKey.securityWorkflowInsights]: {
subFeatureIds: [SecuritySubFeatureId.workflowInsights],
},
// Product features without RBAC
// Endpoint/Osquery PLIs
[ProductFeatureSecurityKey.osqueryAutomatedResponseActions]: {},

View file

@ -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');
@ -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'),

View file

@ -28,7 +28,7 @@ export const getDefendInsightRoute = (router: IRouter<ElasticAssistantRequestHan
path: DEFEND_INSIGHTS_BY_ID,
security: {
authz: {
requiredPrivileges: ['elasticAssistant'],
requiredPrivileges: ['securitySolution-readWorkflowInsights'],
},
},
})
@ -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({
@ -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) {

View file

@ -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');
@ -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' }),

View file

@ -28,7 +28,7 @@ export const getDefendInsightsRoute = (router: IRouter<ElasticAssistantRequestHa
path: DEFEND_INSIGHTS,
security: {
authz: {
requiredPrivileges: ['elasticAssistant'],
requiredPrivileges: ['securitySolution-readWorkflowInsights'],
},
},
})
@ -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,
@ -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();

View file

@ -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');
@ -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),

View file

@ -48,7 +48,7 @@ export const postDefendInsightsRoute = (router: IRouter<ElasticAssistantRequestH
},
security: {
authz: {
requiredPrivileges: ['elasticAssistant'],
requiredPrivileges: ['securitySolution-writeWorkflowInsights'],
},
},
})
@ -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;
@ -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();

View file

@ -177,6 +177,8 @@ describe('Endpoint Authz service', () => {
['canReadBlocklist', 'readBlocklist'],
['canWriteEventFilters', 'writeEventFilters'],
['canReadEventFilters', 'readEventFilters'],
['canReadWorkflowInsights', 'readWorkflowInsights'],
['canWriteWorkflowInsights', 'writeWorkflowInsights'],
])('%s should be true if `packagePrivilege.%s` is `true`', (auth) => {
const authz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles);
expect(authz[auth]).toBe(true);
@ -216,6 +218,8 @@ describe('Endpoint Authz service', () => {
['canReadBlocklist', ['readBlocklist']],
['canWriteEventFilters', ['writeEventFilters']],
['canReadEventFilters', ['readEventFilters']],
['canWriteWorkflowInsights', ['writeWorkflowInsights']],
['canReadWorkflowInsights', ['readWorkflowInsights']],
// all dependent privileges are false and so it should be false
['canAccessResponseConsole', responseConsolePrivileges],
])('%s should be false if `packagePrivilege.%s` is `false`', (auth, privileges) => {
@ -265,6 +269,8 @@ describe('Endpoint Authz service', () => {
['canReadBlocklist', ['readBlocklist']],
['canWriteEventFilters', ['writeEventFilters']],
['canReadEventFilters', ['readEventFilters']],
['canWriteWorkflowInsights', ['writeWorkflowInsights']],
['canReadWorkflowInsights', ['readWorkflowInsights']],
// all dependent privileges are false and so it should be false
['canAccessResponseConsole', responseConsolePrivileges],
])(
@ -334,7 +340,9 @@ describe('Endpoint Authz service', () => {
canWriteScanOperations: false,
canWriteFileOperations: false,
canWriteTrustedApplications: false,
canWriteWorkflowInsights: false,
canReadTrustedApplications: false,
canReadWorkflowInsights: false,
canWriteHostIsolationExceptions: false,
canAccessHostIsolationExceptions: false,
canReadHostIsolationExceptions: false,

View file

@ -97,6 +97,9 @@ export const calculateEndpointAuthz = (
const canReadEndpointExceptions = hasAuth('showEndpointExceptions');
const canWriteEndpointExceptions = hasAuth('crudEndpointExceptions');
const canReadWorkflowInsights = hasAuth('readWorkflowInsights');
const canWriteWorkflowInsights = hasAuth('writeWorkflowInsights');
const authz: EndpointAuthz = {
canWriteSecuritySolution,
canReadSecuritySolution,
@ -122,6 +125,8 @@ export const calculateEndpointAuthz = (
canWriteActionsLogManagement,
canReadActionsLogManagement: canReadActionsLogManagement && isEnterpriseLicense,
canAccessEndpointActionsLogManagement: canReadActionsLogManagement && isPlatinumPlusLicense,
canReadWorkflowInsights: canReadWorkflowInsights && isEnterpriseLicense,
canWriteWorkflowInsights: canWriteWorkflowInsights && isEnterpriseLicense,
// ---------------------------------------------------------
// Response Actions
@ -207,6 +212,8 @@ export const getEndpointAuthzInitialState = (): EndpointAuthz => {
canReadEventFilters: false,
canReadEndpointExceptions: false,
canWriteEndpointExceptions: false,
canReadWorkflowInsights: false,
canWriteWorkflowInsights: false,
};
};

View file

@ -93,6 +93,10 @@ export interface EndpointAuthz {
canReadEndpointExceptions: boolean;
/** if the user has read permissions for endpoint exceptions */
canWriteEndpointExceptions: boolean;
/** if the user has write permissions for workflow insights */
canWriteWorkflowInsights: boolean;
/** if the user has read permissions for workflow insights */
canReadWorkflowInsights: boolean;
}
export type EndpointAuthzKeyList = Array<keyof EndpointAuthz>;

View file

@ -15,54 +15,69 @@ import {
import { closeAllToasts } from '../../tasks/toasts';
import { login, ROLE } from '../../tasks/login';
describe('When defining a kibana role for Endpoint security access', { tags: '@ess' }, () => {
const getAllSubFeatureRows = (): Cypress.Chainable<JQuery<HTMLElement>> => {
return cy
.get('#featurePrivilegeControls_siem')
.findByTestSubj('mutexSubFeaturePrivilegeControl')
.closest('.euiFlexGroup');
};
// Unskip when defendInsights assistant feature is enabled by default
describe.skip(
'When defining a kibana role for Endpoint security access',
{
env: {
ftrConfig: {
kbnServerArgs: [
`--xpack.securitySolution.enableExperimental=${JSON.stringify(['defendInsights'])}`,
],
},
},
tags: '@ess',
},
() => {
const getAllSubFeatureRows = (): Cypress.Chainable<JQuery<HTMLElement>> => {
return cy
.get('#featurePrivilegeControls_siem')
.findByTestSubj('mutexSubFeaturePrivilegeControl')
.closest('.euiFlexGroup');
};
beforeEach(() => {
login(ROLE.system_indices_superuser);
navigateToRolePage();
closeAllToasts();
beforeEach(() => {
login(ROLE.system_indices_superuser);
navigateToRolePage();
closeAllToasts();
openKibanaFeaturePrivilegesFlyout();
setKibanaPrivilegeSpace('default');
expandSecuritySolutionCategoryKibanaPrivileges();
expandEndpointSecurityFeaturePrivileges();
});
openKibanaFeaturePrivilegesFlyout();
setKibanaPrivilegeSpace('default');
expandSecuritySolutionCategoryKibanaPrivileges();
expandEndpointSecurityFeaturePrivileges();
});
it('should display RBAC entries with expected controls', () => {
getAllSubFeatureRows()
.then(($subFeatures) => {
const featureRows: string[] = [];
$subFeatures.each((_, $subFeature) => {
featureRows.push($subFeature.textContent ?? '');
});
it('should display RBAC entries with expected controls', () => {
getAllSubFeatureRows()
.then(($subFeatures) => {
const featureRows: string[] = [];
$subFeatures.each((_, $subFeature) => {
featureRows.push($subFeature.textContent ?? '');
});
return featureRows;
})
.should('deep.equal', [
'Endpoint List Displays all hosts running Elastic Defend and their relevant integration details.Endpoint List sub-feature privilegeAllReadNone',
'Trusted Applications Helps mitigate conflicts with other software, usually other antivirus or endpoint security applications.Trusted Applications sub-feature privilegeAllReadNone',
'Host Isolation Exceptions Add specific IP addresses that isolated hosts are still allowed to communicate with, even when isolated from the rest of the network.Host Isolation Exceptions sub-feature privilegeAllReadNone',
'Blocklist Extend Elastic Defends protection against malicious processes and protect against potentially harmful applications.Blocklist sub-feature privilegeAllReadNone',
'Event Filters Filter out endpoint events that you do not need or want stored in Elasticsearch.Event Filters sub-feature privilegeAllReadNone',
'Elastic Defend Policy Management Access the Elastic Defend integration policy to configure protections, event collection, and advanced policy features.Elastic Defend Policy Management sub-feature privilegeAllReadNone',
'Response Actions History Access the history of response actions performed on endpoints.Response Actions History sub-feature privilegeAllReadNone',
'Host Isolation Perform the "isolate" and "release" response actions.Host Isolation sub-feature privilegeAllNone',
'Process Operations Perform process-related response actions in the response console.Process Operations sub-feature privilegeAllNone',
'File Operations Perform file-related response actions in the response console.File Operations sub-feature privilegeAllNone',
'Execute Operations Perform script execution response actions in the response console.Execute Operations sub-feature privilegeAllNone',
'Scan Operations Perform folder scan response actions in the response console.Scan Operations sub-feature privilegeAllNone',
]);
});
return featureRows;
})
.should('deep.equal', [
'Endpoint List Displays all hosts running Elastic Defend and their relevant integration details.Endpoint List sub-feature privilegeAllReadNone',
'Endpoint Insights Access the endpoint insights.Endpoint Insights sub-feature privilegeAllReadNone',
'Trusted Applications Helps mitigate conflicts with other software, usually other antivirus or endpoint security applications.Trusted Applications sub-feature privilegeAllReadNone',
'Host Isolation Exceptions Add specific IP addresses that isolated hosts are still allowed to communicate with, even when isolated from the rest of the network.Host Isolation Exceptions sub-feature privilegeAllReadNone',
'Blocklist Extend Elastic Defends protection against malicious processes and protect against potentially harmful applications.Blocklist sub-feature privilegeAllReadNone',
'Event Filters Filter out endpoint events that you do not need or want stored in Elasticsearch.Event Filters sub-feature privilegeAllReadNone',
'Elastic Defend Policy Management Access the Elastic Defend integration policy to configure protections, event collection, and advanced policy features.Elastic Defend Policy Management sub-feature privilegeAllReadNone',
'Response Actions History Access the history of response actions performed on endpoints.Response Actions History sub-feature privilegeAllReadNone',
'Host Isolation Perform the "isolate" and "release" response actions.Host Isolation sub-feature privilegeAllNone',
'Process Operations Perform process-related response actions in the response console.Process Operations sub-feature privilegeAllNone',
'File Operations Perform file-related response actions in the response console.File Operations sub-feature privilegeAllNone',
'Execute Operations Perform script execution response actions in the response console.Execute Operations sub-feature privilegeAllNone',
'Scan Operations Perform folder scan response actions in the response console.Scan Operations sub-feature privilegeAllNone',
]);
});
it('should display all RBAC entries set to None by default', () => {
getAllSubFeatureRows()
.findByTestSubj('none')
.should('have.class', 'euiButtonGroupButton-isSelected');
});
});
it('should display all RBAC entries set to None by default', () => {
getAllSubFeatureRows()
.findByTestSubj('none')
.should('have.class', 'euiButtonGroupButton-isSelected');
});
}
);

View file

@ -0,0 +1,47 @@
/*
* 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 { fetchRunningDefendInsights, fetchWorkflowInsights } from '../../../../tasks/insights';
import { login, ROLE } from '../../../../tasks/login';
// Unskip when defendInsights assistant feature is enabled by default
describe.skip(
'Workflow Insights APIs',
{
tags: ['@serverless', '@skipInServerlessMKI'], // remove @skipInServerlessMKI once changes are merged
env: {
ftrConfig: {
kbnServerArgs: [
`--xpack.securitySolution.enableExperimental=${JSON.stringify(['defendInsights'])}`,
],
productTypes: [
{ product_line: 'security', product_tier: 'complete' },
{ product_line: 'endpoint', product_tier: 'complete' },
],
},
},
},
() => {
beforeEach(() => {
login(ROLE.system_indices_superuser);
});
describe('/workflow_insights', () => {
it('GET should allow access to workflow insights api endpoint', () => {
fetchWorkflowInsights().then((response) => {
expect(response.status).to.equal(200);
});
});
});
describe('/defend_insights', () => {
it('GET should allow access to defend insights api endpoint', () => {
fetchRunningDefendInsights().then((response) => {
expect(response.status).to.equal(200);
});
});
});
}
);

View file

@ -0,0 +1,62 @@
/*
* 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 {
fetchRunningDefendInsights,
fetchWorkflowInsights,
triggerRunningDefendInsights,
updateWorkflowInsights,
} from '../../../../tasks/insights';
import { login, ROLE } from '../../../../tasks/login';
// Unskip when defendInsights assistant feature is enabled by default
describe.skip(
'Workflow Insights APIs',
{
tags: ['@serverless', '@skipInServerlessMKI'], // remove @skipInServerlessMKI once changes are merged
env: {
ftrConfig: {
kbnServerArgs: [
`--xpack.securitySolution.enableExperimental=${JSON.stringify(['defendInsights'])}`,
],
productTypes: [
{ product_line: 'security', product_tier: 'essentials' },
{ product_line: 'endpoint', product_tier: 'essentials' },
],
},
},
},
() => {
beforeEach(() => {
login(ROLE.system_indices_superuser);
});
describe('/workflow_insights', () => {
it('GET should NOT allow access to workflow insights api endpoint', () => {
fetchWorkflowInsights({ failOnStatusCode: false }).then((response) => {
expect(response.status).to.equal(403);
});
});
it('UPDATE should NOT allow access to workflow insights api endpoint', () => {
updateWorkflowInsights().then((response) => {
expect(response.status).to.equal(403);
});
});
});
describe('/defend_insights', () => {
it('GET should NOT allow access to defend insights api endpoint', () => {
fetchRunningDefendInsights().then((response) => {
expect(response.status).to.equal(404);
});
});
it('POST should NOT allow access to defend insights api endpoint', () => {
triggerRunningDefendInsights().then((response) => {
expect(response.status).to.equal(404);
});
});
});
}
);

View file

@ -0,0 +1,64 @@
/*
* 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 { loadEndpointDetailsFlyout, workflowInsightsSelectors } from '../../../../screens/insights';
import type { CyIndexEndpointHosts } from '../../../../tasks/index_endpoint_hosts';
import { indexEndpointHosts } from '../../../../tasks/index_endpoint_hosts';
import { login, ROLE } from '../../../../tasks/login';
const { insightsComponentExists, addConnectorButtonExists } = workflowInsightsSelectors;
// Unskip when defendInsights assistant feature is enabled by default
describe.skip(
'Endpoint details',
{
tags: [
'@serverless',
// skipped on MKI since feature flags are not supported there
'@skipInServerlessMKI',
],
env: {
ftrConfig: {
kbnServerArgs: [
`--xpack.securitySolution.enableExperimental=${JSON.stringify(['defendInsights'])}`,
],
productTypes: [
{ product_line: 'security', product_tier: 'complete' },
{ product_line: 'endpoint', product_tier: 'complete' },
],
},
},
},
() => {
let loadedEndpoint: CyIndexEndpointHosts;
let endpointId: string;
// Since the endpoint is used only for displaying details flyout, we can use the same endpoint for all tests
before(() => {
indexEndpointHosts({ count: 1 }).then((indexedEndpoint) => {
loadedEndpoint = indexedEndpoint;
endpointId = indexedEndpoint.data.hosts[0].agent.id;
});
});
after(() => {
if (loadedEndpoint) {
loadedEndpoint.cleanup();
}
});
beforeEach(() => {
login(ROLE.system_indices_superuser);
});
it('should render Insights section on endpoint flyout with option to define connectors', () => {
loadEndpointDetailsFlyout(endpointId);
insightsComponentExists();
addConnectorButtonExists();
});
}
);

View file

@ -0,0 +1,63 @@
/*
* 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 { loadEndpointDetailsFlyout, workflowInsightsSelectors } from '../../../../screens/insights';
import type { CyIndexEndpointHosts } from '../../../../tasks/index_endpoint_hosts';
import { indexEndpointHosts } from '../../../../tasks/index_endpoint_hosts';
import { login, ROLE } from '../../../../tasks/login';
const { insightsComponentDoesntExist } = workflowInsightsSelectors;
// Unskip when defendInsights assistant feature is enabled by default
describe.skip(
'Endpoint details',
{
tags: [
'@serverless',
// skipped on MKI since feature flags are not supported there
'@skipInServerlessMKI',
],
env: {
ftrConfig: {
kbnServerArgs: [
`--xpack.securitySolution.enableExperimental=${JSON.stringify(['defendInsights'])}`,
],
productTypes: [
{ product_line: 'security', product_tier: 'essentials' },
{ product_line: 'endpoint', product_tier: 'essentials' },
],
},
},
},
() => {
let loadedEndpoint: CyIndexEndpointHosts;
let endpointId: string;
// Since the endpoint is used only for displaying details flyout, we can use the same endpoint for all tests
before(() => {
indexEndpointHosts({ count: 1 }).then((indexedEndpoint) => {
loadedEndpoint = indexedEndpoint;
endpointId = indexedEndpoint.data.hosts[0].agent.id;
});
});
after(() => {
if (loadedEndpoint) {
loadedEndpoint.cleanup();
}
});
beforeEach(() => {
login(ROLE.system_indices_superuser);
});
it('should render Insights section on endpoint flyout with option to define connectors', () => {
loadEndpointDetailsFlyout(endpointId);
insightsComponentDoesntExist();
});
}
);

View file

@ -0,0 +1,20 @@
/*
* 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 { loadPage } from '../tasks/common';
export const loadEndpointDetailsFlyout = (endpointId: string) =>
loadPage(
`/app/security/administration/endpoints?page_index=0&page_size=10&selected_endpoint=${endpointId}&show=details`
);
export const workflowInsightsSelectors = {
insightsComponentExists: () => cy.getByTestSubj('endpointDetailsInsightsWrapper').should('exist'),
insightsComponentDoesntExist: () =>
cy.getByTestSubj('endpointDetailsInsightsWrapper').should('not.exist'),
addConnectorButtonExists: () => cy.getByTestSubj('addNewConnectorButton').should('exist'),
};

View file

@ -0,0 +1,74 @@
/*
* 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 { DEFEND_INSIGHTS } from '@kbn/elastic-assistant-common';
import { ActionType } from '../../../../common/endpoint/types/workflow_insights';
import { request } from './common';
import {
WORKFLOW_INSIGHTS_ROUTE,
WORKFLOW_INSIGHTS_UPDATE_ROUTE,
} from '../../../../common/endpoint/constants';
export const triggerRunningDefendInsights = () => {
return request({
method: 'POST',
url: DEFEND_INSIGHTS,
body: JSON.stringify({
endpointIds: ['test'],
insightType: 'incompatible_antivirus',
anonymizationFields: [],
replacements: {},
subAction: 'invokeAI',
apiConfig: {
connectorId: 'test',
actionTypeId: 'test',
},
}),
headers: { 'Elastic-Api-Version': '1' },
failOnStatusCode: false,
});
};
export const fetchRunningDefendInsights = () => {
return request({
method: 'GET',
url: DEFEND_INSIGHTS,
qs: {
status: 'running',
endpoint_ids: 'test',
},
headers: { 'Elastic-Api-Version': '1' },
failOnStatusCode: false,
});
};
export const fetchWorkflowInsights = (overrides?: Record<string, unknown>) => {
return request({
method: 'GET',
url: WORKFLOW_INSIGHTS_ROUTE,
qs: {
actionTypes: JSON.stringify([ActionType.Refreshed]),
targetIds: JSON.stringify(['test']),
},
headers: { 'Elastic-Api-Version': '1' },
...(overrides ?? {}),
});
};
export const updateWorkflowInsights = () => {
return request({
method: 'PUT',
url: WORKFLOW_INSIGHTS_UPDATE_ROUTE.replace('{insightId}', 'test'),
body: JSON.stringify({
action: {
type: ActionType.Remediated,
},
}),
headers: { 'Elastic-Api-Version': '1' },
failOnStatusCode: false,
});
};

View file

@ -35,5 +35,6 @@
"@kbn/dev-utils",
"@kbn/spaces-plugin",
"@kbn/test-suites-xpack/security_solution_cypress/cypress",
"@kbn/elastic-assistant-common",
]
}

View file

@ -87,6 +87,7 @@ export const WorkflowInsights = React.memo(({ endpointId }: WorkflowInsightsProp
return (
<>
<EuiAccordion
data-test-subj={'endpointDetailsInsightsWrapper'}
id={'workflow-insights-wrapper'}
buttonContent={
<EuiText size={'m'}>

View file

@ -16,8 +16,10 @@ import {
EuiPanel,
EuiSpacer,
EuiText,
EuiToolTip,
} from '@elastic/eui';
import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants';
import { useUserPrivileges } from '../../../../../../../common/components/user_privileges';
import { WORKFLOW_INSIGHTS } from '../../../translations';
interface WorkflowInsightsResultsProps {
@ -51,6 +53,7 @@ export const WorkflowInsightsResults = ({
const {
application: { navigateToUrl },
} = useKibana().services;
const { canWriteTrustedApplications } = useUserPrivileges().endpointPrivileges;
useEffect(() => {
setShowEmptyResultsCallout(results?.length === 0 && scanCompleted);
@ -106,6 +109,9 @@ export const WorkflowInsightsResults = ({
} else if (results?.length) {
return results.flatMap((insight, index) => {
return (insight.remediation.exception_list_items ?? []).map((item) => {
const { ariaLabel, tooltipContent, tooltipNoPermissions } =
WORKFLOW_INSIGHTS.issues.remediationButton;
return (
<EuiPanel paddingSize="m" hasShadow={false} hasBorder key={index}>
<EuiFlexGroup alignItems={'center'} gutterSize={'m'}>
@ -128,17 +134,23 @@ export const WorkflowInsightsResults = ({
</EuiFlexItem>
<EuiFlexItem grow={false} style={{ marginLeft: 'auto' }}>
<EuiButtonIcon
aria-label={WORKFLOW_INSIGHTS.issues.insightRemediationButtonAriaLabel}
iconType="popout"
href={`${APP_PATH}${TRUSTED_APPS_PATH}?show=create`}
onClick={(e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
if (insight.id) {
openArtifactCreationPage({ remediation: item, id: insight.id });
}
}}
/>
<EuiToolTip
content={canWriteTrustedApplications ? tooltipContent : tooltipNoPermissions}
position={'top'}
>
<EuiButtonIcon
isDisabled={!canWriteTrustedApplications}
aria-label={ariaLabel}
iconType="popout"
href={`${APP_PATH}${TRUSTED_APPS_PATH}?show=create`}
onClick={(e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
if (insight.id) {
openArtifactCreationPage({ remediation: item, id: insight.id });
}
}}
/>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
@ -147,7 +159,7 @@ export const WorkflowInsightsResults = ({
});
}
return null;
}, [openArtifactCreationPage, results, showEmptyResultsCallout]);
}, [canWriteTrustedApplications, openArtifactCreationPage, results, showEmptyResultsCallout]);
return (
<>

View file

@ -6,7 +6,7 @@
*/
import React, { useCallback, useMemo } from 'react';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText, EuiToolTip } from '@elastic/eui';
import {
DEFEND_INSIGHTS_STORAGE_KEY,
ConnectorSelectorInline,
@ -17,6 +17,7 @@ import { noop } from 'lodash/fp';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import { some } from 'lodash';
import { AssistantBeacon } from '@kbn/ai-assistant-icon';
import { useUserPrivileges } from '../../../../../../../common/components/user_privileges';
import { useSpaceId } from '../../../../../../../common/hooks/use_space_id';
import { WORKFLOW_INSIGHTS } from '../../../translations';
import { useKibana } from '../../../../../../../common/lib/kibana';
@ -43,6 +44,7 @@ export const WorkflowInsightsScanSection = ({
const { data: aiConnectors } = useLoadConnectors({
http,
});
const { canWriteWorkflowInsights } = useUserPrivileges().endpointPrivileges;
// Store the selected connector id in local storage so that it persists across page reloads
const [localStorageWorkflowInsightsConnectorId, setLocalStorageWorkflowInsightsConnectorId] =
@ -78,11 +80,13 @@ export const WorkflowInsightsScanSection = ({
if (!connectorExists) {
return null;
}
return (
const button = (
<EuiFlexItem grow={false}>
<EuiButton
size="s"
isLoading={isScanButtonDisabled}
isDisabled={!canWriteWorkflowInsights}
onClick={() => {
if (!connectorId || !selectedConnectorActionTypeId) return;
onScanButtonClick({ connectorId, actionTypeId: selectedConnectorActionTypeId });
@ -92,7 +96,17 @@ export const WorkflowInsightsScanSection = ({
</EuiButton>
</EuiFlexItem>
);
if (!canWriteWorkflowInsights) {
return (
<EuiToolTip content={WORKFLOW_INSIGHTS.scan.noPermissions} position={'top'}>
{button}
</EuiToolTip>
);
}
return button;
}, [
canWriteWorkflowInsights,
connectorExists,
connectorId,
isScanButtonDisabled,

View file

@ -24,6 +24,7 @@ import { useEndpointSelector } from '../hooks';
import { nonExistingPolicies, uiQueryParams } from '../../store/selectors';
import { POLICY_STATUS_TO_BADGE_COLOR } from '../host_constants';
import { FormattedDate } from '../../../../../common/components/formatted_date';
import { useUserPrivileges } from '../../../../../common/components/user_privileges';
import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
import { getEndpointDetailsPath } from '../../../../common/routing';
import { EndpointPolicyLink } from '../../../../components/endpoint_policy_link';
@ -43,7 +44,16 @@ interface EndpointDetailsContentProps {
export const EndpointDetailsContent = memo<EndpointDetailsContentProps>(
({ hostInfo, policyInfo }) => {
const isWorkflowInsightsEnabled = useIsExperimentalFeatureEnabled('defendInsights');
// Access control
const isWorkflowInsightsFeatureFlagEnabled = useIsExperimentalFeatureEnabled('defendInsights');
const { canReadWorkflowInsights } = useUserPrivileges().endpointPrivileges;
const canAccessWorkflowInsights = useMemo(() => {
if (!isWorkflowInsightsFeatureFlagEnabled) {
return false;
}
return canReadWorkflowInsights;
}, [canReadWorkflowInsights, isWorkflowInsightsFeatureFlagEnabled]);
const queryParams = useEndpointSelector(uiQueryParams);
const policyStatus = useMemo(
() => hostInfo.metadata.Endpoint.policy.applied.status,
@ -185,7 +195,7 @@ export const EndpointDetailsContent = memo<EndpointDetailsContentProps>(
}, [hostInfo, policyInfo, missingPolicies, policyStatus, policyStatusClickHandler]);
return (
<div>
{isWorkflowInsightsEnabled && <WorkflowInsights endpointId={hostInfo.metadata.agent.id} />}
{canAccessWorkflowInsights && <WorkflowInsights endpointId={hostInfo.metadata.agent.id} />}
<EuiDescriptionList
columnWidths={[1, 3]}
compressed

View file

@ -34,6 +34,12 @@ export const WORKFLOW_INSIGHTS = {
defaultMessage: 'Loading...',
}
),
noPermissions: i18n.translate(
'xpack.securitySolution.endpointDetails.workflowInsights.scan.noPermissions',
{
defaultMessage: 'You do not have the privileges required to perform this operation.',
}
),
},
issues: {
title: i18n.translate('xpack.securitySolution.endpointDetails.workflowInsights.issues.title', {
@ -45,12 +51,26 @@ export const WORKFLOW_INSIGHTS = {
defaultMessage: 'No issues had been found',
}
),
insightRemediationButtonAriaLabel: i18n.translate(
'xpack.securitySolution.endpointDetails.workflowInsights.issues.insightRemediationButtonAriaLabel',
{
defaultMessage: 'Create trusted app',
}
),
remediationButton: {
ariaLabel: i18n.translate(
'xpack.securitySolution.endpointDetails.workflowInsights.issues.insightRemediationButtonAriaLabel',
{
defaultMessage: 'Create trusted app',
}
),
tooltipContent: i18n.translate(
'xpack.securitySolution.endpointDetails.workflowInsights.issues.insightRemediationButtonTooltipContent',
{
defaultMessage: 'Create trusted app',
}
),
tooltipNoPermissions: i18n.translate(
'xpack.securitySolution.endpointDetails.workflowInsights.issues.insighRemediationButtonTooltipNoPermissions',
{
defaultMessage: 'You do not have the privileges required to perform this operation.',
}
),
},
},
toasts: {
scanError: i18n.translate(

View file

@ -72,6 +72,7 @@ export const getEndpointOperationsAnalyst: () => Omit<Role, 'name'> = () => {
'file_operations_all',
'execute_operations_all',
'scan_operations_all',
'workflow_insights_all',
],
},
spaces: ['*'],

View file

@ -26,6 +26,8 @@ export const getEndpointSecurityPolicyManager: () => Omit<Role, 'name'> = () =>
'event_filters_all',
'host_isolation_exceptions_all',
'blocklist_all',
'workflow_insights_all',
],
},
},

View file

@ -28,6 +28,8 @@ export const getPlatformEngineer: () => Omit<Role, 'name'> = () => {
'blocklist_all',
'actions_log_management_read',
'workflow_insights_all',
],
},
},

View file

@ -28,6 +28,7 @@ export const getRuleAuthor: () => Omit<Role, 'name'> = () => {
'host_isolation_exceptions_read',
'blocklist_all',
'actions_log_management_read',
'workflow_insights_all',
],
},
},

View file

@ -321,6 +321,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_securitySolutionCases.all
- feature_securitySolutionAssistant.all
- feature_securitySolutionAttackDiscovery.all
@ -448,6 +449,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_securitySolutionCases.all
- feature_securitySolutionAssistant.all
- feature_securitySolutionAttackDiscovery.all
@ -522,6 +524,7 @@ soc_manager:
- feature_siem.file_operations_all
- feature_siem.execute_operations_all
- feature_siem.scan_operations_all
- feature_siem.workflow_insights_all
- feature_securitySolutionCases.all
- feature_securitySolutionAssistant.all
- feature_securitySolutionAttackDiscovery.all
@ -643,6 +646,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_securitySolutionCases.all
- feature_securitySolutionAssistant.all
- feature_securitySolutionAttackDiscovery.all
@ -717,6 +721,7 @@ endpoint_operations_analyst:
- feature_siem.file_operations_all
- feature_siem.execute_operations_all # Execute
- feature_siem.scan_operations_all
- feature_siem.workflow_insights_all
- feature_securitySolutionCases.all
- feature_securitySolutionAssistant.all
- feature_securitySolutionAttackDiscovery.all
@ -785,6 +790,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_securitySolutionCases.all
- feature_securitySolutionAssistant.all
- feature_securitySolutionAttackDiscovery.all

View file

@ -30,6 +30,8 @@ export const getSocManager: () => Omit<Role, 'name'> = () => {
'host_isolation_all',
'process_operations_all',
'actions_log_management_all',
'workflow_insights_all',
],
},
},

View file

@ -32,6 +32,7 @@ export const getT3Analyst: () => Omit<Role, 'name'> = () => {
'actions_log_management_all',
'file_operations_all',
'scan_operations_all',
'workflow_insights_all',
],
},
},

View file

@ -31,7 +31,7 @@ describe('Get Insights Route Handler', () => {
registerGetInsightsRoute(router, mockEndpointContext);
callRoute = async (params, authz = { canReadSecuritySolution: true }) => {
callRoute = async (params, authz = { canReadWorkflowInsights: true }) => {
const mockContext = {
core: {
security: {
@ -96,7 +96,7 @@ describe('Get Insights Route Handler', () => {
describe('with invalid privileges', () => {
it('should return forbidden if user lacks read privileges', async () => {
await callRoute({}, { canReadSecuritySolution: false });
await callRoute({}, { canReadWorkflowInsights: false });
expect(mockResponse.forbidden).toHaveBeenCalled();
});

View file

@ -45,7 +45,7 @@ export const registerGetInsightsRoute = (
},
},
withEndpointAuthz(
{ all: ['canReadSecuritySolution'] },
{ all: ['canReadWorkflowInsights'] },
endpointContext.logFactory.get('workflowInsights'),
getInsightsRouteHandler(endpointContext)
)

View file

@ -35,7 +35,7 @@ describe('Update Insights Route Handler', () => {
registerUpdateInsightsRoute(router, mockEndpointContext);
callRoute = async (params, body, authz = { canReadSecuritySolution: true }) => {
callRoute = async (params, body, authz = { canWriteWorkflowInsights: true }) => {
const mockContext = {
core: {
security: {
@ -102,7 +102,7 @@ describe('Update Insights Route Handler', () => {
await callRoute(
{ insightId: '1' },
{ name: 'Updated Insight' },
{ canReadSecuritySolution: false }
{ canWriteWorkflowInsights: false }
);
expect(mockResponse.forbidden).toHaveBeenCalled();

View file

@ -46,7 +46,7 @@ export const registerUpdateInsightsRoute = (
},
},
withEndpointAuthz(
{ all: ['canReadSecuritySolution'] },
{ all: ['canWriteWorkflowInsights'] },
endpointContext.logFactory.get('workflowInsights'),
updateInsightsRouteHandler(endpointContext)
)

View file

@ -44,6 +44,7 @@ export const PLI_PRODUCT_FEATURES: PliProductFeatures = {
ProductFeatureKey.endpointAgentTamperProtection,
ProductFeatureKey.endpointCustomNotification,
ProductFeatureKey.endpointProtectionUpdates,
ProductFeatureKey.securityWorkflowInsights,
],
},
cloud: {

View file

@ -305,6 +305,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_securitySolutionCases.all
- feature_securitySolutionAssistant.all
- feature_securitySolutionAttackDiscovery.all
@ -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_securitySolutionCases.all
- feature_securitySolutionAssistant.all
- feature_securitySolutionAttackDiscovery.all
@ -509,6 +511,7 @@ soc_manager:
- feature_siem.file_operations_all
- feature_siem.execute_operations_all
- feature_siem.scan_operations_all
- feature_siem.workflow_insights_all
- feature_securitySolutionCases.all
- feature_observabilityCases.all
- feature_securitySolutionAssistant.all
@ -631,6 +634,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_securitySolutionCases.all
- feature_securitySolutionAssistant.all
- feature_securitySolutionAttackDiscovery.all
@ -705,6 +709,7 @@ endpoint_operations_analyst:
- feature_siem.file_operations_all
- feature_siem.execute_operations_all # Execute
- feature_siem.scan_operations_all
- feature_siem.workflow_insights_all
- feature_securitySolutionCases.all
- feature_securitySolutionAssistant.all
- feature_securitySolutionAttackDiscovery.all
@ -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_securitySolutionCases.all
- feature_securitySolutionAssistant.all
- feature_securitySolutionAttackDiscovery.all