diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index dd7ceab0cbd5c..24c1fc13a5753 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1239,6 +1239,7 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib /x-pack/plugins/security_solution/public/common/components/ml_popover @elastic/security-detection-rule-management /x-pack/plugins/security_solution/public/common/components/popover_items @elastic/security-detection-rule-management /x-pack/plugins/security_solution/public/detection_engine/fleet_integrations @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/public/detection_engine/endpoint_exceptions @elastic/security-defend-workflows /x-pack/plugins/security_solution/public/detection_engine/rule_details_ui @elastic/security-detection-rule-management /x-pack/plugins/security_solution/public/detection_engine/rule_management @elastic/security-detection-rule-management /x-pack/plugins/security_solution/public/detection_engine/rule_management_ui @elastic/security-detection-rule-management @@ -1336,6 +1337,7 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib /x-pack/test/security_solution_endpoint_api_int/ @elastic/security-defend-workflows /x-pack/test_serverless/shared/lib/security/kibana_roles/ @elastic/security-defend-workflows /x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management @elastic/security-defend-workflows +/x-pack/plugins/security_solution_serverless/public/upselling/pages/endpoint_management @elastic/security-defend-workflows /x-pack/plugins/security_solution_serverless/server/endpoint @elastic/security-defend-workflows ## Security Solution sub teams - security-telemetry (Data Engineering) diff --git a/x-pack/packages/security-solution/upselling/service/types.ts b/x-pack/packages/security-solution/upselling/service/types.ts index 2f5a06c391471..d14f39ac9796a 100644 --- a/x-pack/packages/security-solution/upselling/service/types.ts +++ b/x-pack/packages/security-solution/upselling/service/types.ts @@ -14,6 +14,7 @@ export type SectionUpsellings = Partial { }); }); + describe('#calculateEndpointExceptionsPrivilegesFromCapabilities', () => { + it('calculates endpoint exceptions privileges correctly', () => { + const endpointExceptionsCapabilities = { + showEndpointExceptions: false, + crudEndpointExceptions: true, + }; + + const expected = { + actions: { + showEndpointExceptions: false, + crudEndpointExceptions: true, + }, + }; + + const actual = calculateEndpointExceptionsPrivilegesFromCapabilities({ + navLinks: {}, + management: {}, + catalogue: {}, + siem: endpointExceptionsCapabilities, + }); + + expect(actual).toEqual(expected); + }); + + it('calculates endpoint exceptions privileges correctly when no matching capabilities', () => { + const endpointCapabilities = { + writeEndpointList: true, + writeTrustedApplications: true, + writePolicyManagement: false, + readPolicyManagement: true, + writeHostIsolationExceptions: true, + writeHostIsolation: false, + }; + const expected = { + actions: { + showEndpointExceptions: false, + crudEndpointExceptions: false, + }, + }; + const actual = calculateEndpointExceptionsPrivilegesFromCapabilities({ + navLinks: {}, + management: {}, + catalogue: {}, + siem: endpointCapabilities, + }); + + expect(actual).toEqual(expected); + }); + }); + describe('calculatePackagePrivilegesFromKibanaPrivileges', () => { it('calculates privileges correctly', () => { const endpointPrivileges = [ @@ -111,4 +164,86 @@ describe('fleet authz', () => { expect(actual).toEqual(expected); }); }); + + describe('#calculateEndpointExceptionsPrivilegesFromKibanaPrivileges', () => { + it('calculates endpoint exceptions privileges correctly', () => { + const endpointExceptionsPrivileges = [ + { privilege: `${SECURITY_SOLUTION_ID}-showEndpointExceptions`, authorized: true }, + { privilege: `${SECURITY_SOLUTION_ID}-crudEndpointExceptions`, authorized: false }, + { privilege: `${SECURITY_SOLUTION_ID}-ignoreMe`, authorized: true }, + ]; + const expected = { + actions: { + showEndpointExceptions: true, + crudEndpointExceptions: false, + }, + }; + const actual = calculateEndpointExceptionsPrivilegesFromKibanaPrivileges( + endpointExceptionsPrivileges + ); + expect(actual).toEqual(expected); + }); + }); + + describe('#getAuthorizationFromPrivileges', () => { + it('returns `false` when no `prefix` nor `searchPrivilege`', () => { + expect( + getAuthorizationFromPrivileges({ + kibanaPrivileges: [ + { + privilege: `${SECURITY_SOLUTION_ID}-ignoreMe`, + authorized: true, + }, + ], + }) + ).toEqual(false); + }); + + it('returns correct Boolean when `prefix` and `searchPrivilege` are given', () => { + const kibanaPrivileges = [ + { privilege: `${SECURITY_SOLUTION_ID}-writeHostIsolationExceptions`, authorized: false }, + { privilege: `${SECURITY_SOLUTION_ID}-writeHostIsolation`, authorized: true }, + { privilege: `${SECURITY_SOLUTION_ID}-ignoreMe`, authorized: false }, + ]; + + expect( + getAuthorizationFromPrivileges({ + kibanaPrivileges, + prefix: `${SECURITY_SOLUTION_ID}-`, + searchPrivilege: `writeHostIsolation`, + }) + ).toEqual(true); + }); + + it('returns correct Boolean when only `prefix` is given', () => { + const kibanaPrivileges = [ + { privilege: `ignore-me-writeHostIsolationExceptions`, authorized: false }, + { privilege: `${SECURITY_SOLUTION_ID}-writeHostIsolation`, authorized: true }, + { privilege: `${SECURITY_SOLUTION_ID}-ignoreMe`, authorized: false }, + ]; + + expect( + getAuthorizationFromPrivileges({ + kibanaPrivileges, + prefix: `${SECURITY_SOLUTION_ID}-`, + searchPrivilege: `writeHostIsolation`, + }) + ).toEqual(true); + }); + + it('returns correct Boolean when only `searchPrivilege` is given', () => { + const kibanaPrivileges = [ + { privilege: `${SECURITY_SOLUTION_ID}-writeHostIsolationExceptions`, authorized: false }, + { privilege: `${SECURITY_SOLUTION_ID}-writeHostIsolation`, authorized: true }, + { privilege: `${SECURITY_SOLUTION_ID}-ignoreMe`, authorized: false }, + ]; + + expect( + getAuthorizationFromPrivileges({ + kibanaPrivileges, + searchPrivilege: `writeHostIsolation`, + }) + ).toEqual(true); + }); + }); }); diff --git a/x-pack/plugins/fleet/common/authz.ts b/x-pack/plugins/fleet/common/authz.ts index 27e72857392a8..97918648db25b 100644 --- a/x-pack/plugins/fleet/common/authz.ts +++ b/x-pack/plugins/fleet/common/authz.ts @@ -9,7 +9,7 @@ import type { Capabilities } from '@kbn/core-capabilities-common'; import { TRANSFORM_PLUGIN_ID } from './constants/plugin'; -import { ENDPOINT_PRIVILEGES } from './constants'; +import { ENDPOINT_EXCEPTIONS_PRIVILEGES, ENDPOINT_PRIVILEGES } from './constants'; export type TransformPrivilege = | 'canGetTransform' @@ -49,6 +49,13 @@ export interface FleetAuthz { }; }; }; + + endpointExceptionsPrivileges?: { + actions: { + crudEndpointExceptions: boolean; + showEndpointExceptions: boolean; + }; + }; } interface CalculateParams { @@ -135,19 +142,50 @@ export function calculatePackagePrivilegesFromCapabilities( }; } -function getAuthorizationFromPrivileges( +export function calculateEndpointExceptionsPrivilegesFromCapabilities( + capabilities: Capabilities | undefined +): FleetAuthz['endpointExceptionsPrivileges'] { + if (!capabilities || !capabilities.siem) { + return; + } + + const endpointExceptionsActions = Object.keys(ENDPOINT_EXCEPTIONS_PRIVILEGES).reduce< + Record + >((acc, privilegeName) => { + acc[privilegeName] = (capabilities.siem[privilegeName] as boolean) || false; + return acc; + }, {}); + + return { + actions: endpointExceptionsActions, + } as FleetAuthz['endpointExceptionsPrivileges']; +} + +export function getAuthorizationFromPrivileges({ + kibanaPrivileges, + searchPrivilege = '', + prefix = '', +}: { kibanaPrivileges: Array<{ resource?: string; privilege: string; authorized: boolean; - }>, - prefix: string, - searchPrivilege: string -): boolean { - const privilege = kibanaPrivileges.find((p) => - p.privilege.endsWith(`${prefix}${searchPrivilege}`) - ); - return privilege?.authorized || false; + }>; + prefix?: string; + searchPrivilege?: string; +}): boolean { + const privilege = kibanaPrivileges.find((p) => { + if (prefix.length && searchPrivilege.length) { + return p.privilege.endsWith(`${prefix}${searchPrivilege}`); + } else if (prefix.length) { + return p.privilege.endsWith(`${prefix}`); + } else if (searchPrivilege.length) { + return p.privilege.endsWith(`${searchPrivilege}`); + } + return false; + }); + + return !!privilege?.authorized; } export function calculatePackagePrivilegesFromKibanaPrivileges( @@ -165,11 +203,11 @@ export function calculatePackagePrivilegesFromKibanaPrivileges( const endpointActions = Object.entries(ENDPOINT_PRIVILEGES).reduce( (acc, [privilege, { appId, privilegeSplit, privilegeName }]) => { - const kibanaPrivilege = getAuthorizationFromPrivileges( + const kibanaPrivilege = getAuthorizationFromPrivileges({ kibanaPrivileges, - `${appId}${privilegeSplit}`, - privilegeName - ); + prefix: `${appId}${privilegeSplit}`, + searchPrivilege: privilegeName, + }); acc[privilege] = { executePackageAction: kibanaPrivilege, }; @@ -178,11 +216,11 @@ export function calculatePackagePrivilegesFromKibanaPrivileges( {} ); - const hasTransformAdmin = getAuthorizationFromPrivileges( + const hasTransformAdmin = getAuthorizationFromPrivileges({ kibanaPrivileges, - `${TRANSFORM_PLUGIN_ID}-`, - `admin` - ); + prefix: `${TRANSFORM_PLUGIN_ID}-`, + searchPrivilege: `admin`, + }); const transformActions: { [key in TransformPrivilege]: { executePackageAction: boolean; @@ -198,11 +236,11 @@ export function calculatePackagePrivilegesFromKibanaPrivileges( executePackageAction: hasTransformAdmin, }, canGetTransform: { - executePackageAction: getAuthorizationFromPrivileges( + executePackageAction: getAuthorizationFromPrivileges({ kibanaPrivileges, - `${TRANSFORM_PLUGIN_ID}-`, - `read` - ), + prefix: `${TRANSFORM_PLUGIN_ID}-`, + searchPrivilege: `read`, + }), }, }; @@ -215,3 +253,28 @@ export function calculatePackagePrivilegesFromKibanaPrivileges( }, }; } + +export function calculateEndpointExceptionsPrivilegesFromKibanaPrivileges( + kibanaPrivileges: + | Array<{ + resource?: string; + privilege: string; + authorized: boolean; + }> + | undefined +): FleetAuthz['endpointExceptionsPrivileges'] { + if (!kibanaPrivileges || !kibanaPrivileges.length) { + return; + } + const endpointExceptionsActions = Object.entries(ENDPOINT_EXCEPTIONS_PRIVILEGES).reduce< + Record + >((acc, [privilege, { appId, privilegeSplit, privilegeName }]) => { + acc[privilege] = getAuthorizationFromPrivileges({ + kibanaPrivileges, + searchPrivilege: privilegeName, + }); + return acc; + }, {}); + + return { actions: endpointExceptionsActions } as FleetAuthz['endpointExceptionsPrivileges']; +} diff --git a/x-pack/plugins/fleet/common/constants/authz.ts b/x-pack/plugins/fleet/common/constants/authz.ts index 77f5c0b798b2f..72c975af1a3d1 100644 --- a/x-pack/plugins/fleet/common/constants/authz.ts +++ b/x-pack/plugins/fleet/common/constants/authz.ts @@ -10,7 +10,7 @@ import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; const SECURITY_SOLUTION_APP_ID = 'siem'; -interface PrivilegeMapObject { +export interface PrivilegeMapObject { appId: string; privilegeSplit: string; privilegeType: 'ui' | 'api'; @@ -163,3 +163,18 @@ export const ENDPOINT_PRIVILEGES: Record = deepFreez privilegeName: 'writeExecuteOperations', }, }); + +export const ENDPOINT_EXCEPTIONS_PRIVILEGES: Record = deepFreeze({ + showEndpointExceptions: { + appId: DEFAULT_APP_CATEGORIES.security.id, + privilegeSplit: '-', + privilegeType: 'api', + privilegeName: 'showEndpointExceptions', + }, + crudEndpointExceptions: { + appId: DEFAULT_APP_CATEGORIES.security.id, + privilegeSplit: '-', + privilegeType: 'api', + privilegeName: 'crudEndpointExceptions', + }, +}); diff --git a/x-pack/plugins/fleet/common/mocks.ts b/x-pack/plugins/fleet/common/mocks.ts index 13d0edcf07e58..8e2545adf5ed5 100644 --- a/x-pack/plugins/fleet/common/mocks.ts +++ b/x-pack/plugins/fleet/common/mocks.ts @@ -6,10 +6,10 @@ */ import type { - PostDeletePackagePoliciesResponse, + AgentPolicy, NewPackagePolicy, PackagePolicy, - AgentPolicy, + PostDeletePackagePoliciesResponse, } from './types'; import type { FleetAuthz } from './authz'; import { dataTypes, ENDPOINT_PRIVILEGES } from './constants'; @@ -108,6 +108,12 @@ export const createFleetAuthzMock = (): FleetAuthz => { }, }, }, + endpointExceptionsPrivileges: { + actions: { + showEndpointExceptions: true, + crudEndpointExceptions: true, + }, + }, }; }; diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts index 95452d52c4b12..7c1aadeb530eb 100644 --- a/x-pack/plugins/fleet/public/plugin.ts +++ b/x-pack/plugins/fleet/public/plugin.ts @@ -9,18 +9,19 @@ import React from 'react'; import type { AppMountParameters, CoreSetup, + CoreStart, Plugin, PluginInitializerContext, - CoreStart, } from '@kbn/core/public'; +import { AppNavLinkStatus, DEFAULT_APP_CATEGORIES } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; import type { - CustomIntegrationsStart, CustomIntegrationsSetup, + CustomIntegrationsStart, } from '@kbn/custom-integrations-plugin/public'; import type { SharePluginStart } from '@kbn/share-plugin/public'; @@ -29,20 +30,17 @@ import { once } from 'lodash'; import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; import type { DiscoverStart } from '@kbn/discover-plugin/public'; -import type { CloudStart } from '@kbn/cloud-plugin/public'; +import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public'; import type { UsageCollectionSetup, UsageCollectionStart, } from '@kbn/usage-collection-plugin/public'; -import { DEFAULT_APP_CATEGORIES, AppNavLinkStatus } from '@kbn/core/public'; - import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import type { LicensingPluginStart } from '@kbn/licensing-plugin/public'; -import type { CloudSetup } from '@kbn/cloud-plugin/public'; import type { GlobalSearchPluginSetup } from '@kbn/global-search-plugin/public'; import type { SendRequestResponse } from '@kbn/es-ui-shared-plugin/public'; @@ -52,40 +50,43 @@ import type { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/ import type { DashboardStart } from '@kbn/dashboard-plugin/public'; -import { PLUGIN_ID, INTEGRATIONS_PLUGIN_ID, setupRouteService, appRoutesService } from '../common'; -import { calculateAuthz, calculatePackagePrivilegesFromCapabilities } from '../common/authz'; -import { parseExperimentalConfigValue } from '../common/experimental_features'; -import type { CheckPermissionsResponse, PostFleetSetupResponse } from '../common/types'; import type { FleetAuthz } from '../common'; +import { appRoutesService, INTEGRATIONS_PLUGIN_ID, PLUGIN_ID, setupRouteService } from '../common'; +import { + calculateAuthz, + calculateEndpointExceptionsPrivilegesFromCapabilities, + calculatePackagePrivilegesFromCapabilities, +} from '../common/authz'; import type { ExperimentalFeatures } from '../common/experimental_features'; - -import type { FleetConfigType } from '../common/types'; +import { parseExperimentalConfigValue } from '../common/experimental_features'; +import type { + CheckPermissionsResponse, + FleetConfigType, + PostFleetSetupResponse, +} from '../common/types'; import { API_VERSIONS } from '../common/constants'; import { CUSTOM_LOGS_INTEGRATION_NAME, INTEGRATIONS_BASE_PATH } from './constants'; -import { licenseService } from './hooks'; +import type { RequestError } from './hooks'; +import { licenseService, sendGetBulkAssets } from './hooks'; import { setHttpClient } from './hooks/use_request'; import { createPackageSearchProvider } from './search_provider'; import { TutorialDirectoryHeaderLink, TutorialModuleNotice } from './components/home_integration'; import { createExtensionRegistrationCallback } from './services/ui_extensions'; import { ExperimentalFeaturesService } from './services/experimental_features'; import type { - UIExtensionRegistrationCallback, - UIExtensionsStorage, GetBulkAssetsRequest, GetBulkAssetsResponse, + UIExtensionRegistrationCallback, + UIExtensionsStorage, } from './types'; import { LazyCustomLogsAssetsExtension } from './lazy_custom_logs_assets_extension'; - -export type { FleetConfigType } from '../common/types'; - import { setCustomIntegrations, setCustomIntegrationsStart } from './services/custom_integrations'; - -import type { RequestError } from './hooks'; -import { sendGetBulkAssets } from './hooks'; import { getFleetDeepLinks } from './deep_links'; +export type { FleetConfigType } from '../common/types'; + // We need to provide an object instead of void so that dependent plugins know when Fleet // is disabled. // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -326,6 +327,8 @@ export class FleetPlugin implements Plugin { diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index 74af2fe533a9b..37e570648e392 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -75,11 +75,13 @@ export { FLEET_PROXY_SAVED_OBJECT_TYPE, // Authz ENDPOINT_PRIVILEGES, + ENDPOINT_EXCEPTIONS_PRIVILEGES, // Message signing service MESSAGE_SIGNING_SERVICE_API_ROUTES, // secrets SECRETS_ENDPOINT_PATH, SECRETS_MINIMUM_FLEET_SERVER_VERSION, + type PrivilegeMapObject, } from '../../common/constants'; export { diff --git a/x-pack/plugins/fleet/server/services/security/security.ts b/x-pack/plugins/fleet/server/services/security/security.ts index 715d8d966484f..76986768416ff 100644 --- a/x-pack/plugins/fleet/server/services/security/security.ts +++ b/x-pack/plugins/fleet/server/services/security/security.ts @@ -9,22 +9,31 @@ import { pick } from 'lodash'; import type { KibanaRequest } from '@kbn/core/server'; +import type { SecurityPluginStart } from '@kbn/security-plugin/server'; + import { TRANSFORM_PLUGIN_ID } from '../../../common/constants/plugin'; import type { FleetAuthz } from '../../../common'; import { INTEGRATIONS_PLUGIN_ID } from '../../../common'; import { calculateAuthz, + calculateEndpointExceptionsPrivilegesFromKibanaPrivileges, calculatePackagePrivilegesFromKibanaPrivileges, + getAuthorizationFromPrivileges, } from '../../../common/authz'; import { appContextService } from '..'; -import { ENDPOINT_PRIVILEGES, PLUGIN_ID } from '../../constants'; +import { + ENDPOINT_EXCEPTIONS_PRIVILEGES, + ENDPOINT_PRIVILEGES, + PLUGIN_ID, + type PrivilegeMapObject, +} from '../../constants'; import type { FleetAuthzRequirements, - FleetRouteRequiredAuthz, FleetAuthzRouteConfig, + FleetRouteRequiredAuthz, } from './types'; export function checkSecurityEnabled() { @@ -51,31 +60,31 @@ export function checkSuperuser(req: KibanaRequest) { return true; } -function getAuthorizationFromPrivileges( - kibanaPrivileges: Array<{ - resource?: string; - privilege: string; - authorized: boolean; - }>, - searchPrivilege: string -) { - const privilege = kibanaPrivileges.find((p) => p.privilege.includes(searchPrivilege)); - return privilege ? privilege.authorized : false; -} +const computeUiApiPrivileges = ( + security: SecurityPluginStart, + privileges: Record +): string[] => { + return Object.entries(privileges).map( + ([_, { appId, privilegeType, privilegeSplit, privilegeName }]) => { + if (privilegeType === 'ui') { + return security.authz.actions[privilegeType].get(`${appId}`, `${privilegeName}`); + } + return security.authz.actions[privilegeType].get(`${appId}${privilegeSplit}${privilegeName}`); + } + ); +}; export async function getAuthzFromRequest(req: KibanaRequest): Promise { const security = appContextService.getSecurity(); if (security.authz.mode.useRbacForRequest(req)) { const checkPrivileges = security.authz.checkPrivilegesDynamicallyWithRequest(req); - const endpointPrivileges = Object.entries(ENDPOINT_PRIVILEGES).map( - ([_, { appId, privilegeType, privilegeName }]) => { - if (privilegeType === 'ui') { - return security.authz.actions[privilegeType].get(`${appId}`, `${privilegeName}`); - } - return security.authz.actions[privilegeType].get(`${appId}-${privilegeName}`); - } + const endpointPrivileges = computeUiApiPrivileges(security, ENDPOINT_PRIVILEGES); + const endpointExceptionsPrivileges = computeUiApiPrivileges( + security, + ENDPOINT_EXCEPTIONS_PRIVILEGES ); + const { privileges } = await checkPrivileges({ kibana: [ security.authz.actions.api.get(`${PLUGIN_ID}-all`), @@ -87,20 +96,27 @@ export async function getAuthzFromRequest(req: KibanaRequest): Promise { expect(authz[auth]).toBe(true); }); + it.each<[EndpointAuthzKeyList[number], string]>([ + ['canReadEndpointExceptions', 'showEndpointExceptions'], + ['canWriteEndpointExceptions', 'crudEndpointExceptions'], + ])('%s should be true if `endpointExceptionsPrivileges.%s` is `true`', (auth) => { + const authz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles); + expect(authz[auth]).toBe(true); + }); + it.each<[EndpointAuthzKeyList[number], string[]]>([ ['canWriteEndpointList', ['writeEndpointList']], ['canReadEndpointList', ['readEndpointList']], @@ -181,6 +189,20 @@ describe('Endpoint Authz service', () => { privileges.forEach((privilege) => { fleetAuthz.packagePrivileges!.endpoint.actions[privilege].executePackageAction = false; }); + + const authz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles); + expect(authz[auth]).toBe(false); + }); + + it.each<[EndpointAuthzKeyList[number], string[]]>([ + ['canReadEndpointExceptions', ['showEndpointExceptions']], + ['canWriteEndpointExceptions', ['crudEndpointExceptions']], + ])('%s should be false if `endpointExceptionsPrivileges.%s` is `false`', (auth, privileges) => { + privileges.forEach((privilege) => { + // @ts-ignore + fleetAuthz.endpointExceptionsPrivileges!.actions[privilege] = false; + }); + const authz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles); expect(authz[auth]).toBe(false); }); @@ -281,6 +303,8 @@ describe('Endpoint Authz service', () => { canReadBlocklist: false, canWriteEventFilters: false, canReadEventFilters: false, + canReadEndpointExceptions: false, + canWriteEndpointExceptions: false, }); }); }); diff --git a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts index c0c5ee06d5488..922e8c3cdd383 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts @@ -30,6 +30,13 @@ export function hasKibanaPrivilege( return fleetAuthz.packagePrivileges?.endpoint?.actions[privilege].executePackageAction ?? false; } +export function hasEndpointExceptionsPrivilege( + fleetAuthz: FleetAuthz, + privilege: 'showEndpointExceptions' | 'crudEndpointExceptions' +): boolean { + return fleetAuthz.endpointExceptionsPrivileges?.actions[privilege] ?? false; +} + /** * Used by both the server and the UI to generate the Authorization for access to Endpoint related * functionality @@ -84,6 +91,15 @@ export const calculateEndpointAuthz = ( const canWriteExecuteOperations = hasKibanaPrivilege(fleetAuthz, 'writeExecuteOperations'); + const canReadEndpointExceptions = hasEndpointExceptionsPrivilege( + fleetAuthz, + 'showEndpointExceptions' + ); + const canWriteEndpointExceptions = hasEndpointExceptionsPrivilege( + fleetAuthz, + 'crudEndpointExceptions' + ); + const authz: EndpointAuthz = { canWriteSecuritySolution, canReadSecuritySolution, @@ -123,6 +139,8 @@ export const calculateEndpointAuthz = ( canReadBlocklist, canWriteEventFilters, canReadEventFilters, + canReadEndpointExceptions, + canWriteEndpointExceptions, }; // Response console is only accessible when license is Enterprise and user has access to any @@ -172,5 +190,7 @@ export const getEndpointAuthzInitialState = (): EndpointAuthz => { canReadBlocklist: false, canWriteEventFilters: false, canReadEventFilters: false, + canReadEndpointExceptions: false, + canWriteEndpointExceptions: false, }; }; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/authz.ts b/x-pack/plugins/security_solution/common/endpoint/types/authz.ts index 23ed5cbe7c439..1ec8f84a87073 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/authz.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/authz.ts @@ -10,74 +10,79 @@ * used both on the client and server for consistency */ export interface EndpointAuthz { - /** if user has write permissions to the security solution app */ + /** If the user has write permissions to the security solution app */ canWriteSecuritySolution: boolean; - /** if user has read permissions to the security solution app */ + /** If the user has read permissions to the security solution app */ canReadSecuritySolution: boolean; - /** If user has permissions to access Fleet */ + /** If the user has permissions to access Fleet */ canAccessFleet: boolean; - /** If user has permissions to access Endpoint management (includes check to ensure they also have access to fleet) */ + /** If the user has permissions to access Endpoint management (includes check to ensure they also have access to fleet) */ canAccessEndpointManagement: boolean; - /** If user has permissions to access Actions Log management and also has a platinum license (used for endpoint details flyout) */ + /** If the user has permissions to access Actions Log management and also has a platinum license (used for endpoint details flyout) */ canAccessEndpointActionsLogManagement: boolean; - /** if user has permissions to create Artifacts by Policy */ + /** If the user has permissions to create Artifacts by Policy */ canCreateArtifactsByPolicy: boolean; - /** if user has write permissions to endpoint list */ + /** If the user has write permissions to endpoint list */ canWriteEndpointList: boolean; - /** if user has read permissions to endpoint list */ + /** If the user has read permissions to endpoint list */ canReadEndpointList: boolean; - /** if user has write permissions for policy management */ + /** If the user has write permissions for policy management */ canWritePolicyManagement: boolean; - /** if user has read permissions for policy management */ + /** If the user has read permissions for policy management */ canReadPolicyManagement: boolean; - /** if user has write permissions for actions log management */ + /** If the user has write permissions for actions log management */ canWriteActionsLogManagement: boolean; - /** if user has read permissions for actions log management */ + /** If the user has read permissions for actions log management */ canReadActionsLogManagement: boolean; - /** If user has permissions to isolate hosts */ + /** If the user has permissions to isolate hosts */ canIsolateHost: boolean; - /** If user has permissions to un-isolate (release) hosts */ + /** If the user has permissions to un-isolate (release) hosts */ canUnIsolateHost: boolean; - /** If user has permissions to kill process on hosts */ + /** If the user has permissions to kill process on hosts */ canKillProcess: boolean; - /** If user has permissions to suspend process on hosts */ + /** If the user has permissions to suspend process on hosts */ canSuspendProcess: boolean; - /** If user has permissions to get running processes on hosts */ + /** If the user has permissions to get running processes on hosts */ canGetRunningProcesses: boolean; - /** If user has permissions to use the Response Actions Console */ + /** If the user has permissions to use the Response Actions Console */ canAccessResponseConsole: boolean; - /** If user has write permissions to use execute action */ + /** If the user has write permissions to use execute action */ canWriteExecuteOperations: boolean; - /** If user has write permissions to use file operations */ + /** If the user has write permissions to use file operations */ canWriteFileOperations: boolean; - /** if user has write permissions for trusted applications */ + /** If the user has write permissions for trusted applications */ canWriteTrustedApplications: boolean; - /** if user has read permissions for trusted applications */ + /** If the user has read permissions for trusted applications */ canReadTrustedApplications: boolean; - /** if user has write permissions for host isolation exceptions */ + /** If the user has write permissions for host isolation exceptions */ canWriteHostIsolationExceptions: boolean; - /** if user has read permissions for host isolation exceptions */ + /** If the user has read permissions for host isolation exceptions */ canReadHostIsolationExceptions: boolean; /** - * if user has permissions to access host isolation exceptions. This could be set to false, while + * If the user has permissions to access host isolation exceptions. This could be set to false, while * `canReadHostIsolationExceptions` is true in cases where the license might have been downgraded. * It is used to show the UI elements that allow users to navigate to the host isolation exceptions. */ canAccessHostIsolationExceptions: boolean; /** - * if user has permissions to delete host isolation exceptions. This could be set to true, while + * If the user has permissions to delete host isolation exceptions. This could be set to true, while * `canWriteHostIsolationExceptions` is false in cases where the license might have been downgraded. * In that use case, users should still be allowed to ONLY delete entries. */ canDeleteHostIsolationExceptions: boolean; - /** if user has write permissions for blocklist entries */ + /** If the user has write permissions for blocklist entries */ canWriteBlocklist: boolean; - /** if user has read permissions for blocklist entries */ + /** If the user has read permissions for blocklist entries */ canReadBlocklist: boolean; - /** if user has write permissions for event filters */ + /** If the user has write permissions for event filters */ canWriteEventFilters: boolean; - /** if user has read permissions for event filters */ + /** If the user has read permissions for event filters */ canReadEventFilters: boolean; + + /** if the user has write permissions for endpoint exceptions */ + canReadEndpointExceptions: boolean; + /** if the user has read permissions for endpoint exceptions */ + canWriteEndpointExceptions: boolean; } export type EndpointAuthzKeyList = Array; diff --git a/x-pack/plugins/security_solution/public/detection_engine/endpoint_exceptions/endpoint_exceptions_viewer.tsx b/x-pack/plugins/security_solution/public/detection_engine/endpoint_exceptions/endpoint_exceptions_viewer.tsx new file mode 100644 index 0000000000000..5ee61735a4b89 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/endpoint_exceptions/endpoint_exceptions_viewer.tsx @@ -0,0 +1,54 @@ +/* + * 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 React, { memo } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; +import type { Rule } from '../rule_management/logic'; +import { useGetEndpointExceptionsUnavailableComponent } from './use_get_endpoint_exceptions_unavailablle_component'; +import { ExceptionsViewer } from '../rule_exceptions/components/all_exception_items_table'; + +const RULE_ENDPOINT_EXCEPTION_LIST_TYPE = [ExceptionListTypeEnum.ENDPOINT]; + +interface EndpointExceptionsViewerProps { + isViewReadOnly: boolean; + onRuleChange: () => void; + rule: Rule | null; + 'data-test-subj': string; +} + +export const EndpointExceptionsViewer = memo( + ({ + isViewReadOnly, + onRuleChange, + rule, + 'data-test-subj': dataTestSubj, + }: EndpointExceptionsViewerProps) => { + const EndpointExceptionsUnavailableComponent = useGetEndpointExceptionsUnavailableComponent(); + return ( + <> + {!EndpointExceptionsUnavailableComponent ? ( + + ) : ( + + + + + + )} + + ); + } +); + +EndpointExceptionsViewer.displayName = 'EndpointExceptionsViewer'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/endpoint_exceptions/use_get_endpoint_exceptions_unavailablle_component.tsx b/x-pack/plugins/security_solution/public/detection_engine/endpoint_exceptions/use_get_endpoint_exceptions_unavailablle_component.tsx new file mode 100644 index 0000000000000..a3b0afb2caba5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/endpoint_exceptions/use_get_endpoint_exceptions_unavailablle_component.tsx @@ -0,0 +1,13 @@ +/* + * 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 type React from 'react'; +import { useUpsellingComponent } from '../../common/hooks/use_upselling'; + +export const useGetEndpointExceptionsUnavailableComponent = (): React.ComponentType | null => { + return useUpsellingComponent('ruleDetailsEndpointExceptions'); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx index 97f02b8e35cf2..223593ef3e095 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx @@ -19,7 +19,7 @@ import { EuiWindowEvent, } from '@elastic/eui'; import type { Filter } from '@kbn/es-query'; -import { Routes, Route } from '@kbn/shared-ux-router'; +import { Route, Routes } from '@kbn/shared-ux-router'; import { noop } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; @@ -32,12 +32,13 @@ import type { Dispatch } from 'redux'; import { isTab } from '@kbn/timelines-plugin/public'; import { - tableDefaults, dataTableActions, dataTableSelectors, FILTER_OPEN, + tableDefaults, TableId, } from '@kbn/securitysolution-data-table'; +import { EndpointExceptionsViewer } from '../../../endpoint_exceptions/endpoint_exceptions_viewer'; import { AlertsTableComponent } from '../../../../detections/components/alerts_table'; import { GroupedAlertsTable } from '../../../../detections/components/alerts_table/alerts_grouping'; import { useDataTableFilters } from '../../../../common/hooks/use_data_table_filters'; @@ -100,10 +101,10 @@ import { import { useSourcererDataView } from '../../../../common/containers/sourcerer'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; import { - explainLackOfPermission, canEditRuleWithActions, - isBoolean, + explainLackOfPermission, hasUserCRUDPermission, + isBoolean, } from '../../../../common/utils/privileges'; import { @@ -149,8 +150,6 @@ const RULE_EXCEPTION_LIST_TYPES = [ ExceptionListTypeEnum.RULE_DEFAULT, ]; -const RULE_ENDPOINT_EXCEPTION_LIST_TYPE = [ExceptionListTypeEnum.ENDPOINT]; - /** * Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space. */ @@ -780,9 +779,8 @@ const RuleDetailsPageComponent: React.FC = ({ - { beforeAll(() => { - (useRuleExecutionSettings as jest.Mock).mockReturnValue({ + mockUseRuleExecutionSettings.mockReturnValue({ extendedLogging: { isEnabled: false, minLevel: 'debug', }, }); + mockUseEndpointExceptionsCapability.mockReturnValue(true); }); beforeEach(() => { @@ -119,6 +125,32 @@ describe('useRuleDetailsTabs', () => { expect(tabsNames).toContain(RuleDetailTabs.endpointExceptions); }); + it('hides endpoint exceptions tab when rule includes endpoint list but no endpoint PLI', async () => { + mockUseEndpointExceptionsCapability.mockReturnValue(false); + const tabs = render({ + rule: { + ...mockRule, + outcome: 'conflict', + alias_target_id: 'aliased_rule_id', + alias_purpose: 'savedObjectConversion', + exceptions_list: [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + type: 'endpoint', + namespace_type: 'agnostic', + }, + ], + }, + ruleId: mockRule.rule_id, + isExistingRule: true, + hasIndexRead: true, + }); + const tabsNames = Object.keys(tabs.result.current); + + expect(tabsNames).not.toContain(RuleDetailTabs.endpointExceptions); + }); + it('does not return the execution events tab if extended logging is disabled', async () => { const tabs = render({ rule: mockRule, @@ -132,7 +164,7 @@ describe('useRuleDetailsTabs', () => { }); it('returns the execution events tab if extended logging is enabled', async () => { - (useRuleExecutionSettings as jest.Mock).mockReturnValue({ + mockUseRuleExecutionSettings.mockReturnValue({ extendedLogging: { isEnabled: true, minLevel: 'debug', diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/use_rule_details_tabs.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/use_rule_details_tabs.tsx index 42a42caeb732d..8c73bafd049aa 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/use_rule_details_tabs.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/use_rule_details_tabs.tsx @@ -8,6 +8,7 @@ import { useEffect, useMemo, useState } from 'react'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { omit } from 'lodash/fp'; +import { useEndpointExceptionsCapability } from '../../../../exceptions/hooks/use_endpoint_exceptions_capability'; import * as detectionI18n from '../../../../detections/pages/detection_engine/translations'; import * as i18n from './translations'; import type { Rule } from '../../../rule_management/logic'; @@ -80,9 +81,10 @@ export const useRuleDetailsTabs = ({ ); const [pageTabs, setTabs] = useState>>(ruleDetailTabs); - const ruleExecutionSettings = useRuleExecutionSettings(); + const canReadEndpointExceptions = useEndpointExceptionsCapability('showEndpointExceptions'); + useEffect(() => { const hiddenTabs = []; @@ -92,6 +94,9 @@ export const useRuleDetailsTabs = ({ if (!ruleExecutionSettings.extendedLogging.isEnabled) { hiddenTabs.push(RuleDetailTabs.executionEvents); } + if (!canReadEndpointExceptions) { + hiddenTabs.push(RuleDetailTabs.endpointExceptions); + } if (rule != null) { const hasEndpointList = (rule.exceptions_list ?? []).some( (list) => list.type === ExceptionListTypeEnum.ENDPOINT @@ -104,7 +109,7 @@ export const useRuleDetailsTabs = ({ const tabs = omit>(hiddenTabs, ruleDetailTabs); setTabs(tabs); - }, [hasIndexRead, rule, ruleDetailTabs, ruleExecutionSettings]); + }, [canReadEndpointExceptions, hasIndexRead, rule, ruleDetailTabs, ruleExecutionSettings]); return pageTabs; }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.test.tsx index 1d1dd112da377..05e6e0513f741 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.test.tsx @@ -19,7 +19,9 @@ import type { Rule } from '../../../rule_management/logic/types'; import { mockRule } from '../../../rule_management_ui/components/rules_table/__mocks__/mock'; import { useFindExceptionListReferences } from '../../logic/use_find_references'; import * as i18n from './translations'; +import { useEndpointExceptionsCapability } from '../../../../exceptions/hooks/use_endpoint_exceptions_capability'; +jest.mock('../../../../exceptions/hooks/use_endpoint_exceptions_capability'); jest.mock('../../../../common/lib/kibana'); jest.mock('@kbn/securitysolution-list-hooks'); jest.mock('@kbn/securitysolution-list-api'); @@ -29,6 +31,8 @@ jest.mock('react', () => { return { ...r, useReducer: jest.fn() }; }); +const mockUseEndpointExceptionsCapability = useEndpointExceptionsCapability as jest.Mock; + const sampleExceptionItem = { _version: 'WzEwMjM4MSwxXQ==', comments: [], @@ -81,6 +85,8 @@ describe('ExceptionsViewer', () => { }, }); + mockUseEndpointExceptionsCapability.mockReturnValue(true); + (fetchExceptionListsItemsByListIds as jest.Mock).mockReturnValue({ total: 0 }); (useFindExceptionListReferences as jest.Mock).mockReturnValue([ diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx index df6a6c2835a1d..cc86bbf02291b 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx @@ -5,18 +5,18 @@ * 2.0. */ -import React, { useCallback, useMemo, useEffect, useReducer } from 'react'; +import React, { useCallback, useEffect, useMemo, useReducer } from 'react'; import styled from 'styled-components'; import { EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; -import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import type { ExceptionListItemSchema, - UseExceptionListItemsSuccess, - Pagination, ExceptionListSchema, + Pagination, + UseExceptionListItemsSuccess, } from '@kbn/securitysolution-io-ts-list-types'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { transformInput } from '@kbn/securitysolution-list-hooks'; import { @@ -29,6 +29,7 @@ import { buildShowExpiredExceptionsFilter, getSavedObjectTypes, } from '@kbn/securitysolution-list-utils'; +import { useEndpointExceptionsCapability } from '../../../../exceptions/hooks/use_endpoint_exceptions_capability'; import { useUserData } from '../../../../detections/components/user_info'; import { useKibana, useToasts } from '../../../../common/lib/kibana'; import { ExceptionsViewerSearchBar } from './search_bar'; @@ -120,6 +121,8 @@ const ExceptionsViewerComponent = ({ [listTypes] ); + const canWriteEndpointExceptions = useEndpointExceptionsCapability('crudEndpointExceptions'); + // Reducer state const [ { @@ -531,7 +534,7 @@ const ExceptionsViewerComponent = ({ !listsConfigLoading && !needsListsConfiguration && canReadEndpointExceptions, - [canReadEndpointExceptions, listsConfigLoading, needsListsConfiguration] - ); + const canWriteEndpointExceptions = useEndpointExceptionsCapability('crudEndpointExceptions'); // Endpoint exceptions are available for: // Serverless Endpoint Essentials/Complete PLI and // on ESS Security Kibana sub-feature Endpoint Exceptions (enabled when Security feature is enabled) diff --git a/x-pack/plugins/security_solution/public/exceptions/components/list_exception_items/index.tsx b/x-pack/plugins/security_solution/public/exceptions/components/list_exception_items/index.tsx index 1b1b6ad36d1ea..990fd818505b7 100644 --- a/x-pack/plugins/security_solution/public/exceptions/components/list_exception_items/index.tsx +++ b/x-pack/plugins/security_solution/public/exceptions/components/list_exception_items/index.tsx @@ -4,8 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useMemo } from 'react'; import type { FC } from 'react'; +import React, { useMemo } from 'react'; import type { ExceptionListItemIdentifiers, GetExceptionItemProps, @@ -13,8 +13,8 @@ import type { ViewerStatus, } from '@kbn/securitysolution-exception-list-components'; import { ExceptionItems } from '@kbn/securitysolution-exception-list-components'; -import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import type { Pagination } from '@elastic/eui'; import { FormattedDate } from '../../../common/components/formatted_date'; @@ -22,6 +22,7 @@ import { getFormattedComments } from '../../utils/ui.helpers'; import { LinkToRuleDetails } from '../link_to_rule_details'; import { ExceptionsUtility } from '../exceptions_utility'; import * as i18n from '../../translations/list_exception_items'; +import { useEndpointExceptionsCapability } from '../../hooks/use_endpoint_exceptions_capability'; interface ListExceptionItemsProps { isReadOnly: boolean; @@ -58,6 +59,8 @@ const ListExceptionItemsComponent: FC = ({ onPaginationChange, onCreateExceptionListItem, }) => { + const canWriteEndpointExceptions = useEndpointExceptionsCapability('crudEndpointExceptions'); + const editButtonText = useMemo(() => { return listType === ExceptionListTypeEnum.ENDPOINT ? i18n.EXCEPTION_ITEM_CARD_EDIT_ENDPOINT_LABEL @@ -76,7 +79,7 @@ const ListExceptionItemsComponent: FC = ({ viewerStatus={viewerStatus as ViewerStatus} listType={listType as ExceptionListTypeEnum} ruleReferences={ruleReferences} - isReadOnly={isReadOnly} + isReadOnly={isReadOnly || !canWriteEndpointExceptions} exceptions={exceptions} emptyViewerTitle={emptyViewerTitle} emptyViewerBody={emptyViewerBody} diff --git a/x-pack/plugins/security_solution/public/exceptions/hooks/use_endpoint_exceptions_capability/index.tsx b/x-pack/plugins/security_solution/public/exceptions/hooks/use_endpoint_exceptions_capability/index.tsx new file mode 100644 index 0000000000000..a96b11225e659 --- /dev/null +++ b/x-pack/plugins/security_solution/public/exceptions/hooks/use_endpoint_exceptions_capability/index.tsx @@ -0,0 +1,23 @@ +/* + * 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 { useMemo } from 'react'; +import { useListsConfig } from '../../../detections/containers/detection_engine/lists/use_lists_config'; +import { useHasSecurityCapability } from '../../../helper_hooks'; + +export const useEndpointExceptionsCapability = ( + capability: 'showEndpointExceptions' | 'crudEndpointExceptions' +) => { + const { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration } = + useListsConfig(); + const hasEndpointExceptionCapability = useHasSecurityCapability(capability); + + return useMemo( + () => !listsConfigLoading && !needsListsConfiguration && hasEndpointExceptionCapability, + [hasEndpointExceptionCapability, listsConfigLoading, needsListsConfiguration] + ); +}; diff --git a/x-pack/plugins/security_solution/public/exceptions/pages/shared_lists/index.tsx b/x-pack/plugins/security_solution/public/exceptions/pages/shared_lists/index.tsx index 0e457d520984c..3c0fab0343a36 100644 --- a/x-pack/plugins/security_solution/public/exceptions/pages/shared_lists/index.tsx +++ b/x-pack/plugins/security_solution/public/exceptions/pages/shared_lists/index.tsx @@ -28,7 +28,6 @@ import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { useApi, useExceptionLists } from '@kbn/securitysolution-list-hooks'; import { EmptyViewerState, ViewerStatus } from '@kbn/securitysolution-exception-list-components'; -import { useHasSecurityCapability } from '../../../helper_hooks'; import { AutoDownload } from '../../../common/components/auto_download/auto_download'; import { useKibana } from '../../../common/lib/kibana'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; @@ -52,6 +51,7 @@ import { MissingPrivilegesCallOut } from '../../../detections/components/callout import { ALL_ENDPOINT_ARTIFACT_LIST_IDS } from '../../../../common/endpoint/service/artifacts/constants'; import { AddExceptionFlyout } from '../../../detection_engine/rule_exceptions/components/add_exception_flyout'; +import { useEndpointExceptionsCapability } from '../../hooks/use_endpoint_exceptions_capability'; export type Func = () => Promise; @@ -82,15 +82,10 @@ const SORT_FIELDS: Array<{ field: string; label: string; defaultOrder: 'asc' | ' export const SharedLists = React.memo(() => { const [{ loading: userInfoLoading, canUserCRUD, canUserREAD }] = useUserData(); - const { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration } = - useListsConfig(); + const { loading: listsConfigLoading } = useListsConfig(); const loading = userInfoLoading || listsConfigLoading; - const canShowEndpointExceptions = useHasSecurityCapability('showEndpointExceptions'); - const canAccessEndpointExceptions = useMemo( - () => !listsConfigLoading && !needsListsConfiguration && canShowEndpointExceptions, - [canShowEndpointExceptions, listsConfigLoading, needsListsConfiguration] - ); + const canAccessEndpointExceptions = useEndpointExceptionsCapability('showEndpointExceptions'); const { services: { http, diff --git a/x-pack/plugins/security_solution/public/exceptions/routes.tsx b/x-pack/plugins/security_solution/public/exceptions/routes.tsx index 747a96cb72eea..e6faf794dcdbf 100644 --- a/x-pack/plugins/security_solution/public/exceptions/routes.tsx +++ b/x-pack/plugins/security_solution/public/exceptions/routes.tsx @@ -5,21 +5,22 @@ * 2.0. */ import React from 'react'; -import { Routes, Route } from '@kbn/shared-ux-router'; +import { Route, Routes } from '@kbn/shared-ux-router'; import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; import * as i18n from './translations'; import { + EXCEPTION_LIST_DETAIL_PATH, EXCEPTIONS_PATH, SecurityPageName, - EXCEPTION_LIST_DETAIL_PATH, } from '../../common/constants'; -import { SharedLists, ListsDetailView } from './pages'; +import { ListsDetailView, SharedLists } from './pages'; import { SpyRoute } from '../common/utils/route/spy_routes'; import { NotFoundPage } from '../app/404'; import { useReadonlyHeader } from '../use_readonly_header'; import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper'; +import { SecurityRoutePageWrapper } from '../common/components/security_route_page_wrapper'; const ExceptionsRoutes = () => ( @@ -32,9 +33,9 @@ const ExceptionsRoutes = () => ( const ExceptionsListDetailRoute = () => ( - + - + ); diff --git a/x-pack/plugins/security_solution/public/rules/links.ts b/x-pack/plugins/security_solution/public/rules/links.ts index 23334456ac04f..e50a9aa670812 100644 --- a/x-pack/plugins/security_solution/public/rules/links.ts +++ b/x-pack/plugins/security_solution/public/rules/links.ts @@ -7,13 +7,13 @@ import { i18n } from '@kbn/i18n'; import { - RULES_PATH, - RULES_CREATE_PATH, + COVERAGE_OVERVIEW_PATH, EXCEPTIONS_PATH, - RULES_LANDING_PATH, RULES_ADD_PATH, + RULES_CREATE_PATH, + RULES_LANDING_PATH, + RULES_PATH, SERVER_APP_ID, - COVERAGE_OVERVIEW_PATH, } from '../../common/constants'; import { ADD_RULES, @@ -78,6 +78,7 @@ export const links: LinkItem = { }), landingIcon: IconConsoleCloud, path: EXCEPTIONS_PATH, + capabilities: [`${SERVER_APP_ID}.showEndpointExceptions`], skipUrlState: true, hideTimeline: true, globalSearchKeywords: [ diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index 917d366354270..3fb28c1c5099d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -6,27 +6,27 @@ */ import type { + ElasticsearchClient, KibanaRequest, Logger, - ElasticsearchClient, SavedObjectsClientContract, } from '@kbn/core/server'; import type { ExceptionListClient, ListsServerExtensionRegistrar } from '@kbn/lists-plugin/server'; import type { CasesClient, CasesStart } from '@kbn/cases-plugin/server'; import type { SecurityPluginStart } from '@kbn/security-plugin/server'; import type { + FleetFromHostFileClientInterface, FleetStartContract, MessageSigningServiceInterface, - FleetFromHostFileClientInterface, } from '@kbn/fleet-plugin/server'; import type { PluginStartContract as AlertsPluginStartContract } from '@kbn/alerting-plugin/server'; import type { CloudSetup } from '@kbn/cloud-plugin/server'; import type { FleetActionsClientInterface } from '@kbn/fleet-plugin/server/services/actions/types'; import { getPackagePolicyCreateCallback, - getPackagePolicyUpdateCallback, getPackagePolicyDeleteCallback, getPackagePolicyPostCreateCallback, + getPackagePolicyUpdateCallback, } from '../fleet_integration/fleet_integration'; import type { ManifestManager } from './services/artifacts'; import type { ConfigType } from '../config'; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/feature_usage/service.ts b/x-pack/plugins/security_solution/server/endpoint/services/feature_usage/service.ts index f736fffffcf17..ffebaac4420fc 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/feature_usage/service.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/feature_usage/service.ts @@ -24,6 +24,7 @@ const FEATURES = { GET_FILE: 'Get file', EXECUTE: 'Execute command', ALERTS_BY_PROCESS_ANCESTRY: 'Get related alerts by process ancestry', + ENDPOINT_EXCEPTIONS: 'Endpoint exceptions', } as const; export type FeatureKeys = keyof typeof FEATURES; diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_create_handler.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_create_handler.ts index f87e845487a04..0e7a2db2d4e47 100644 --- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_create_handler.ts +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_create_handler.ts @@ -11,10 +11,11 @@ import type { } from '@kbn/lists-plugin/server'; import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services'; import { + BlocklistValidator, + EndpointExceptionsValidator, EventFilterValidator, - TrustedAppValidator, HostIsolationExceptionsValidator, - BlocklistValidator, + TrustedAppValidator, } from '../validators'; type ValidatorCallback = ExceptionsListPreCreateItemServerExtension['callback']; @@ -65,6 +66,17 @@ export const getExceptionsPreCreateItemHandler = ( return validatedItem; } + // validate endpoint exceptions + if (EndpointExceptionsValidator.isEndpointException(data)) { + const endpointExceptionValidator = new EndpointExceptionsValidator( + endpointAppContext, + request + ); + const validatedItem = await endpointExceptionValidator.validatePreCreateItem(data); + endpointExceptionValidator.notifyFeatureUsage(data, 'ENDPOINT_EXCEPTIONS'); + return validatedItem; + } + return data; }; }; diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_delete_item_handler.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_delete_item_handler.ts index 66a9b4709bd2b..719cd7b655b22 100644 --- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_delete_item_handler.ts +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_delete_item_handler.ts @@ -9,10 +9,11 @@ import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-t import type { ExceptionsListPreDeleteItemServerExtension } from '@kbn/lists-plugin/server'; import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services'; import { - TrustedAppValidator, - HostIsolationExceptionsValidator, - EventFilterValidator, BlocklistValidator, + EndpointExceptionsValidator, + EventFilterValidator, + HostIsolationExceptionsValidator, + TrustedAppValidator, } from '../validators'; type ValidatorCallback = ExceptionsListPreDeleteItemServerExtension['callback']; @@ -64,6 +65,15 @@ export const getExceptionsPreDeleteItemHandler = ( return data; } + // Validate Endpoint Exceptions + if (EndpointExceptionsValidator.isEndpointException({ listId })) { + await new EndpointExceptionsValidator( + endpointAppContextService, + request + ).validatePreDeleteItem(); + return data; + } + return data; }; }; diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_export_handler.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_export_handler.ts index 7dac876b34f22..47ed89ce19982 100644 --- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_export_handler.ts +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_export_handler.ts @@ -8,10 +8,11 @@ import type { ExceptionsListPreExportServerExtension } from '@kbn/lists-plugin/server'; import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services'; import { - TrustedAppValidator, - HostIsolationExceptionsValidator, - EventFilterValidator, BlocklistValidator, + EndpointExceptionsValidator, + EventFilterValidator, + HostIsolationExceptionsValidator, + TrustedAppValidator, } from '../validators'; type ValidatorCallback = ExceptionsListPreExportServerExtension['callback']; @@ -61,6 +62,12 @@ export const getExceptionsPreExportHandler = ( return data; } + // Validate Endpoint Exceptions + if (EndpointExceptionsValidator.isEndpointException({ listId })) { + await new EndpointExceptionsValidator(endpointAppContextService, request).validatePreExport(); + return data; + } + return data; }; }; diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_get_one_handler.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_get_one_handler.ts index 9ed81b1f0d585..15e9af5e61ac9 100644 --- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_get_one_handler.ts +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_get_one_handler.ts @@ -9,10 +9,11 @@ import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-t import type { ExceptionsListPreGetOneItemServerExtension } from '@kbn/lists-plugin/server'; import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services'; import { - TrustedAppValidator, - HostIsolationExceptionsValidator, - EventFilterValidator, BlocklistValidator, + EndpointExceptionsValidator, + EventFilterValidator, + HostIsolationExceptionsValidator, + TrustedAppValidator, } from '../validators'; type ValidatorCallback = ExceptionsListPreGetOneItemServerExtension['callback']; @@ -64,6 +65,15 @@ export const getExceptionsPreGetOneHandler = ( return data; } + // Validate Endpoint Exceptions + if (EndpointExceptionsValidator.isEndpointException({ listId })) { + await new EndpointExceptionsValidator( + endpointAppContextService, + request + ).validatePreGetOneItem(); + return data; + } + return data; }; }; diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_multi_list_find_handler.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_multi_list_find_handler.ts index 973bb6ce5072a..e3e66daebc57d 100644 --- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_multi_list_find_handler.ts +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_multi_list_find_handler.ts @@ -8,10 +8,11 @@ import type { ExceptionsListPreMultiListFindServerExtension } from '@kbn/lists-plugin/server'; import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services'; import { - TrustedAppValidator, - HostIsolationExceptionsValidator, - EventFilterValidator, BlocklistValidator, + EndpointExceptionsValidator, + EventFilterValidator, + HostIsolationExceptionsValidator, + TrustedAppValidator, } from '../validators'; type ValidatorCallback = ExceptionsListPreMultiListFindServerExtension['callback']; @@ -54,6 +55,15 @@ export const getExceptionsPreMultiListFindHandler = ( return data; } + // Validate Endpoint Exceptions + if (data.listId.some((id) => EndpointExceptionsValidator.isEndpointException({ listId: id }))) { + await new EndpointExceptionsValidator( + endpointAppContextService, + request + ).validatePreMultiListFind(); + return data; + } + return data; }; }; diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_single_list_find_handler.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_single_list_find_handler.ts index 946a0bf6d7c43..e647ea2a710f5 100644 --- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_single_list_find_handler.ts +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_single_list_find_handler.ts @@ -8,10 +8,11 @@ import type { ExceptionsListPreSingleListFindServerExtension } from '@kbn/lists-plugin/server'; import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services'; import { - TrustedAppValidator, - HostIsolationExceptionsValidator, - EventFilterValidator, BlocklistValidator, + EndpointExceptionsValidator, + EventFilterValidator, + HostIsolationExceptionsValidator, + TrustedAppValidator, } from '../validators'; type ValidatorCallback = ExceptionsListPreSingleListFindServerExtension['callback']; @@ -55,6 +56,15 @@ export const getExceptionsPreSingleListFindHandler = ( return data; } + // Validate Endpoint Exceptions + if (EndpointExceptionsValidator.isEndpointException({ listId })) { + await new EndpointExceptionsValidator( + endpointAppContextService, + request + ).validatePreSingleListFind(); + return data; + } + return data; }; }; diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_summary_handler.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_summary_handler.ts index 6b9af37f877ab..d50504572b369 100644 --- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_summary_handler.ts +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_summary_handler.ts @@ -8,10 +8,11 @@ import type { ExceptionsListPreSummaryServerExtension } from '@kbn/lists-plugin/server'; import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services'; import { - TrustedAppValidator, - HostIsolationExceptionsValidator, - EventFilterValidator, BlocklistValidator, + EndpointExceptionsValidator, + EventFilterValidator, + HostIsolationExceptionsValidator, + TrustedAppValidator, } from '../validators'; type ValidatorCallback = ExceptionsListPreSummaryServerExtension['callback']; @@ -61,6 +62,15 @@ export const getExceptionsPreSummaryHandler = ( return data; } + // Validate Endpoint Exceptions + if (EndpointExceptionsValidator.isEndpointException({ listId })) { + await new EndpointExceptionsValidator( + endpointAppContextService, + request + ).validatePreGetListSummary(); + return data; + } + return data; }; }; diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_update_handler.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_update_handler.ts index 681d16a1e44b8..810b569ecf8a6 100644 --- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_update_handler.ts +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_update_handler.ts @@ -12,10 +12,11 @@ import type { import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services'; import type { ExceptionItemLikeOptions } from '../types'; import { + BlocklistValidator, + EndpointExceptionsValidator, EventFilterValidator, - TrustedAppValidator, HostIsolationExceptionsValidator, - BlocklistValidator, + TrustedAppValidator, } from '../validators'; type ValidatorCallback = ExceptionsListPreUpdateItemServerExtension['callback']; @@ -98,6 +99,20 @@ export const getExceptionsPreUpdateItemHandler = ( return validatedItem; } + // Validate Endpoint Exceptions + if (EndpointExceptionsValidator.isEndpointException({ listId })) { + const endpointExceptionValidator = new EndpointExceptionsValidator( + endpointAppContextService, + request + ); + const validatedItem = await endpointExceptionValidator.validatePreUpdateItem(data); + endpointExceptionValidator.notifyFeatureUsage( + data as ExceptionItemLikeOptions, + 'ENDPOINT_EXCEPTIONS' + ); + return validatedItem; + } + return data; }; }; diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.ts index ecc2ac7893336..4630ad9edec07 100644 --- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.ts +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.ts @@ -10,6 +10,7 @@ import { schema } from '@kbn/config-schema'; import { isEqual } from 'lodash/fp'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import { OperatingSystem } from '@kbn/securitysolution-utils'; + import type { EndpointAuthz } from '../../../../common/endpoint/types/authz'; import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services'; import type { ExceptionItemLikeOptions } from '../types'; @@ -19,6 +20,7 @@ import { isArtifactByPolicy, } from '../../../../common/endpoint/service/artifacts'; import { EndpointArtifactExceptionValidationError } from './errors'; +import { EndpointExceptionsValidationError } from './endpoint_exception_errors'; import type { FeatureKeys } from '../../../endpoint/services/feature_usage/service'; export const BasicEndpointExceptionDataSchema = schema.object( @@ -74,6 +76,14 @@ export class BaseValidator { } } + protected async validateHasEndpointExceptionsPrivileges( + privilege: keyof EndpointAuthz + ): Promise { + if (!(await this.endpointAuthzPromise)[privilege]) { + throw new EndpointExceptionsValidationError('Endpoint exceptions authorization failure', 403); + } + } + protected async validateHasPrivilege(privilege: keyof EndpointAuthz): Promise { if (!(await this.endpointAuthzPromise)[privilege]) { throw new EndpointArtifactExceptionValidationError('Endpoint authorization failure', 403); diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/endpoint_exception_errors.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/endpoint_exception_errors.ts new file mode 100644 index 0000000000000..d0f2ba0537b55 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/endpoint_exception_errors.ts @@ -0,0 +1,14 @@ +/* + * 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 { ListsErrorWithStatusCode } from '@kbn/lists-plugin/server'; + +export class EndpointExceptionsValidationError extends ListsErrorWithStatusCode { + constructor(message: string, statusCode: number = 400) { + super(`EndpointExceptionsError: ${message}`, statusCode); + } +} diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/endpoint_exceptions_validator.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/endpoint_exceptions_validator.ts new file mode 100644 index 0000000000000..23d1d28ba0a59 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/endpoint_exceptions_validator.ts @@ -0,0 +1,61 @@ +/* + * 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 type { + CreateExceptionListItemOptions, + UpdateExceptionListItemOptions, +} from '@kbn/lists-plugin/server'; +import { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants'; +import { BaseValidator } from './base_validator'; + +export class EndpointExceptionsValidator extends BaseValidator { + static isEndpointException(item: { listId: string }): boolean { + return item.listId === ENDPOINT_LIST_ID; + } + + protected async validateHasReadPrivilege(): Promise { + return this.validateHasEndpointExceptionsPrivileges('canReadEndpointExceptions'); + } + + protected async validateHasWritePrivilege(): Promise { + return this.validateHasEndpointExceptionsPrivileges('canWriteEndpointExceptions'); + } + + async validatePreCreateItem(item: CreateExceptionListItemOptions) { + await this.validateHasWritePrivilege(); + return item; + } + + async validatePreUpdateItem(item: UpdateExceptionListItemOptions) { + await this.validateHasWritePrivilege(); + return item; + } + + async validatePreDeleteItem(): Promise { + await this.validateHasWritePrivilege(); + } + + async validatePreGetOneItem(): Promise { + await this.validateHasReadPrivilege(); + } + + async validatePreMultiListFind(): Promise { + await this.validateHasReadPrivilege(); + } + + async validatePreExport(): Promise { + await this.validateHasReadPrivilege(); + } + + async validatePreSingleListFind(): Promise { + await this.validateHasReadPrivilege(); + } + + async validatePreGetListSummary(): Promise { + await this.validateHasReadPrivilege(); + } +} diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/index.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/index.ts index ccd6ebd8e08d6..8dd357b15ebd9 100644 --- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/index.ts +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/index.ts @@ -9,3 +9,4 @@ export { TrustedAppValidator } from './trusted_app_validator'; export { EventFilterValidator } from './event_filter_validator'; export { HostIsolationExceptionsValidator } from './host_isolation_exceptions_validator'; export { BlocklistValidator } from './blocklist_validator'; +export { EndpointExceptionsValidator } from './endpoint_exceptions_validator'; diff --git a/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts b/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts index 1ef3da121b910..62acff6857a8e 100644 --- a/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts +++ b/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts @@ -33,6 +33,7 @@ export const PLI_APP_FEATURES: PliAppFeatures = { complete: [ AppFeatureKey.endpointResponseActions, AppFeatureKey.osqueryAutomatedResponseActions, + AppFeatureKey.endpointExceptions, ], }, cloud: { diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/lazy_upselling.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/lazy_upselling.tsx index e24434ea0b9e8..71f787e19c3bd 100644 --- a/x-pack/plugins/security_solution_serverless/public/upselling/lazy_upselling.tsx +++ b/x-pack/plugins/security_solution_serverless/public/upselling/lazy_upselling.tsx @@ -27,6 +27,10 @@ export const OsqueryResponseActionsUpsellingSectionLazy = withSuspenseUpsell( lazy(() => import('./pages/osquery_automated_response_actions')) ); +export const EndpointExceptionsDetailsUpsellingLazy = withSuspenseUpsell( + lazy(() => import('./pages/endpoint_management/endpoint_exceptions_details')) +); + export const EntityAnalyticsUpsellingLazy = withSuspenseUpsell( lazy(() => import('@kbn/security-solution-upselling/pages/entity_analytics')) ); diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/pages/endpoint_management/endpoint_exceptions_details.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/pages/endpoint_management/endpoint_exceptions_details.tsx new file mode 100644 index 0000000000000..fd514350eaf0a --- /dev/null +++ b/x-pack/plugins/security_solution_serverless/public/upselling/pages/endpoint_management/endpoint_exceptions_details.tsx @@ -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 { EuiEmptyPrompt, EuiIcon } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React, { memo } from 'react'; +import type { AppFeatureKeyType } from '@kbn/security-solution-features/keys'; +import { getProductTypeByPLI } from '../../hooks/use_product_type_by_pli'; + +const EndpointExceptionsDetailsUpselling: React.FC<{ requiredPLI: AppFeatureKeyType }> = memo( + ({ requiredPLI }) => { + const productTypeRequired = getProductTypeByPLI(requiredPLI); + + return ( + } + color="subdued" + title={ +

