mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Security Solution] use endpoint rbac for isolate/unisolate host (#141810)
This commit is contained in:
parent
27483d5aa9
commit
e4080d5b64
9 changed files with 137 additions and 42 deletions
|
@ -15,7 +15,10 @@ import { ENDPOINT_PRIVILEGES } from './constants';
|
|||
|
||||
const SECURITY_SOLUTION_ID = DEFAULT_APP_CATEGORIES.security.id;
|
||||
|
||||
function generateActions(privileges: string[] = [], overrides: Record<string, boolean> = {}) {
|
||||
function generateActions(
|
||||
privileges: typeof ENDPOINT_PRIVILEGES,
|
||||
overrides: Record<string, boolean> = {}
|
||||
) {
|
||||
return privileges.reduce((acc, privilege) => {
|
||||
const executePackageAction = overrides[privilege] || false;
|
||||
|
||||
|
|
|
@ -23,4 +23,4 @@ export const ENDPOINT_PRIVILEGES = [
|
|||
'writeHostIsolation',
|
||||
'writeProcessOperations',
|
||||
'writeFileOperations',
|
||||
];
|
||||
] as const;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import type { DeletePackagePoliciesResponse, NewPackagePolicy, PackagePolicy } from './types';
|
||||
import type { FleetAuthz } from './authz';
|
||||
import { ENDPOINT_PRIVILEGES } from './constants';
|
||||
|
||||
export const createNewPackagePolicyMock = (): NewPackagePolicy => {
|
||||
return {
|
||||
|
@ -61,6 +62,15 @@ export const deletePackagePolicyMock = (): DeletePackagePoliciesResponse => {
|
|||
* Creates mock `authz` object
|
||||
*/
|
||||
export const createFleetAuthzMock = (): FleetAuthz => {
|
||||
const endpointActions = ENDPOINT_PRIVILEGES.reduce((acc, privilege) => {
|
||||
return {
|
||||
...acc,
|
||||
[privilege]: {
|
||||
executePackageAction: true,
|
||||
},
|
||||
};
|
||||
}, {});
|
||||
|
||||
return {
|
||||
fleet: {
|
||||
all: true,
|
||||
|
@ -80,5 +90,10 @@ export const createFleetAuthzMock = (): FleetAuthz => {
|
|||
readIntegrationPolicies: true,
|
||||
writeIntegrationPolicies: true,
|
||||
},
|
||||
packagePrivileges: {
|
||||
endpoint: {
|
||||
actions: endpointActions,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -107,6 +107,40 @@ describe('Endpoint Authz service', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('endpoint rbac is enabled', () => {
|
||||
describe('canIsolateHost', () => {
|
||||
it('should be true if packagePrivilege.writeHostIsolation is true', () => {
|
||||
fleetAuthz.packagePrivileges!.endpoint.actions.writeHostIsolation.executePackageAction =
|
||||
true;
|
||||
const authz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles, true);
|
||||
expect(authz.canIsolateHost).toBe(true);
|
||||
});
|
||||
|
||||
it('should be false if packagePrivilege.writeHostIsolation is false', () => {
|
||||
fleetAuthz.packagePrivileges!.endpoint.actions.writeHostIsolation.executePackageAction =
|
||||
false;
|
||||
const authz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles, true);
|
||||
expect(authz.canIsolateHost).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('canUnIsolateHost', () => {
|
||||
it('should be true if packagePrivilege.writeHostIsolation is true', () => {
|
||||
fleetAuthz.packagePrivileges!.endpoint.actions.writeHostIsolation.executePackageAction =
|
||||
true;
|
||||
const authz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles, true);
|
||||
expect(authz.canUnIsolateHost).toBe(true);
|
||||
});
|
||||
|
||||
it('should be false if packagePrivilege.writeHostIsolation is false', () => {
|
||||
fleetAuthz.packagePrivileges!.endpoint.actions.writeHostIsolation.executePackageAction =
|
||||
false;
|
||||
const authz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles, true);
|
||||
expect(authz.canUnIsolateHost).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEndpointAuthzInitialState()', () => {
|
||||
|
|
|
@ -28,14 +28,18 @@ export const calculateEndpointAuthz = (
|
|||
const isPlatinumPlusLicense = licenseService.isPlatinumPlus();
|
||||
const isEnterpriseLicense = licenseService.isEnterprise();
|
||||
const hasEndpointManagementAccess = userRoles.includes('superuser');
|
||||
const canIsolateHost = isEndpointRbacEnabled
|
||||
? fleetAuthz.packagePrivileges?.endpoint?.actions?.writeHostIsolation?.executePackageAction ||
|
||||
false
|
||||
: hasEndpointManagementAccess;
|
||||
|
||||
return {
|
||||
canAccessFleet: fleetAuthz?.fleet.all ?? userRoles.includes('superuser'),
|
||||
canAccessEndpointManagement: hasEndpointManagementAccess,
|
||||
canCreateArtifactsByPolicy: hasEndpointManagementAccess && isPlatinumPlusLicense,
|
||||
// Response Actions
|
||||
canIsolateHost: isPlatinumPlusLicense && hasEndpointManagementAccess,
|
||||
canUnIsolateHost: hasEndpointManagementAccess,
|
||||
canIsolateHost: isPlatinumPlusLicense && canIsolateHost,
|
||||
canUnIsolateHost: canIsolateHost,
|
||||
canKillProcess: hasEndpointManagementAccess && isEnterpriseLicense,
|
||||
canSuspendProcess: hasEndpointManagementAccess && isEnterpriseLicense,
|
||||
canGetRunningProcesses: hasEndpointManagementAccess && isEnterpriseLicense,
|
||||
|
|
|
@ -7,15 +7,19 @@
|
|||
|
||||
import type { RenderHookResult, RenderResult } from '@testing-library/react-hooks';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { useCurrentUser, useKibana } from '../../../lib/kibana';
|
||||
import { useEndpointPrivileges } from './use_endpoint_privileges';
|
||||
|
||||
import { securityMock } from '@kbn/security-plugin/public/mocks';
|
||||
import type { AuthenticatedUser } from '@kbn/security-plugin/common';
|
||||
import { licenseService } from '../../../hooks/use_license';
|
||||
import { getEndpointPrivilegesInitialStateMock } from './mocks';
|
||||
import { createFleetAuthzMock } from '@kbn/fleet-plugin/common';
|
||||
|
||||
import type { EndpointPrivileges } from '../../../../../common/endpoint/types';
|
||||
import { useCurrentUser, useKibana } from '../../../lib/kibana';
|
||||
import { licenseService } from '../../../hooks/use_license';
|
||||
import { useEndpointPrivileges } from './use_endpoint_privileges';
|
||||
import { getEndpointPrivilegesInitialStateMock } from './mocks';
|
||||
import { getEndpointPrivilegesInitialState } from './utils';
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
jest.mock('../../../lib/kibana');
|
||||
jest.mock('../../../hooks/use_license', () => {
|
||||
const licenseServiceInstance = {
|
||||
|
@ -47,6 +51,7 @@ describe('When using useEndpointPrivileges hook', () => {
|
|||
});
|
||||
|
||||
(useCurrentUser as jest.Mock).mockReturnValue(authenticatedUser);
|
||||
useKibanaMock().services.fleet!.authz = createFleetAuthzMock();
|
||||
|
||||
licenseServiceMock.isPlatinumPlus.mockReturnValue(true);
|
||||
|
||||
|
|
|
@ -14,6 +14,9 @@ import { act } from '@testing-library/react';
|
|||
import { endpointPageHttpMock } from '../../../mocks';
|
||||
import { fireEvent } from '@testing-library/dom';
|
||||
import { licenseService } from '../../../../../../common/hooks/use_license';
|
||||
import { useUserPrivileges } from '../../../../../../common/components/user_privileges';
|
||||
import { initialUserPrivilegesState } from '../../../../../../common/components/user_privileges/user_privileges_context';
|
||||
import { getUserPrivilegesMockDefaultValue } from '../../../../../../common/components/user_privileges/__mocks__';
|
||||
|
||||
jest.mock('../../../../../../common/lib/kibana/kibana_react', () => {
|
||||
const originalModule = jest.requireActual('../../../../../../common/lib/kibana/kibana_react');
|
||||
|
@ -31,6 +34,7 @@ jest.mock('../../../../../../common/lib/kibana/kibana_react', () => {
|
|||
};
|
||||
});
|
||||
jest.mock('../../../../../../common/hooks/use_license');
|
||||
jest.mock('../../../../../../common/components/user_privileges');
|
||||
|
||||
describe('When using the Endpoint Details Actions Menu', () => {
|
||||
let render: () => Promise<ReturnType<AppContextTestRender['render']>>;
|
||||
|
@ -59,6 +63,8 @@ describe('When using the Endpoint Details Actions Menu', () => {
|
|||
waitForAction = mockedContext.middlewareSpy.waitForAction;
|
||||
httpMocks = endpointPageHttpMock(mockedContext.coreStart.http);
|
||||
|
||||
(useUserPrivileges as jest.Mock).mockReturnValue(getUserPrivilegesMockDefaultValue());
|
||||
|
||||
act(() => {
|
||||
mockedContext.history.push(
|
||||
'/administration/endpoints?selected_endpoint=5fe11314-678c-413e-87a2-b4a3461878ee'
|
||||
|
@ -80,6 +86,10 @@ describe('When using the Endpoint Details Actions Menu', () => {
|
|||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(useUserPrivileges as jest.Mock).mockClear();
|
||||
});
|
||||
|
||||
it('should not show the response actions history link', async () => {
|
||||
await render();
|
||||
expect(renderResult.queryByTestId('actionsLink')).toBeNull();
|
||||
|
@ -121,6 +131,7 @@ describe('When using the Endpoint Details Actions Menu', () => {
|
|||
describe('and endpoint host is isolated', () => {
|
||||
beforeEach(() => setEndpointMetadataResponse(true));
|
||||
|
||||
describe('and user has unisolate privilege', () => {
|
||||
it('should display Unisolate action', async () => {
|
||||
await render();
|
||||
expect(renderResult.getByTestId('unIsolateLink')).not.toBeNull();
|
||||
|
@ -136,6 +147,25 @@ describe('When using the Endpoint Details Actions Menu', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('and user does not have unisolate privilege', () => {
|
||||
beforeEach(() => {
|
||||
(useUserPrivileges as jest.Mock).mockReturnValue({
|
||||
...initialUserPrivilegesState(),
|
||||
endpointPrivileges: {
|
||||
...initialUserPrivilegesState().endpointPrivileges,
|
||||
canIsolateHost: false,
|
||||
canUnIsolateHost: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not display unisolate action', async () => {
|
||||
await render();
|
||||
expect(renderResult.queryByTestId('unIsolateLink')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and license is NOT PlatinumPlus', () => {
|
||||
const licenseServiceMock = licenseService as jest.Mocked<typeof licenseService>;
|
||||
|
||||
|
@ -143,12 +173,6 @@ describe('When using the Endpoint Details Actions Menu', () => {
|
|||
|
||||
afterEach(() => licenseServiceMock.isPlatinumPlus.mockReturnValue(true));
|
||||
|
||||
it('should not show the `isolate` action', async () => {
|
||||
setEndpointMetadataResponse();
|
||||
await render();
|
||||
expect(renderResult.queryByTestId('isolateLink')).toBeNull();
|
||||
});
|
||||
|
||||
it('should still show `unisolate` action for endpoints that are currently isolated', async () => {
|
||||
setEndpointMetadataResponse(true);
|
||||
await render();
|
||||
|
|
|
@ -19,7 +19,6 @@ import { agentPolicies, uiQueryParams } from '../../store/selectors';
|
|||
import { useAppUrl } from '../../../../../common/lib/kibana/hooks';
|
||||
import type { ContextMenuItemNavByRouterProps } from '../../../../components/context_menu_with_router_support/context_menu_item_nav_by_router';
|
||||
import { isEndpointHostIsolated } from '../../../../../common/utils/validators';
|
||||
import { useLicense } from '../../../../../common/hooks/use_license';
|
||||
import { isIsolationSupported } from '../../../../../../common/endpoint/service/host_isolation/utils';
|
||||
import { useDoesEndpointSupportResponder } from '../../../../../common/hooks/endpoint/use_does_endpoint_support_responder';
|
||||
import { UPGRADE_ENDPOINT_FOR_RESPONDER } from '../../../../../common/translations';
|
||||
|
@ -36,7 +35,6 @@ export const useEndpointActionItems = (
|
|||
endpointMetadata: MaybeImmutable<HostMetadata> | undefined,
|
||||
options?: Options
|
||||
): ContextMenuItemNavByRouterProps[] => {
|
||||
const isPlatinumPlus = useLicense().isPlatinumPlus();
|
||||
const { getAppUrl } = useAppUrl();
|
||||
const fleetAgentPolicies = useEndpointSelector(agentPolicies);
|
||||
const allCurrentUrlParams = useEndpointSelector(uiQueryParams);
|
||||
|
@ -44,7 +42,8 @@ export const useEndpointActionItems = (
|
|||
const isResponseActionsConsoleEnabled = useIsExperimentalFeatureEnabled(
|
||||
'responseActionsConsoleEnabled'
|
||||
);
|
||||
const canAccessResponseConsole = useUserPrivileges().endpointPrivileges.canAccessResponseConsole;
|
||||
const { canAccessResponseConsole, canIsolateHost, canUnIsolateHost } =
|
||||
useUserPrivileges().endpointPrivileges;
|
||||
const isResponderCapabilitiesEnabled = useDoesEndpointSupportResponder(endpointMetadata);
|
||||
|
||||
return useMemo<ContextMenuItemNavByRouterProps[]>(() => {
|
||||
|
@ -82,8 +81,8 @@ export const useEndpointActionItems = (
|
|||
|
||||
const isolationActions = [];
|
||||
|
||||
if (isIsolated) {
|
||||
// Un-isolate is always available to users regardless of license level
|
||||
if (isIsolated && canUnIsolateHost) {
|
||||
// Un-isolate is available to users regardless of license level if they have unisolate permissions
|
||||
isolationActions.push({
|
||||
'data-test-subj': 'unIsolateLink',
|
||||
icon: 'lockOpen',
|
||||
|
@ -100,7 +99,7 @@ export const useEndpointActionItems = (
|
|||
/>
|
||||
),
|
||||
});
|
||||
} else if (isPlatinumPlus && isolationSupported) {
|
||||
} else if (isolationSupported && canIsolateHost) {
|
||||
// For Platinum++ licenses, users also have ability to isolate
|
||||
isolationActions.push({
|
||||
'data-test-subj': 'isolateLink',
|
||||
|
@ -260,10 +259,11 @@ export const useEndpointActionItems = (
|
|||
endpointMetadata,
|
||||
fleetAgentPolicies,
|
||||
getAppUrl,
|
||||
isPlatinumPlus,
|
||||
isResponseActionsConsoleEnabled,
|
||||
showEndpointResponseActionsConsole,
|
||||
options?.isEndpointList,
|
||||
isResponderCapabilitiesEnabled,
|
||||
canIsolateHost,
|
||||
canUnIsolateHost,
|
||||
]);
|
||||
};
|
||||
|
|
|
@ -1007,6 +1007,7 @@ describe('when on the endpoint list page', () => {
|
|||
let agentId: string;
|
||||
let agentPolicyId: string;
|
||||
let renderResult: ReturnType<AppContextTestRender['render']>;
|
||||
let endpointActionsButton: HTMLElement;
|
||||
|
||||
// 2nd endpoint only has isolation capabilities
|
||||
const mockEndpointListApi = () => {
|
||||
|
@ -1081,13 +1082,7 @@ describe('when on the endpoint list page', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
mockEndpointListApi();
|
||||
(useUserPrivileges as jest.Mock).mockReturnValue({
|
||||
...mockInitialUserPrivilegesState(),
|
||||
endpointPrivileges: {
|
||||
...mockInitialUserPrivilegesState().endpointPrivileges,
|
||||
canAccessResponseConsole: true,
|
||||
},
|
||||
});
|
||||
(useUserPrivileges as jest.Mock).mockReturnValue(getUserPrivilegesMockDefaultValue());
|
||||
|
||||
reactTestingLibrary.act(() => {
|
||||
history.push(`${MANAGEMENT_PATH}/endpoints`);
|
||||
|
@ -1097,9 +1092,7 @@ describe('when on the endpoint list page', () => {
|
|||
await middlewareSpy.waitForAction('serverReturnedEndpointList');
|
||||
await middlewareSpy.waitForAction('serverReturnedEndpointAgentPolicies');
|
||||
|
||||
const endpointActionsButton = (
|
||||
await renderResult.findAllByTestId('endpointTableRowActions')
|
||||
)[0];
|
||||
endpointActionsButton = (await renderResult.findAllByTestId('endpointTableRowActions'))[0];
|
||||
|
||||
reactTestingLibrary.act(() => {
|
||||
reactTestingLibrary.fireEvent.click(endpointActionsButton);
|
||||
|
@ -1108,7 +1101,6 @@ describe('when on the endpoint list page', () => {
|
|||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useUserPrivileges as jest.Mock).mockReturnValue(getUserPrivilegesMockDefaultValue());
|
||||
});
|
||||
|
||||
it('shows the Responder option when all 3 processes capabilities are present in the endpoint', async () => {
|
||||
|
@ -1141,6 +1133,24 @@ describe('when on the endpoint list page', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('hides isolate host option if canIsolateHost is false', () => {
|
||||
(useUserPrivileges as jest.Mock).mockReturnValue({
|
||||
...mockInitialUserPrivilegesState(),
|
||||
endpointPrivileges: {
|
||||
...mockInitialUserPrivilegesState().endpointPrivileges,
|
||||
canIsolateHost: false,
|
||||
},
|
||||
});
|
||||
reactTestingLibrary.act(() => {
|
||||
reactTestingLibrary.fireEvent.click(endpointActionsButton);
|
||||
});
|
||||
reactTestingLibrary.act(() => {
|
||||
reactTestingLibrary.fireEvent.click(endpointActionsButton);
|
||||
});
|
||||
const isolateLink = screen.queryByTestId('isolateLink');
|
||||
expect(isolateLink).toBeNull();
|
||||
});
|
||||
|
||||
it('navigates to the Security Solution Host Details page', async () => {
|
||||
const hostLink = await renderResult.findByTestId('hostLink');
|
||||
expect(hostLink.getAttribute('href')).toEqual(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue