Skip to content

Commit

Permalink
[Security Solution][Endpoint] Add ability for users to release an i…
Browse files Browse the repository at this point in the history
…solated 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)
  • Loading branch information
paul-tavares authored and bryce-b committed Aug 22, 2023
1 parent b33fc31 commit 53a566b
Show file tree
Hide file tree
Showing 16 changed files with 245 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -82,7 +84,7 @@ export const calculateEndpointAuthz = (

const canWriteExecuteOperations = hasKibanaPrivilege(fleetAuthz, 'writeExecuteOperations');

return {
const authz: EndpointAuthz = {
canWriteSecuritySolution,
canReadSecuritySolution,
canAccessFleet: fleetAuthz?.fleet.all ?? false,
Expand All @@ -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,
Expand All @@ -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 => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand All @@ -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,
Expand All @@ -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]);

return useMemo(() => {
if (
!isEndpointAlert ||
!doesHostSupportIsolation ||
loadingHostIsolationStatus ||
isHostIsolationPanelOpen
) {
return [];
}

const isolateHostTitle = isolationStatus === false ? ISOLATE_HOST : UNISOLATE_HOST;
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,
},
];

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;
return canIsolateHost || (isHostIsolated && canUnIsolateHost) ? menuItems : [];
}, [
isEndpointAlert,
doesHostSupportIsolation,
loadingHostIsolationStatus,
isHostIsolationPanelOpen,
agentStatus,
isolateHostHandler,
canIsolateHost,
isHostIsolated,
canUnIsolateHost,
]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -396,14 +396,18 @@ const hostIsolationSubFeature: SubFeatureConfig = {
groupType: 'mutually_exclusive',
privileges: [
{
api: [`${APP_ID}-writeHostIsolationRelease`],
id: 'host_isolation_all',
includeIn: 'none',
name: 'All',
savedObject: {
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'],
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
});
});
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -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' }] },
Expand Down Expand Up @@ -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);
});
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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);
});
}
);
Loading

0 comments on commit 53a566b

Please sign in to comment.