+ +

+ } + body={ +

+ +

+ } + /> + ); + } +); + +EndpointExceptionsDetailsUpselling.displayName = 'EndpointExceptionsDetailsUpselling'; + +// eslint-disable-next-line import/no-default-export +export { EndpointExceptionsDetailsUpselling as default }; diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/pages/osquery_automated_response_actions.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/pages/osquery_automated_response_actions.tsx index 3097d41819058..b1abd3400797c 100644 --- a/x-pack/plugins/security_solution_serverless/public/upselling/pages/osquery_automated_response_actions.tsx +++ b/x-pack/plugins/security_solution_serverless/public/upselling/pages/osquery_automated_response_actions.tsx @@ -8,6 +8,7 @@ import { EuiEmptyPrompt, EuiIcon } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import React from 'react'; + import type { AppFeatureKeyType } from '@kbn/security-solution-features'; import { getProductTypeByPLI } from '../hooks/use_product_type_by_pli'; diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx index f1b8da6b1557d..4c9f9f65fc0a7 100644 --- a/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx +++ b/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx @@ -17,10 +17,14 @@ import React from 'react'; import { UPGRADE_INVESTIGATION_GUIDE } from '@kbn/security-solution-upselling/messages'; import { AppFeatureKey } from '@kbn/security-solution-features/keys'; import type { AppFeatureKeyType } from '@kbn/security-solution-features'; -import { EndpointPolicyProtectionsLazy } from './sections/endpoint_management'; +import { + EndpointPolicyProtectionsLazy, + RuleDetailsEndpointExceptionsLazy, +} from './sections/endpoint_management'; import type { SecurityProductTypes } from '../../common/config'; import { getProductAppFeatures } from '../../common/pli/pli_features'; import { + EndpointExceptionsDetailsUpsellingLazy, EntityAnalyticsUpsellingLazy, OsqueryResponseActionsUpsellingSectionLazy, ThreatIntelligencePaywallLazy, @@ -86,7 +90,7 @@ export const registerUpsellings = ( upselling.setMessages(upsellingMessagesToRegister); }; -// Upsellings for entire pages, linked to a SecurityPageName +// Upselling for entire pages, linked to a SecurityPageName export const upsellingPages: UpsellingPages = [ // It is highly advisable to make use of lazy loaded components to minimize bundle size. { @@ -105,9 +109,16 @@ export const upsellingPages: UpsellingPages = [ ), }, + { + pageName: SecurityPageName.exceptions, + pli: AppFeatureKey.endpointExceptions, + component: () => ( + + ), + }, ]; -// Upsellings for sections, linked by arbitrary ids +// Upselling for sections, linked by arbitrary ids export const upsellingSections: UpsellingSections = [ // It is highly advisable to make use of lazy loaded components to minimize bundle size. { @@ -124,9 +135,14 @@ export const upsellingSections: UpsellingSections = [ pli: AppFeatureKey.endpointPolicyProtections, component: EndpointPolicyProtectionsLazy, }, + { + id: 'ruleDetailsEndpointExceptions', + pli: AppFeatureKey.endpointExceptions, + component: RuleDetailsEndpointExceptionsLazy, + }, ]; -// Upsellings for sections, linked by arbitrary ids +// Upselling for sections, linked by arbitrary ids export const upsellingMessages: UpsellingMessages = [ { id: 'investigation_guide', diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/index.ts b/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/index.ts index a76b1cc0bacc8..e6db03492885e 100644 --- a/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/index.ts +++ b/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/index.ts @@ -12,3 +12,9 @@ export const EndpointPolicyProtectionsLazy = lazy(() => default: EndpointPolicyProtections, })) ); + +export const RuleDetailsEndpointExceptionsLazy = lazy(() => + import('./rule_details_endpoint_exceptions').then(({ RuleDetailsEndpointExceptions }) => ({ + default: RuleDetailsEndpointExceptions, + })) +); diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/rule_details_endpoint_exceptions.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/rule_details_endpoint_exceptions.tsx new file mode 100644 index 0000000000000..39bf086d7558b --- /dev/null +++ b/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/rule_details_endpoint_exceptions.tsx @@ -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 React, { memo } from 'react'; +import { EuiCard, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import styled from '@emotion/styled'; + +const BADGE_TEXT = i18n.translate( + 'xpack.securitySolutionServerless.rules.endpointSecurity.endpointExceptions.badgeText', + { + defaultMessage: 'Endpoint Essentials', + } +); +const CARD_TITLE = i18n.translate( + 'xpack.securitySolutionServerless.rules.endpointSecurity.endpointExceptions.cardTitle', + { + defaultMessage: 'Do more with Security!', + } +); +const CARD_MESSAGE = i18n.translate( + 'xpack.securitySolutionServerless.rules.endpointSecurity.endpointExceptions.cardMessage', + { + defaultMessage: + 'Upgrade your license to {productTypeRequired} to use Endpoint Security Exception List.', + values: { productTypeRequired: BADGE_TEXT }, + } +); + +const CardDescription = styled.p` + padding: 0 33.3%; +`; + +/** + * Component displayed trying to access endpoint exceptions tab on Endpoint security rule details. + */ +export const RuleDetailsEndpointExceptions = memo(() => { + return ( + } + betaBadgeProps={{ + 'data-test-subj': 'rules-endpointSecurity-endpointExceptionsLockedCard-badge', + label: BADGE_TEXT, + }} + title={ +

+ {CARD_TITLE} +

+ } + > + {CARD_MESSAGE} +
+ ); +}); +RuleDetailsEndpointExceptions.displayName = 'RuleDetailsEndpointExceptions';