mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[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>





</details>
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
40f6628c22
commit
2f61892e84
40 changed files with 691 additions and 80 deletions
|
@ -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
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -121,6 +121,9 @@ export const securityDefaultProductFeaturesConfig: DefaultSecurityProductFeature
|
|||
],
|
||||
},
|
||||
|
||||
[ProductFeatureSecurityKey.securityWorkflowInsights]: {
|
||||
subFeatureIds: [SecuritySubFeatureId.workflowInsights],
|
||||
},
|
||||
// Product features without RBAC
|
||||
// Endpoint/Osquery PLIs
|
||||
[ProductFeatureSecurityKey.osqueryAutomatedResponseActions]: {},
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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' }),
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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 Defend’s 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 Defend’s 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');
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
);
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
);
|
|
@ -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'),
|
||||
};
|
|
@ -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,
|
||||
});
|
||||
};
|
|
@ -35,5 +35,6 @@
|
|||
"@kbn/dev-utils",
|
||||
"@kbn/spaces-plugin",
|
||||
"@kbn/test-suites-xpack/security_solution_cypress/cypress",
|
||||
"@kbn/elastic-assistant-common",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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'}>
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -72,6 +72,7 @@ export const getEndpointOperationsAnalyst: () => Omit<Role, 'name'> = () => {
|
|||
'file_operations_all',
|
||||
'execute_operations_all',
|
||||
'scan_operations_all',
|
||||
'workflow_insights_all',
|
||||
],
|
||||
},
|
||||
spaces: ['*'],
|
||||
|
|
|
@ -26,6 +26,8 @@ export const getEndpointSecurityPolicyManager: () => Omit<Role, 'name'> = () =>
|
|||
'event_filters_all',
|
||||
'host_isolation_exceptions_all',
|
||||
'blocklist_all',
|
||||
|
||||
'workflow_insights_all',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -28,6 +28,8 @@ export const getPlatformEngineer: () => Omit<Role, 'name'> = () => {
|
|||
'blocklist_all',
|
||||
|
||||
'actions_log_management_read',
|
||||
|
||||
'workflow_insights_all',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -28,6 +28,7 @@ export const getRuleAuthor: () => Omit<Role, 'name'> = () => {
|
|||
'host_isolation_exceptions_read',
|
||||
'blocklist_all',
|
||||
'actions_log_management_read',
|
||||
'workflow_insights_all',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -30,6 +30,8 @@ export const getSocManager: () => Omit<Role, 'name'> = () => {
|
|||
'host_isolation_all',
|
||||
'process_operations_all',
|
||||
'actions_log_management_all',
|
||||
|
||||
'workflow_insights_all',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -32,6 +32,7 @@ export const getT3Analyst: () => Omit<Role, 'name'> = () => {
|
|||
'actions_log_management_all',
|
||||
'file_operations_all',
|
||||
'scan_operations_all',
|
||||
'workflow_insights_all',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -45,7 +45,7 @@ export const registerGetInsightsRoute = (
|
|||
},
|
||||
},
|
||||
withEndpointAuthz(
|
||||
{ all: ['canReadSecuritySolution'] },
|
||||
{ all: ['canReadWorkflowInsights'] },
|
||||
endpointContext.logFactory.get('workflowInsights'),
|
||||
getInsightsRouteHandler(endpointContext)
|
||||
)
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -46,7 +46,7 @@ export const registerUpdateInsightsRoute = (
|
|||
},
|
||||
},
|
||||
withEndpointAuthz(
|
||||
{ all: ['canReadSecuritySolution'] },
|
||||
{ all: ['canWriteWorkflowInsights'] },
|
||||
endpointContext.logFactory.get('workflowInsights'),
|
||||
updateInsightsRouteHandler(endpointContext)
|
||||
)
|
||||
|
|
|
@ -44,6 +44,7 @@ export const PLI_PRODUCT_FEATURES: PliProductFeatures = {
|
|||
ProductFeatureKey.endpointAgentTamperProtection,
|
||||
ProductFeatureKey.endpointCustomNotification,
|
||||
ProductFeatureKey.endpointProtectionUpdates,
|
||||
ProductFeatureKey.securityWorkflowInsights,
|
||||
],
|
||||
},
|
||||
cloud: {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue