mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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`.  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.  - #### 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:
parent
db6fa7317c
commit
a8de031ddf
46 changed files with 920 additions and 193 deletions
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
|
@ -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)
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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'];
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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';
|
|
@ -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');
|
||||
};
|
|
@ -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"
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -33,6 +33,7 @@ export const PLI_APP_FEATURES: PliAppFeatures = {
|
|||
complete: [
|
||||
AppFeatureKey.endpointResponseActions,
|
||||
AppFeatureKey.osqueryAutomatedResponseActions,
|
||||
AppFeatureKey.endpointExceptions,
|
||||
],
|
||||
},
|
||||
cloud: {
|
||||
|
|
|
@ -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'))
|
||||
);
|
||||
|
|
|
@ -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 };
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -12,3 +12,9 @@ export const EndpointPolicyProtectionsLazy = lazy(() =>
|
|||
default: EndpointPolicyProtections,
|
||||
}))
|
||||
);
|
||||
|
||||
export const RuleDetailsEndpointExceptionsLazy = lazy(() =>
|
||||
import('./rule_details_endpoint_exceptions').then(({ RuleDetailsEndpointExceptions }) => ({
|
||||
default: RuleDetailsEndpointExceptions,
|
||||
}))
|
||||
);
|
||||
|
|
|
@ -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';
|
Loading…
Add table
Add a link
Reference in a new issue