[Security Solution][Endpoint] Add ability for users to release an isolated host in serverless tiers where Response Actions are not available (#163616)

## Summary

- Fixes the loading of the Host Isolation sub-feature control into
kibana - should always be loaded and includes only the `release`
privilege in it
- Fixes the "Take action" menu items for Host Isolation (displayed in
alert details) to ensure `release` is displayed when host is isolated
and user has `release` privilege only
- Endpoint Response console will now NOT be available to users who only
have `release` response action (this is a downgrade scenario where the
user is still allowed to `release` isolated hosts)
This commit is contained in:
Paul Tavares 2023-08-14 21:20:55 -04:00 committed by GitHub
parent 12be587348
commit 8366d5f172
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 245 additions and 74 deletions

View file

@ -235,7 +235,15 @@ describe('Endpoint Authz service', () => {
].executePackageAction = true;
const authz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles);
expect(authz.canAccessResponseConsole).toBe(true);
// Having ONLY host isolation Release response action can only be true in a
// downgrade scenario, where we allow the user to continue to release isolated
// hosts. In that scenario, we don't show access to the response console
if (responseConsolePrivilege === 'writeHostIsolationRelease') {
expect(authz.canAccessResponseConsole).toBe(false);
} else {
expect(authz.canAccessResponseConsole).toBe(true);
}
}
);
});

View file

@ -7,6 +7,8 @@
import type { ENDPOINT_PRIVILEGES, FleetAuthz } from '@kbn/fleet-plugin/common';
import { omit } from 'lodash';
import { RESPONSE_CONSOLE_ACTION_COMMANDS_TO_REQUIRED_AUTHZ } from '../response_actions/constants';
import type { LicenseService } from '../../../license';
import type { EndpointAuthz } from '../../types/authz';
import type { MaybeImmutable } from '../../types';
@ -82,7 +84,7 @@ export const calculateEndpointAuthz = (
const canWriteExecuteOperations = hasKibanaPrivilege(fleetAuthz, 'writeExecuteOperations');
return {
const authz: EndpointAuthz = {
canWriteSecuritySolution,
canReadSecuritySolution,
canAccessFleet: fleetAuthz?.fleet.all ?? false,
@ -95,22 +97,22 @@ export const calculateEndpointAuthz = (
canWriteActionsLogManagement,
canReadActionsLogManagement: canReadActionsLogManagement && isEnterpriseLicense,
canAccessEndpointActionsLogManagement: canReadActionsLogManagement && isPlatinumPlusLicense,
// ---------------------------------------------------------
// Response Actions
// ---------------------------------------------------------
canIsolateHost: canIsolateHost && isPlatinumPlusLicense,
canUnIsolateHost,
canKillProcess: canWriteProcessOperations && isEnterpriseLicense,
canSuspendProcess: canWriteProcessOperations && isEnterpriseLicense,
canGetRunningProcesses: canWriteProcessOperations && isEnterpriseLicense,
canAccessResponseConsole:
isEnterpriseLicense &&
(canIsolateHost ||
canUnIsolateHost ||
canWriteProcessOperations ||
canWriteFileOperations ||
canWriteExecuteOperations),
canAccessResponseConsole: false, // set further below
canWriteExecuteOperations: canWriteExecuteOperations && isEnterpriseLicense,
canWriteFileOperations: canWriteFileOperations && isEnterpriseLicense,
// ---------------------------------------------------------
// artifacts
// ---------------------------------------------------------
canWriteTrustedApplications,
canReadTrustedApplications,
canWriteHostIsolationExceptions: canWriteHostIsolationExceptions && isPlatinumPlusLicense,
@ -122,6 +124,20 @@ export const calculateEndpointAuthz = (
canWriteEventFilters,
canReadEventFilters,
};
// Response console is only accessible when license is Enterprise and user has access to any
// of the response actions except `release`. Sole access to `release` is something
// that is supported for a user in a license downgrade scenario, and in that case, we don't want
// to allow access to Response Console.
authz.canAccessResponseConsole =
isEnterpriseLicense &&
Object.values(omit(RESPONSE_CONSOLE_ACTION_COMMANDS_TO_REQUIRED_AUTHZ, 'release')).some(
(responseActionAuthzKey) => {
return authz[responseActionAuthzKey];
}
);
return authz;
};
export const getEndpointAuthzInitialState = (): EndpointAuthz => {

View file

@ -27,7 +27,9 @@ export const useHostIsolationAction = ({
detailsData,
isHostIsolationPanelOpen,
onAddIsolationStatusClick,
}: UseHostIsolationActionProps) => {
}: UseHostIsolationActionProps): AlertTableContextMenuItem[] => {
const { canIsolateHost, canUnIsolateHost } = useUserPrivileges().endpointPrivileges;
const isEndpointAlert = useMemo(() => {
return isAlertFromEndpointEvent({ data: detailsData || [] });
}, [detailsData]);
@ -49,14 +51,14 @@ export const useHostIsolationAction = ({
const {
loading: loadingHostIsolationStatus,
isIsolated: isolationStatus,
isIsolated: isHostIsolated,
agentStatus,
capabilities,
} = useHostIsolationStatus({
agentId,
});
const isolationSupported = useMemo(() => {
const doesHostSupportIsolation = useMemo(() => {
return isEndpointAlert
? isIsolationSupported({
osName: hostOsFamily,
@ -66,46 +68,45 @@ export const useHostIsolationAction = ({
: false;
}, [agentVersion, capabilities, hostOsFamily, isEndpointAlert]);
const isIsolationAllowed = useUserPrivileges().endpointPrivileges.canIsolateHost;
const isolateHostHandler = useCallback(() => {
closePopover();
if (isolationStatus === false) {
if (!isHostIsolated) {
onAddIsolationStatusClick('isolateHost');
} else {
onAddIsolationStatusClick('unisolateHost');
}
}, [closePopover, isolationStatus, onAddIsolationStatusClick]);
}, [closePopover, isHostIsolated, onAddIsolationStatusClick]);
const isolateHostTitle = isolationStatus === false ? ISOLATE_HOST : UNISOLATE_HOST;
return useMemo(() => {
if (
!isEndpointAlert ||
!doesHostSupportIsolation ||
loadingHostIsolationStatus ||
isHostIsolationPanelOpen
) {
return [];
}
const hostIsolationAction: AlertTableContextMenuItem[] = useMemo(
() =>
isIsolationAllowed &&
isEndpointAlert &&
isolationSupported &&
isHostIsolationPanelOpen === false &&
loadingHostIsolationStatus === false
? [
{
key: 'isolate-host-action-item',
'data-test-subj': 'isolate-host-action-item',
disabled: agentStatus === HostStatus.UNENROLLED,
onClick: isolateHostHandler,
name: isolateHostTitle,
},
]
: [],
[
agentStatus,
isEndpointAlert,
isHostIsolationPanelOpen,
isIsolationAllowed,
isolateHostHandler,
isolateHostTitle,
isolationSupported,
loadingHostIsolationStatus,
]
);
return hostIsolationAction;
const menuItems = [
{
key: 'isolate-host-action-item',
'data-test-subj': 'isolate-host-action-item',
disabled: agentStatus === HostStatus.UNENROLLED,
onClick: isolateHostHandler,
name: isHostIsolated ? UNISOLATE_HOST : ISOLATE_HOST,
},
];
return canIsolateHost || (isHostIsolated && canUnIsolateHost) ? menuItems : [];
}, [
isEndpointAlert,
doesHostSupportIsolation,
loadingHostIsolationStatus,
isHostIsolationPanelOpen,
agentStatus,
isolateHostHandler,
canIsolateHost,
isHostIsolated,
canUnIsolateHost,
]);
};

View file

@ -122,9 +122,14 @@ export const getSecurityBaseKibanaFeature = (): BaseKibanaFeatureConfig => ({
},
});
/**
* Returns the list of Security SubFeature IDs that should be loaded and available in
* kibana regardless of PLI or License level.
* @param _
*/
export const getSecurityBaseKibanaSubFeatureIds = (
_: ExperimentalFeatures // currently un-used, but left here as a convenience for possible future use
): SecuritySubFeatureId[] => [];
): SecuritySubFeatureId[] => [SecuritySubFeatureId.hostIsolation];
/**
* Maps the AppFeatures keys to Kibana privileges that will be merged
@ -214,12 +219,13 @@ export const getSecurityAppFeaturesConfig = (
SecuritySubFeatureId.hostIsolationExceptions,
SecuritySubFeatureId.responseActionsHistory,
SecuritySubFeatureId.hostIsolation,
SecuritySubFeatureId.processOperations,
SecuritySubFeatureId.fileOperations,
SecuritySubFeatureId.executeAction,
],
subFeaturesPrivileges: [
// Adds the privilege to Isolate hosts to the already loaded `host_isolation_all`
// sub-feature (always loaded), which included the `release` privilege already
{
id: 'host_isolation_all',
api: [`${APP_ID}-writeHostIsolation`],

View file

@ -396,7 +396,6 @@ const hostIsolationSubFeature: SubFeatureConfig = {
groupType: 'mutually_exclusive',
privileges: [
{
api: [`${APP_ID}-writeHostIsolationRelease`],
id: 'host_isolation_all',
includeIn: 'none',
name: 'All',
@ -404,6 +403,11 @@ const hostIsolationSubFeature: SubFeatureConfig = {
all: [],
read: [],
},
// FYI: The current set of values below (`api`, `ui`) cover only `release` response action.
// There is a second set of values for API and UI that are added later if `endpointResponseActions`
// appFeature is enabled. Needed to ensure that in a downgrade of license condition,
// users are still able to un-isolate a host machine.
api: [`${APP_ID}-writeHostIsolationRelease`],
ui: ['writeHostIsolationRelease'],
},
],

View file

@ -24,12 +24,7 @@ export const PLI_APP_FEATURES: PliAppFeatures = {
],
},
endpoint: {
essentials: [
AppFeatureKey.endpointHostManagement,
AppFeatureKey.endpointPolicyManagement,
AppFeatureKey.endpointPolicyProtections,
AppFeatureKey.endpointArtifactManagement,
],
essentials: [AppFeatureKey.endpointPolicyProtections, AppFeatureKey.endpointArtifactManagement],
complete: [
AppFeatureKey.endpointResponseActions,
AppFeatureKey.osqueryAutomatedResponseActions,

View file

@ -0,0 +1,60 @@
/*
* 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 { login } from '../../tasks/login';
import {
getConsoleActionMenuItem,
getUnIsolateActionMenuItem,
openRowActionMenu,
visitEndpointList,
} from '../../screens/endpoint_management';
import {
CyIndexEndpointHosts,
indexEndpointHosts,
} from '../../tasks/endpoint_management/index_endpoint_hosts';
describe(
'When on the Endpoint List in Security Essentials PLI',
{
env: {
ftrConfig: {
productTypes: [{ product_line: 'security', product_tier: 'essentials' }],
},
},
},
() => {
describe('and Isolated hosts exist', () => {
let indexedEndpointData: CyIndexEndpointHosts;
before(() => {
indexEndpointHosts({ isolation: true }).then((response) => {
indexedEndpointData = response;
});
});
after(() => {
if (indexedEndpointData) {
indexedEndpointData.cleanup();
}
});
beforeEach(() => {
login();
visitEndpointList();
openRowActionMenu();
});
it('should display `release` options in host row actions', () => {
getUnIsolateActionMenuItem().should('exist');
});
it('should NOT display access to response console', () => {
getConsoleActionMenuItem().should('not.exist');
});
});
}
);

View file

@ -12,7 +12,7 @@ import { getEndpointManagementPageList } from '../../../screens/endpoint_managem
import { ensureResponseActionAuthzAccess } from '../../../tasks/endpoint_management';
describe(
'App Features for Complete PLI',
'App Features for Security Complete PLI',
{
env: {
ftrConfig: { productTypes: [{ product_line: 'security', product_tier: 'complete' }] },
@ -50,10 +50,17 @@ describe(
});
}
for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES) {
// No access to response actions (except `unisolate`)
for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES.filter(
(apiName) => apiName !== 'unisolate'
)) {
it(`should not allow access to Response Action: ${actionName}`, () => {
ensureResponseActionAuthzAccess('none', actionName, username, password);
});
}
it('should have access to `unisolate` api', () => {
ensureResponseActionAuthzAccess('all', 'unisolate', username, password);
});
}
);

View file

@ -12,7 +12,7 @@ import { getEndpointManagementPageList } from '../../../screens/endpoint_managem
import { ensureResponseActionAuthzAccess } from '../../../tasks/endpoint_management';
describe(
'App Features for Complete PLI with Endpoint Complete',
'App Features for Security Complete PLI with Endpoint Complete Addon',
{
env: {
ftrConfig: {

View file

@ -12,7 +12,7 @@ import { ensureResponseActionAuthzAccess } from '../../../tasks/endpoint_managem
import { getEndpointManagementPageList } from '../../../screens/endpoint_management';
describe(
'App Features for Essential PLI',
'App Features for Security Essential PLI',
{
env: {
ftrConfig: {
@ -52,10 +52,17 @@ describe(
});
}
for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES) {
it(`should NOT allow access to Response Action: ${actionName}`, () => {
// No access to response actions (except `unisolate`)
for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES.filter(
(apiName) => apiName !== 'unisolate'
)) {
it(`should not allow access to Response Action: ${actionName}`, () => {
ensureResponseActionAuthzAccess('none', actionName, username, password);
});
}
it('should have access to `unisolate` api', () => {
ensureResponseActionAuthzAccess('all', 'unisolate', username, password);
});
}
);

View file

@ -12,7 +12,7 @@ import { getEndpointManagementPageMap } from '../../../screens/endpoint_manageme
import { ensureResponseActionAuthzAccess } from '../../../tasks/endpoint_management';
describe(
'App Features for Essentials PLI with Endpoint Essentials',
'App Features for Security Essentials PLI with Endpoint Essentials Addon',
{
env: {
ftrConfig: {
@ -57,12 +57,18 @@ describe(
});
}
for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES) {
for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES.filter(
(apiName) => apiName !== 'unisolate'
)) {
it(`should not allow access to Response Action: ${actionName}`, () => {
ensureResponseActionAuthzAccess('none', actionName, username, password);
});
}
it('should have access to `unisolate` api', () => {
ensureResponseActionAuthzAccess('all', 'unisolate', username, password);
});
it(`should have access to Fleet`, () => {
visitFleetAgentList();
getAgentListTable().should('exist');

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { IndexedHostsAndAlertsResponse } from '@kbn/security-solution-plugin/common/endpoint/index_data';
import { pick } from 'lodash';
import { login } from '../../../tasks/login';
import { ServerlessRoleName } from '../../../../../../../shared/lib';
@ -33,6 +32,10 @@ import {
openConsoleHelpPanel,
} from '../../../screens/endpoint_management/response_console';
import { ensurePolicyDetailsPageAuthzAccess } from '../../../screens/endpoint_management/policy_details';
import {
CyIndexEndpointHosts,
indexEndpointHosts,
} from '../../../tasks/endpoint_management/index_endpoint_hosts';
describe(
'User Roles for Security Complete PLI with Endpoint Complete addon',
@ -51,17 +54,17 @@ describe(
const pageById = getEndpointManagementPageMap();
const consoleHelpPanelResponseActionsTestSubj = getConsoleHelpPanelResponseActionTestSubj();
let loadedEndpoints: IndexedHostsAndAlertsResponse;
let loadedEndpoints: CyIndexEndpointHosts;
before(() => {
cy.task('indexEndpointHosts', {}, { timeout: 240000 }).then((response) => {
indexEndpointHosts().then((response) => {
loadedEndpoints = response;
});
});
after(() => {
if (loadedEndpoints) {
cy.task('deleteIndexedEndpointHosts', loadedEndpoints);
loadedEndpoints.cleanup();
}
});
@ -136,7 +139,11 @@ describe(
it('should have read access to Endpoint Policy Management', () => {
ensurePolicyListPageAuthzAccess('read', true);
ensurePolicyDetailsPageAuthzAccess(loadedEndpoints.integrationPolicies[0].id, 'read', true);
ensurePolicyDetailsPageAuthzAccess(
loadedEndpoints.data.integrationPolicies[0].id,
'read',
true
);
});
for (const { title, id } of artifactPagesFullAccess) {

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { IndexedHostsAndAlertsResponse } from '@kbn/security-solution-plugin/common/endpoint/index_data';
import { login } from '../../../tasks/login';
import {
getNoPrivilegesPage,
@ -24,6 +23,10 @@ import {
} from '../../../screens';
import { ServerlessRoleName } from '../../../../../../../shared/lib';
import { ensurePolicyDetailsPageAuthzAccess } from '../../../screens/endpoint_management/policy_details';
import {
CyIndexEndpointHosts,
indexEndpointHosts,
} from '../../../tasks/endpoint_management/index_endpoint_hosts';
describe(
'Roles for Security Essential PLI with Endpoint Essentials addon',
@ -41,17 +44,17 @@ describe(
const allPages = getEndpointManagementPageList();
const pageById = getEndpointManagementPageMap();
let loadedEndpoints: IndexedHostsAndAlertsResponse;
let loadedEndpoints: CyIndexEndpointHosts;
before(() => {
cy.task('indexEndpointHosts', {}, { timeout: 240000 }).then((response) => {
indexEndpointHosts().then((response) => {
loadedEndpoints = response;
});
});
after(() => {
if (loadedEndpoints) {
cy.task('deleteIndexedEndpointHosts', loadedEndpoints);
loadedEndpoints.cleanup();
}
});
@ -99,7 +102,11 @@ describe(
it('should have read access to Endpoint Policy Management', () => {
ensurePolicyListPageAuthzAccess('read', true);
ensurePolicyDetailsPageAuthzAccess(loadedEndpoints.integrationPolicies[0].id, 'read', true);
ensurePolicyDetailsPageAuthzAccess(
loadedEndpoints.data.integrationPolicies[0].id,
'read',
true
);
});
for (const { title, id } of artifactPagesFullAccess) {
@ -175,7 +182,11 @@ describe(
it('should have access to policy management', () => {
ensurePolicyListPageAuthzAccess('all', true);
ensurePolicyDetailsPageAuthzAccess(loadedEndpoints.integrationPolicies[0].id, 'all', true);
ensurePolicyDetailsPageAuthzAccess(
loadedEndpoints.data.integrationPolicies[0].id,
'all',
true
);
});
it(`should NOT have access to Host Isolation Exceptions`, () => {

View file

@ -73,3 +73,11 @@ export const openRowActionMenu = (options?: ListRowOptions): Cypress.Chainable =
export const openConsoleFromEndpointList = (options?: ListRowOptions): Cypress.Chainable => {
return openRowActionMenu(options).findByTestSubj('console').click();
};
export const getUnIsolateActionMenuItem = (): Cypress.Chainable => {
return cy.getByTestSubj('tableRowActionsMenuPanel').findByTestSubj('unIsolateLink');
};
export const getConsoleActionMenuItem = (): Cypress.Chainable => {
return cy.getByTestSubj('tableRowActionsMenuPanel').findByTestSubj('console');
};

View file

@ -0,0 +1,35 @@
/*
* 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 {
DeleteIndexedHostsAndAlertsResponse,
IndexedHostsAndAlertsResponse,
} from '@kbn/security-solution-plugin/common/endpoint/index_data';
import { IndexEndpointHostsCyTaskOptions } from '@kbn/security-solution-plugin/public/management/cypress/types';
export interface CyIndexEndpointHosts {
data: IndexedHostsAndAlertsResponse;
cleanup: () => Cypress.Chainable<DeleteIndexedHostsAndAlertsResponse>;
}
export const indexEndpointHosts = (
options: IndexEndpointHostsCyTaskOptions = {}
): Cypress.Chainable<CyIndexEndpointHosts> => {
return cy.task('indexEndpointHosts', options, { timeout: 240000 }).then((indexHosts) => {
return {
data: indexHosts,
cleanup: () => {
cy.log(
'Deleting Endpoint Host data',
indexHosts.hosts.map((host) => `${host.host.name} (${host.host.id})`)
);
return cy.task('deleteIndexedEndpointHosts', indexHosts);
},
};
});
};