[Serverless][Security Solution][Endpoint] Gate endpoint exceptions on rule details and API changes (#165613)

## What this PR changes

Follow up of elastic/kibana/pull/164107/

For serverless ES/Kibana, it gates exception list API for endpoint
exceptions and restricts endpoint exceptions tab on Endpoint Security
rule details based on project PLIs. If no endpoint PLIs, endpoint
exceptions should not be accessible.

- [x] Add upselling to `app/security/exceptions/details/endpoint_list`
page
- [ ] Tests (WIP) - in a follow up PR

### How to review

Best to follow along commits for a code review. Below are details to
manually test the changes.

- Setup for _Servlerless_
- Run `yarn es serverless --kill --clean --license trial -E
xpack.security.authc.api_key.enabled=true` on a terminal window to start
ES.
- Copy `config/serverless.security.yml` to
`config/serverless.security.dev.yml`
- Run `yarn serverless-security --no-base-path` on another terminal
window to start kibana in serverless mode
  - Log in using `serverless_security` user.

### Tests (Serverless)
This needs to be tested with a custom user/role and not
`elastic_serverless` which has `superuser` role.

1. ### PLI configs
`{ product_line: 'security', product_tier: 'essentials' }` or `{
product_line: 'security', product_tier: 'complete' }`
and
`{ product_line: 'endpoint', product_tier: 'essentials' }` or `{
product_line: 'endpoint', product_tier: 'complete' }`

- #### UX
1. Navigate to Rules via `http://localhost:5601/app/security/rules/`.
Click on `Add Elastic rules`.
  2. Select and add `Endpoint Security` rule.
3. Click `Endpoint Security` and navigate to the rules details page, and
you should see `Endpoint exceptions` tab. The tabs visible are `Alerts`,
`Endpoint exceptions`, `Rule exceptions`, `Execution results`.
4. Navigate to Rules>Shared Exception Lists > Endpoint Security
Exception List via `app/security/exceptions/details/endpoint_list` and
you should be able to see the page with any added endpoint exceptions.

- #### API requests (with user `serverless_security`)
  1. should get a status `200` on`POST api/exception_lists/items`
2. should get a status `200` on `POST
api/exception_lists/_export?id=endpoint_list&list_id=endpoint_list&namespace_type=agnostic&include_expired_exceptions=true`
  3. should get a status `200` on `PUT api/exception_lists/items`
  5. should get a status `200` on `DELETE api/exception_lists/items`
6. should get a status `200` on `GET
api/exception_lists/items/_find?list_id=endpoint_list&namespace_type=agnostic`

2. ### PLI configs
`{ product_line: 'security', product_tier: 'essentials' }` or `{
product_line: 'security', product_tier: 'complete' }`

- #### UX
1. Navigate to Rules via `http://localhost:5601/app/security/rules/`.
Click on `Add Elastic rules`.
  2. Select and add `Endpoint Security` rule. 
3. Click `Endpoint Security` and navigate to the rules details page, and
you should not see `Endpoint exceptions` tab. The only tabs visible are
`Alerts`, `Rule exceptions`, `Execution results`.
![Screenshot 2023-09-14 at 3 33 24
PM](185ea210-c457-4469-a824-cdcaa2893cb6)
4. Navigate to Rules>Shared Exception Lists > Endpoint Security
Exception List via `app/security/exceptions/details/endpoint_list` and
you should see an upsell message.
![Screenshot 2023-09-14 at 3 29 14
PM](6700fc2d-9a9d-4a57-ad7f-5505e02cec71)


- #### API requests
  1. should get a status `403` on`POST api/exception_lists/items`
2. should get a status `403` on `POST
api/exception_lists/_export?id=endpoint_list&list_id=endpoint_list&namespace_type=agnostic&include_expired_exceptions=true`
  3. should get a status `403` on `PUT api/exception_lists/items`
  6. should get a status `403` on `DELETE api/exception_lists/items`
7. should get a status `403` on `GET
api/exception_lists/items/_find?list_id=endpoint_list&namespace_type=agnostic`
---


**Flaky FTRs**
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3248
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3255


### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)


### Risk Matrix

Delete this section if it is not applicable to this PR.

Before closing this PR, invite QA, stakeholders, and other developers to
identify risks that should be tested prior to the change/feature
release.

When forming the risk matrix, consider some of the following examples
and how they may potentially impact the change:

| Risk | Probability | Severity | Mitigation/Notes |

|---------------------------|-------------|----------|-------------------------|
| Multiple Spaces—unexpected behavior in non-default Kibana Space.
| Low | High | Integration tests will verify that all features are still
supported in non-default Kibana Space and when user switches between
spaces. |
| Multiple nodes—Elasticsearch polling might have race conditions
when multiple Kibana nodes are polling for the same tasks. | High | Low
| Tasks are idempotent, so executing them multiple times will not result
in logical error, but will degrade performance. To test for this case we
add plenty of unit tests around this logic and document manual testing
procedure. |
| Code should gracefully handle cases when feature X or plugin Y are
disabled. | Medium | High | Unit tests will verify that any feature flag
or plugin combination still results in our service operational. |
| [See more potential risk
examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) |


### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ash 2023-10-02 15:52:12 +02:00 committed by GitHub
parent db6fa7317c
commit a8de031ddf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 920 additions and 193 deletions

2
.github/CODEOWNERS vendored
View file

@ -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)

View file

@ -14,6 +14,7 @@ export type SectionUpsellings = Partial<Record<UpsellingSectionId, React.Compone
export type UpsellingSectionId =
| 'entity_analytics_panel'
| 'endpointPolicyProtections'
| 'osquery_automated_response_actions';
| 'osquery_automated_response_actions'
| 'ruleDetailsEndpointExceptions';
export type UpsellingMessageId = 'investigation_guide';

View file

@ -10,8 +10,11 @@ import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common';
import { TRANSFORM_PLUGIN_ID } from './constants/plugin';
import {
calculateEndpointExceptionsPrivilegesFromCapabilities,
calculateEndpointExceptionsPrivilegesFromKibanaPrivileges,
calculatePackagePrivilegesFromCapabilities,
calculatePackagePrivilegesFromKibanaPrivileges,
getAuthorizationFromPrivileges,
} from './authz';
import { ENDPOINT_PRIVILEGES } from './constants';
@ -74,6 +77,56 @@ describe('fleet authz', () => {
});
});
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);
});
});
});

View file

@ -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<string, boolean>
>((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<PrivilegeMap>(
(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<string, boolean>
>((acc, [privilege, { appId, privilegeSplit, privilegeName }]) => {
acc[privilege] = getAuthorizationFromPrivileges({
kibanaPrivileges,
searchPrivilege: privilegeName,
});
return acc;
}, {});
return { actions: endpointExceptionsActions } as FleetAuthz['endpointExceptionsPrivileges'];
}

View file

@ -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<string, PrivilegeMapObject> = deepFreez
privilegeName: 'writeExecuteOperations',
},
});
export const ENDPOINT_EXCEPTIONS_PRIVILEGES: Record<string, PrivilegeMapObject> = 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',
},
});

View file

@ -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,
},
},
};
};

View file

@ -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';
import { setCustomIntegrations, setCustomIntegrationsStart } from './services/custom_integrations';
import { getFleetDeepLinks } from './deep_links';
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';
// 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<FleetSetup, FleetStart, FleetSetupDep
isSuperuser: false,
}),
packagePrivileges: calculatePackagePrivilegesFromCapabilities(capabilities),
endpointExceptionsPrivileges:
calculateEndpointExceptionsPrivilegesFromCapabilities(capabilities),
},
isInitialized: once(async () => {

View file

@ -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 {

View file

@ -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, PrivilegeMapObject>
): 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<FleetAuthz> {
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<FleetAuth
security.authz.actions.api.get(`${TRANSFORM_PLUGIN_ID}-read`),
...endpointPrivileges,
...endpointExceptionsPrivileges,
],
});
const fleetAllAuth = getAuthorizationFromPrivileges(privileges.kibana, `${PLUGIN_ID}-all`);
const intAllAuth = getAuthorizationFromPrivileges(
privileges.kibana,
`${INTEGRATIONS_PLUGIN_ID}-all`
);
const intReadAuth = getAuthorizationFromPrivileges(
privileges.kibana,
`${INTEGRATIONS_PLUGIN_ID}-read`
);
const fleetSetupAuth = getAuthorizationFromPrivileges(privileges.kibana, 'fleet-setup');
const fleetAllAuth = getAuthorizationFromPrivileges({
kibanaPrivileges: privileges.kibana,
prefix: `${PLUGIN_ID}-all`,
});
const intAllAuth = getAuthorizationFromPrivileges({
kibanaPrivileges: privileges.kibana,
prefix: `${INTEGRATIONS_PLUGIN_ID}-all`,
});
const intReadAuth = getAuthorizationFromPrivileges({
kibanaPrivileges: privileges.kibana,
prefix: `${INTEGRATIONS_PLUGIN_ID}-read`,
});
const fleetSetupAuth = getAuthorizationFromPrivileges({
kibanaPrivileges: privileges.kibana,
searchPrivilege: 'fleet-setup',
});
const authz = {
return {
...calculateAuthz({
fleet: { all: fleetAllAuth, setup: fleetSetupAuth },
integrations: {
@ -110,9 +126,10 @@ export async function getAuthzFromRequest(req: KibanaRequest): Promise<FleetAuth
isSuperuser: checkSuperuser(req),
}),
packagePrivileges: calculatePackagePrivilegesFromKibanaPrivileges(privileges.kibana),
endpointExceptionsPrivileges: calculateEndpointExceptionsPrivilegesFromKibanaPrivileges(
privileges.kibana
),
};
return authz;
}
return calculateAuthz({

View file

@ -11,8 +11,8 @@ import { createFleetAuthzMock } from '@kbn/fleet-plugin/common/mocks';
import { createLicenseServiceMock } from '../../../license/mocks';
import type { EndpointAuthzKeyList } from '../../types/authz';
import {
RESPONSE_CONSOLE_ACTION_COMMANDS_TO_RBAC_FEATURE_CONTROL,
CONSOLE_RESPONSE_ACTION_COMMANDS,
RESPONSE_CONSOLE_ACTION_COMMANDS_TO_RBAC_FEATURE_CONTROL,
type ResponseConsoleRbacControls,
} from '../response_actions/constants';
@ -150,6 +150,14 @@ describe('Endpoint Authz service', () => {
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,
});
});
});

View file

@ -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,
};
};

View file

@ -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<keyof EndpointAuthz>;

View file

@ -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 ? (
<ExceptionsViewer
rule={rule}
listTypes={RULE_ENDPOINT_EXCEPTION_LIST_TYPE}
onRuleChange={onRuleChange}
isViewReadOnly={isViewReadOnly}
data-test-subj={dataTestSubj}
/>
) : (
<EuiFlexGroup justifyContent="spaceAround">
<EuiFlexItem grow={false}>
<EndpointExceptionsUnavailableComponent />
</EuiFlexItem>
</EuiFlexGroup>
)}
</>
);
}
);
EndpointExceptionsViewer.displayName = 'EndpointExceptionsViewer';

View file

@ -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');
};

View file

@ -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<DetectionEngineComponentProps> = ({
<Route
path={`/rules/id/:detailName/:tabName(${RuleDetailTabs.endpointExceptions})`}
>
<ExceptionsViewer
<EndpointExceptionsViewer
rule={rule}
listTypes={RULE_ENDPOINT_EXCEPTION_LIST_TYPE}
onRuleChange={refreshRule}
isViewReadOnly={!isExistingRule}
data-test-subj="endpointExceptionsTab"

View file

@ -5,13 +5,18 @@
* 2.0.
*/
import { renderHook, cleanup } from '@testing-library/react-hooks';
import { cleanup, renderHook } from '@testing-library/react-hooks';
import type { UseRuleDetailsTabsProps } from './use_rule_details_tabs';
import { RuleDetailTabs, useRuleDetailsTabs } from './use_rule_details_tabs';
import type { Rule } from '../../../rule_management/logic';
import { useRuleExecutionSettings } from '../../../rule_monitoring';
import { useEndpointExceptionsCapability } from '../../../../exceptions/hooks/use_endpoint_exceptions_capability';
jest.mock('../../../rule_monitoring');
jest.mock('../../../../exceptions/hooks/use_endpoint_exceptions_capability');
const mockUseRuleExecutionSettings = useRuleExecutionSettings as jest.Mock;
const mockUseEndpointExceptionsCapability = useEndpointExceptionsCapability as jest.Mock;
const mockRule: Rule = {
id: 'myfakeruleid',
@ -51,12 +56,13 @@ const mockRule: Rule = {
describe('useRuleDetailsTabs', () => {
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',

View file

@ -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<Partial<Record<RuleDetailTabs, NavTab>>>(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<Record<RuleDetailTabs, NavTab>>(hiddenTabs, ruleDetailTabs);
setTabs(tabs);
}, [hasIndexRead, rule, ruleDetailTabs, ruleExecutionSettings]);
}, [canReadEndpointExceptions, hasIndexRead, rule, ruleDetailTabs, ruleExecutionSettings]);
return pageTabs;
};

View file

@ -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([

View file

@ -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 = ({
<ExceptionsViewerItems
isReadOnly={isReadOnly}
disableActions={isReadOnly || viewerState === 'deleting'}
disableActions={isReadOnly || viewerState === 'deleting' || !canWriteEndpointExceptions}
exceptions={exceptions}
isEndpoint={isEndpointSpecified}
ruleReferences={allReferences}

View file

@ -8,8 +8,7 @@
import { useCallback, useMemo } from 'react';
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import { useListsConfig } from '../../../containers/detection_engine/lists/use_lists_config';
import { useHasSecurityCapability } from '../../../../helper_hooks';
import { useEndpointExceptionsCapability } from '../../../../exceptions/hooks/use_endpoint_exceptions_capability';
import { useUserData } from '../../user_info';
import { ACTION_ADD_ENDPOINT_EXCEPTION, ACTION_ADD_EXCEPTION } from '../translations';
import type { AlertTableContextMenuItem } from '../types';
@ -76,14 +75,7 @@ export const useAlertExceptionActions = ({
onAddExceptionTypeClick,
});
const { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration } =
useListsConfig();
const canReadEndpointExceptions = useHasSecurityCapability('crudEndpointExceptions');
const canWriteEndpointExceptions = useMemo(
() => !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)

View file

@ -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<ListExceptionItemsProps> = ({
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<ListExceptionItemsProps> = ({
viewerStatus={viewerStatus as ViewerStatus}
listType={listType as ExceptionListTypeEnum}
ruleReferences={ruleReferences}
isReadOnly={isReadOnly}
isReadOnly={isReadOnly || !canWriteEndpointExceptions}
exceptions={exceptions}
emptyViewerTitle={emptyViewerTitle}
emptyViewerBody={emptyViewerBody}

View file

@ -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]
);
};

View file

@ -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<void>;
@ -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,

View file

@ -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 = () => (
<PluginTemplateWrapper>
@ -32,9 +33,9 @@ const ExceptionsRoutes = () => (
const ExceptionsListDetailRoute = () => (
<PluginTemplateWrapper>
<TrackApplicationView viewId={SecurityPageName.exceptions}>
<SecurityRoutePageWrapper pageName={SecurityPageName.exceptions}>
<ListsDetailView />
</TrackApplicationView>
</SecurityRoutePageWrapper>
</PluginTemplateWrapper>
);

View file

@ -7,13 +7,13 @@
import { i18n } from '@kbn/i18n';
import {
RULES_PATH,
RULES_CREATE_PATH,
EXCEPTIONS_PATH,
RULES_LANDING_PATH,
RULES_ADD_PATH,
SERVER_APP_ID,
COVERAGE_OVERVIEW_PATH,
EXCEPTIONS_PATH,
RULES_ADD_PATH,
RULES_CREATE_PATH,
RULES_LANDING_PATH,
RULES_PATH,
SERVER_APP_ID,
} 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: [

View file

@ -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';

View file

@ -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;

View file

@ -11,10 +11,11 @@ import type {
} from '@kbn/lists-plugin/server';
import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
import {
EventFilterValidator,
TrustedAppValidator,
HostIsolationExceptionsValidator,
BlocklistValidator,
EndpointExceptionsValidator,
EventFilterValidator,
HostIsolationExceptionsValidator,
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;
};
};

View file

@ -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;
};
};

View file

@ -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;
};
};

View file

@ -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;
};
};

View file

@ -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;
};
};

View file

@ -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;
};
};

View file

@ -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;
};
};

View file

@ -12,10 +12,11 @@ import type {
import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
import type { ExceptionItemLikeOptions } from '../types';
import {
EventFilterValidator,
TrustedAppValidator,
HostIsolationExceptionsValidator,
BlocklistValidator,
EndpointExceptionsValidator,
EventFilterValidator,
HostIsolationExceptionsValidator,
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;
};
};

View file

@ -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<void> {
if (!(await this.endpointAuthzPromise)[privilege]) {
throw new EndpointExceptionsValidationError('Endpoint exceptions authorization failure', 403);
}
}
protected async validateHasPrivilege(privilege: keyof EndpointAuthz): Promise<void> {
if (!(await this.endpointAuthzPromise)[privilege]) {
throw new EndpointArtifactExceptionValidationError('Endpoint authorization failure', 403);

View file

@ -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);
}
}

View file

@ -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<void> {
return this.validateHasEndpointExceptionsPrivileges('canReadEndpointExceptions');
}
protected async validateHasWritePrivilege(): Promise<void> {
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<void> {
await this.validateHasWritePrivilege();
}
async validatePreGetOneItem(): Promise<void> {
await this.validateHasReadPrivilege();
}
async validatePreMultiListFind(): Promise<void> {
await this.validateHasReadPrivilege();
}
async validatePreExport(): Promise<void> {
await this.validateHasReadPrivilege();
}
async validatePreSingleListFind(): Promise<void> {
await this.validateHasReadPrivilege();
}
async validatePreGetListSummary(): Promise<void> {
await this.validateHasReadPrivilege();
}
}

View file

@ -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';

View file

@ -33,6 +33,7 @@ export const PLI_APP_FEATURES: PliAppFeatures = {
complete: [
AppFeatureKey.endpointResponseActions,
AppFeatureKey.osqueryAutomatedResponseActions,
AppFeatureKey.endpointExceptions,
],
},
cloud: {

View file

@ -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'))
);

View file

@ -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 (
<EuiEmptyPrompt
icon={<EuiIcon type="logoSecurity" size="xl" />}
color="subdued"
title={
<h2>
<FormattedMessage
id="xpack.securitySolutionServerless.endpoint.exceptions.details.paywall.title"
defaultMessage="Do more with Security!"
/>
</h2>
}
body={
<p>
<FormattedMessage
id="xpack.securitySolutionServerless.endpoint.exceptions.details.paywall.body"
defaultMessage="Upgrade your license to {productTypeRequired} to use Endpoint Security Exception List."
values={{ productTypeRequired }}
/>
</p>
}
/>
);
}
);
EndpointExceptionsDetailsUpselling.displayName = 'EndpointExceptionsDetailsUpselling';
// eslint-disable-next-line import/no-default-export
export { EndpointExceptionsDetailsUpselling as default };

View file

@ -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';

View file

@ -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 = [
<ThreatIntelligencePaywallLazy requiredPLI={AppFeatureKey.threatIntelligence} />
),
},
{
pageName: SecurityPageName.exceptions,
pli: AppFeatureKey.endpointExceptions,
component: () => (
<EndpointExceptionsDetailsUpsellingLazy requiredPLI={AppFeatureKey.endpointExceptions} />
),
},
];
// 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',

View file

@ -12,3 +12,9 @@ export const EndpointPolicyProtectionsLazy = lazy(() =>
default: EndpointPolicyProtections,
}))
);
export const RuleDetailsEndpointExceptionsLazy = lazy(() =>
import('./rule_details_endpoint_exceptions').then(({ RuleDetailsEndpointExceptions }) => ({
default: RuleDetailsEndpointExceptions,
}))
);

View file

@ -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 (
<EuiCard
data-test-subj="endpointPolicy-protectionsLockedCard"
isDisabled={true}
description={false}
icon={<EuiIcon size="xl" type="lock" />}
betaBadgeProps={{
'data-test-subj': 'rules-endpointSecurity-endpointExceptionsLockedCard-badge',
label: BADGE_TEXT,
}}
title={
<h3 data-test-subj="rules-endpointSecurity-endpointExceptionsLockedCard-title">
<strong>{CARD_TITLE}</strong>
</h3>
}
>
<CardDescription>{CARD_MESSAGE}</CardDescription>
</EuiCard>
);
});
RuleDetailsEndpointExceptions.displayName = 'RuleDetailsEndpointExceptions';