mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Security Solution] [Attack discovery] Attack Discovery RBAC / Display an upgrade CTA for the serverless essentials product tier (#188788)
## [Security Solution] [Attack discovery] Attack Discovery RBAC / Display an upgrade CTA for the serverless essentials product tier ### Summary This PR adds Role Based Access Control (RBAC) to Attack discovery. Security users may enable or disable the new `Attack Discovery` RBAC feature shown in the figure below:  _Above: The new `Attack discovery` RBAC feature_ It is possible to for example, configure a custom role that enables Attack discovery, but disables the assistant, as illustrated by the table below: | Role | License | Navigation visible | Show upsell | Upsell has actions | View in assistant enabled | |-------------------------------------------|------------|--------------------|-------------|--------------------|---------------------------| | `has_attack_discovery_all_assistant_none` | Basic | ✅ | ✅ | ✅ | ❌ | | `has_attack_discovery_all_assistant_none` | Trial | ✅ | ❌ | ❌ | ❌ | | `has_attack_discovery_all_assistant_none` | Platinum | ✅ | ✅ | ✅ | ❌ | | `has_attack_discovery_all_assistant_none` | Enterprise | ✅ | ❌ | ❌ | ❌ | _Above: An example role that enables Attack discovery, but disables the assistant_ See the `Desk Testing` section of this PR for details. This PR also fixes an issue where Attack discovery does not display an upgrade call to action (CTA) for the serverless _essentials_ product tier, per the before and after screenshots below: #### Before  _Above: Before the fix, an upgrade CTA is NOT displayed for the serverless essentials product tier_ #### After  _Above: After the fix, an upgrade CTA is displayed for the serverless essentials product tier_ The fix above is implemented by adopting the upselling framework. ### New Feature ID This PR adds a new Feature ID for attack discovery: ```typescript export const ATTACK_DISCOVERY_FEATURE_ID = 'securitySolutionAttackDiscovery' as const; ``` in `x-pack/packages/security-solution/features/src/constants.ts` ### Upselling framework usage This PR updates the Attack discovery page to use the upselling framework via the following summarized steps: 1. Removed the branching logic from `x-pack/plugins/security_solution/public/attack_discovery/pages/upgrade/index.tsx`, and moved the component to an upselling `section` component in `x-pack/packages/security-solution/upselling/sections/attack_discovery/index.tsx`, where the component was renamed to `AttackDiscoveryUpsellingSection`. This `section` component handles (just) the styling of the upselling message and actions (by itself, without the page wrapper), and receives the following props: ```typescript interface Props { actions?: React.ReactNode; availabilityMessage: string; upgradeMessage: string; } ``` The self managed and serverless-specific actions and `i18n` messages are passed down via the components described in the later steps below. 2. Removed all previous references to the `Upgrade` component (and references to `useProductTypes`) from the Attack discovery page in `x-pack/plugins/security_solution/public/attack_discovery/pages/index.tsx`, because the framework manages the upgrade case instead of the page itself. 3. Created an upselling `page` component `AttackDiscoveryUpsellingPage` in `x-pack/packages/security-solution/upselling/pages/attack_discovery/index.tsx`. This component handles (just) the styling of the _page_ that wraps the Attack discovery `section`. It passes the same props to the previously described `AttackDiscoveryUpsellingSection` component. 4. Created a self-managed-specific `AttackDiscoveryUpsellingPageESS` component in `x-pack/plugins/security_solution_ess/public/upselling/pages/attack_discovery/index.tsx` This component passes self-managed-specific upgrade action buttons / links and `i18n` strings to the previously described `AttackDiscoveryUpsellingPage` 5. Also for self managed, added a new `AttackDiscoveryUpsellingPageLazy` component to the existing file: `x-pack/plugins/security_solution_ess/public/upselling/lazy_upselling.tsx` This component lazy loads the previously described `AttackDiscoveryUpsellingPageESS` component. 6. Added registration for the previously described `AttackDiscoveryUpsellingPageLazy` component to the existing `UpsellingPages` section in `x-pack/plugins/security_solution_ess/public/upselling/register_upsellings.tsx` with a `minimumLicenseRequired` of `enterprise`: ``` minimumLicenseRequired: 'enterprise', ``` 7. Created a serverless-specific `AttackDiscoveryUpsellingPageServerless` component in `x-pack/plugins/security_solution_serverless/public/upselling/pages/attack_discovery/index.tsx` This component passes serverless-specific `i18n` messages to the platform agnostic `AttackDiscoveryUpsellingPage` component. 8. Also for serverless, added a new `AttackDiscoveryUpsellingPageLazy` component to the existing file: `x-pack/plugins/security_solution_serverless/public/upselling/lazy_upselling.tsx` 9. Added registration for the previously described `AttackDiscoveryUpsellingPageLazy` component to the existing `upsellingPages` section in `x-pack/plugins/security_solution_serverless/public/upselling/upsellings.tsx` with the `assistant` PLI: ``` pli: ProductFeatureKey.assistant, ``` 10. Added the `${ASSISTANT_FEATURE_ID}.ai-assistant` capability as an OR condition (via nested array, per the [framework](https://github.com/elastic/kibana/blob/main/x-pack/plugins/security_solution/public/common/lib/capabilities/has_capabilities.ts#L11-L22)) to the Attack discovery link in `x-pack/plugins/security_solution/public/attack_discovery/links.ts`. This addition enables the security route wrapper to check for upselling pages in serverless: ``` capabilities: [[`${SERVER_APP_ID}.show`, `${ASSISTANT_FEATURE_ID}.ai-assistant`]], ``` 11. Added `licenseType: 'enterprise'` to the Attack discovery link in `x-pack/plugins/security_solution/public/attack_discovery/links.ts` to require an `enterprise` license for self managed ### Upgrade CTA gallery The screenshots in this section document the CTA (or Welcome message when the feature is licensed) displayed for various license levels after the fix: #### Users with the `None` privilege If users with the `None` privilege manually enter an Attack discovery URL, e.g. `http://localhost:5601/app/security/attack_discovery`, the framework will display the following error prompt:  #### Self managed BASIC  #### Self managed PLATINUM  #### Self managed TRIAL  #### Self managed ENTERPRISE  #### Serverless ESSENTIALS  #### Serverless COMPLETE  ## Desk Testing ### Severless: Desk testing (just) the upgrade CTA Reproduction steps: 1) Comment-out any preconfigured connectors in `config/kibana.dev.yml` 2) Edit the configuration of `config/serverless.security.yml` to enable the `essentials` product tier: ```yaml xpack.securitySolutionServerless.productTypes: [ { product_line: 'security', product_tier: 'essentials' } ] ``` 3) Start Elasticsearch ```sh yarn es serverless --projectType security ``` 4) Start a development instance of Kibana ``` yarn start --serverless=security --no-base-path ``` 5) Navigate to Security > Attack discovery **Expected result** - An upgrade CTA is displayed, as illustrated by the _after_ screenshot below:  - The video tour is NOT displayed for serverless, as noted in the [original PR](https://github.com/elastic/kibana/pull/182605#issuecomment-2100607857) **Actual result** - An upgrade CTA is NOT displayed, as illustrated by the _before_ screenshot below:  ### Desk testing Self Managed To desk test self manged, we will: 1) Create (three) roles for testing 2) Create (three) users assigned to the roles 3) Test each role at `Basic`, `Trial`, `Platinum`, and `Enterprise` levels to verify: - `Attack discovery` link visibility in the Security solution navigation - Visibility of the upsell empty prompt for license levels where Attack discovery is unavailable - The upsell empty prompt includes the `Subscription plans` and `Manage license` actions - When Attack discoveries are generated, the `View in Ai Assistant` button and popover menu action are enabled / disabled, based on availability of the `AI Assistant` feature #### Creating (three) roles for testing In this section, we will start a new (development) self managed deployment, and create the following three roles via Kibana Dev Tools: - `has_attack_discovery_all_assistant_all` - `has_attack_discovery_all_assistant_none` - `has_attack_discovery_none_assistant_all` To start the deployment and create the roles: 1) Add a pre-configured GenAI connector to `config/kibana.dev.yml` 2) Start a new (development) instance of Elasticsearch: ```sh yarn es snapshot -E path.data=/Users/$USERNAME/data-2024-07-31a ``` 3) Start a local (development) instance of Kibana: ``` yarn start --no-base-path ```` 4) Login to Kibana as the `elastic` user 5) Generate some alerts 6) Navigate to Dev Tools 7) Execute the following three API calls to create the roles: <details><summary>PUT /_security/role/has_attack_discovery_all_assistant_all</summary> <p> ``` ts PUT /_security/role/has_attack_discovery_all_assistant_all { "cluster": [ "all" ], "indices": [ { "names": [ "*" ], "privileges": [ "all" ], "field_security": { "grant": [ "*" ], "except": [] }, "allow_restricted_indices": false } ], "applications": [ { "application": "kibana-.kibana", "privileges": [ "feature_securitySolutionAssistant.minimal_all", "feature_securitySolutionAttackDiscovery.minimal_all", "feature_siem.all", "feature_securitySolutionCases.all", "feature_actions.all" ], "resources": [ "*" ] } ], "run_as": [], "metadata": {}, "transient_metadata": { "enabled": true } } ``` </p> </details> <details><summary>PUT /_security/role/has_attack_discovery_all_assistant_none</summary> <p> ``` ts PUT /_security/role/has_attack_discovery_all_assistant_none { "cluster": [ "all" ], "indices": [ { "names": [ "*" ], "privileges": [ "all" ], "field_security": { "grant": [ "*" ], "except": [] }, "allow_restricted_indices": false } ], "applications": [ { "application": "kibana-.kibana", "privileges": [ "feature_securitySolutionAttackDiscovery.minimal_all", "feature_siem.all", "feature_securitySolutionCases.all", "feature_actions.all" ], "resources": [ "*" ] } ], "run_as": [], "metadata": {}, "transient_metadata": { "enabled": true } } ``` </p> </details> <details><summary>PUT /_security/role/has_attack_discovery_none_assistant_all</summary> <p> ``` ts PUT /_security/role/has_attack_discovery_none_assistant_all { "cluster": [ "all" ], "indices": [ { "names": [ "*" ], "privileges": [ "all" ], "field_security": { "grant": [ "*" ], "except": [] }, "allow_restricted_indices": false } ], "applications": [ { "application": "kibana-.kibana", "privileges": [ "feature_securitySolutionAssistant.minimal_all", "feature_siem.all", "feature_securitySolutionCases.all", "feature_actions.all" ], "resources": [ "*" ] } ], "run_as": [], "metadata": {}, "transient_metadata": { "enabled": true } } ``` </p> </details> #### Creating (three) users assigned to the roles In this section, we will create the following three users via Kibana Dev Tools using the API calls below (expand for details): - `attack_discovery_all_assistant_all` - `attack_discovery_all_assistant_none` - `attack_discovery_none_assistant_all` 1) Navigate to Dev Tools 2) Execute the following three API calls to create the users: <details><summary>POST /_security/user/attack_discovery_all_assistant_all</summary> <p> ``` ts POST /_security/user/attack_discovery_all_assistant_all { "username": "attack_discovery_all_assistant_all", "password": "changeme", "roles": [ "has_attack_discovery_all_assistant_all" ], "full_name": "Attack Discovery All Assistant All", "email": "user@example.com", "metadata": {}, "enabled": true } ``` </p> </details> <details><summary>POST /_security/user/attack_discovery_all_assistant_none</summary> <p> ``` ts POST /_security/user/attack_discovery_all_assistant_none { "username": "attack_discovery_all_assistant_none", "password": "changeme", "roles": [ "has_attack_discovery_all_assistant_none" ], "full_name": "Attack Discovery All Assistant None", "email": "user@example.com", "metadata": {}, "enabled": true } ``` </p> </details> <details><summary>POST /_security/user/attack_discovery_none_assistant_all</summary> <p> ``` ts POST /_security/user/attack_discovery_none_assistant_all { "username": "attack_discovery_none_assistant_all", "password": "changeme", "roles": [ "has_attack_discovery_none_assistant_all" ], "full_name": "Attack Discovery None Assistant All", "email": "user@example.com", "metadata": {}, "enabled": true } ``` </p> </details> #### Testing each role at `Basic`, `Trial`, `Platinum`, and `Enterprise` levels In this section, we will test each of the self managed `Basic`, `Trial`, `Platinum`, and `Enterprise` license levels with the three roles we created for testing. ##### Testing the `has_attack_discovery_all_assistant_all` role 1) Login as the `attack_discovery_all_assistant_all` user 2) Navigate to the Security solution 3) For each of the `Basic`, `Trial`, `Platinum`, and `Enterprise` levels, verify your observations match the expected behavior in the table below: | Role | License | Navigation visible | Show upsell | Upsell has actions | View in assistant enabled | |------------------------------------------|------------|--------------------|-------------|--------------------|---------------------------| | `has_attack_discovery_all_assistant_all` | Basic | ✅ | ✅ | ✅ | ❌ | | `has_attack_discovery_all_assistant_all` | Trial | ✅ | ❌ | ❌ | ✅ | | `has_attack_discovery_all_assistant_all` | Platinum | ✅ | ✅ | ✅ | ❌ | | `has_attack_discovery_all_assistant_all` | Enterprise | ✅ | ❌ | ❌ | ✅ | ##### Testing the `has_attack_discovery_all_assistant_none` role 1) Login as the `attack_discovery_all_assistant_none` user 2) Navigate to the Security solution 3) For each of the `Basic`, `Trial`, `Platinum`, and `Enterprise` levels, verify your observations match the expected behavior in the table below: | Role | License | Navigation visible | Show upsell | Upsell has actions | View in assistant enabled | |-------------------------------------------|------------|--------------------|-------------|--------------------|---------------------------| | `has_attack_discovery_all_assistant_none` | Basic | ✅ | ✅ | ✅ | ❌ | | `has_attack_discovery_all_assistant_none` | Trial | ✅ | ❌ | ❌ | ❌ | | `has_attack_discovery_all_assistant_none` | Platinum | ✅ | ✅ | ✅ | ❌ | | `has_attack_discovery_all_assistant_none` | Enterprise | ✅ | ❌ | ❌ | ❌ | ##### Testing the `has_attack_discovery_none_assistant_all` role 1) Login as the `attack_discovery_none_assistant_all` user 2) Navigate to the Security solution 3) For each of the `Basic`, `Trial`, `Platinum`, and `Enterprise` levels, verify your observations match the expected behavior in the table below: | Role | License | Navigation visible | Show upsell | Upsell has actions | View in assistant enabled | |-------------------------------------------|------------|--------------------|-------------|--------------------|---------------------------| | `has_attack_discovery_none_assistant_all` | Basic | ✅ | ✅ | ✅ | ❌ | | `has_attack_discovery_none_assistant_all` | Trial | ❌ | ❌ | ❌ | ❌ | | `has_attack_discovery_none_assistant_all` | Platinum | ✅ | ✅ | ✅ | ❌ | | `has_attack_discovery_none_assistant_all` | Enterprise | ❌ | ❌ | ❌ | ❌ | --------------------------------------------- ### Serverless Testing To desk test serverless, we will test the `essentials` and `complete` product tiers to verify: - `Attack discovery` link visibility in the Security project navigation - Visibility of the upsell empty prompt for license levels where Attack discovery is unavailable - The upsell empty prompt does NOT include the `Subscription plans` and `Manage license` actions - When Attack discoveries are generated, the `View in Ai Assistant` button and popover menu action are enabled #### Essentials tier testing 1) Add a pre-configured GenAI connector to `config/kibana.dev.yml` 2) Edit the configuration of `config/serverless.security.yml` to enable the `essentials` product tier: ```yaml xpack.securitySolutionServerless.productTypes: [ { product_line: 'security', product_tier: 'essentials' }, { product_line: 'endpoint', product_tier: 'essentials' }, ] ``` 2) Start a new (development) instance of Elasticsearch: ```sh yarn es serverless --clean --projectType security ``` 3) Start a local (development) instance of Kibana: ``` yarn start --serverless=security --no-base-path ```` 4) select the `admin` role 5) Generate some alerts 6) Verify your observations match the expected behavior in the table below: | Role | Tier | Navigation visible | Show upsell | Upsell has actions | View in assistant enabled | |-------------------------------|------------|--------------------|-------------|--------------------|---------------------------| | `viewer` | essentials | ✅ | ✅ | ❌ | ❌ | | `editor` | essentials | ✅ | ✅ | ❌ | ❌ | | `t1_analyst` | essentials | ✅ | ✅ | ❌ | ❌ | | `t2_analyst` | essentials | ✅ | ✅ | ❌ | ❌ | | `t3_analyst` | essentials | ✅ | ✅ | ❌ | ❌ | | `threat_intelligence_analyst` | essentials | ✅ | ✅ | ❌ | ❌ | | `rule_author` | essentials | ✅ | ✅ | ❌ | ❌ | | `soc_manager` | essentials | ✅ | ✅ | ❌ | ❌ | | `detections_admin` | essentials | ✅ | ✅ | ❌ | ❌ | | `platform_engineer` | essentials | ✅ | ✅ | ❌ | ❌ | | `endpoint_operations_analyst` | essentials | ✅ | ✅ | ❌ | ❌ | | `endpoint_policy_manager` | essentials | ✅ | ✅ | ❌ | ❌ | | `admin` | essentials | ✅ | ✅ | ❌ | ❌ | | `system_indices_superuser` | essentials | ✅ | ✅ | ❌ | ❌ | ### Complete tier testing 1) Stop the running Kibana server (from the previous Essentials tier testing) 2) Edit the configuration of `config/serverless.security.yml` to enable the `complete` product tier: ```yaml xpack.securitySolutionServerless.productTypes: [ { product_line: 'security', product_tier: 'complete' }, { product_line: 'endpoint', product_tier: 'complete' }, ] ``` 3) Restart a local (development) instance of Kibana: ``` yarn start --serverless=security --no-base-path ```` 4) Verify your observations match the expected behavior in the table below: | Role | Tier | Navigation visible | Show upsell | Upsell has actions | View in assistant enabled | |-------------------------------|----------|--------------------|-------------|--------------------|---------------------------| | `viewer` | complete | ✅ | ❌ | ❌ | ✅ | | `editor` | complete | ✅ | ❌ | ❌ | ✅ | | `t1_analyst` | complete | ✅ | ❌ | ❌ | ✅ | | `t2_analyst` | complete | ✅ | ❌ | ❌ | ✅ | | `t3_analyst` | complete | ✅ | ❌ | ❌ | ✅ | | `threat_intelligence_analyst` | complete | ✅ | ❌ | ❌ | ✅ | | `rule_author` | complete | ✅ | ❌ | ❌ | ✅ | | `soc_manager` | complete | ✅ | ❌ | ❌ | ✅ | | `detections_admin` | complete | ✅ | ❌ | ❌ | ✅ | | `platform_engineer` | complete | ✅ | ❌ | ❌ | ✅ | | `endpoint_operations_analyst` | complete | ✅ | ❌ | ❌ | ✅ | | `endpoint_policy_manager` | complete | ✅ | ❌ | ❌ | ✅ | | `admin` | complete | ✅ | ❌ | ❌ | ✅ | | `system_indices_superuser` | complete | ✅ | ❌ | ❌ | ✅ |
This commit is contained in:
parent
571fe047c1
commit
8ee04937fe
73 changed files with 1192 additions and 289 deletions
|
@ -47,6 +47,7 @@ viewer:
|
|||
- feature_siem.endpoint_list_read
|
||||
- feature_securitySolutionCases.read
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
- feature_builtInAlerts.read
|
||||
- feature_osquery.read
|
||||
|
@ -124,6 +125,7 @@ editor:
|
|||
- feature_siem.file_operations_all
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
- feature_builtInAlerts.all
|
||||
- feature_osquery.all
|
||||
|
@ -171,6 +173,7 @@ t1_analyst:
|
|||
- feature_siem.endpoint_list_read
|
||||
- feature_securitySolutionCases.read
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
- feature_builtInAlerts.read
|
||||
- feature_osquery.read
|
||||
|
@ -224,6 +227,7 @@ t2_analyst:
|
|||
- feature_siem.endpoint_list_read
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
- feature_builtInAlerts.read
|
||||
- feature_osquery.read
|
||||
|
@ -292,6 +296,7 @@ t3_analyst:
|
|||
- feature_siem.scan_operations_all
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
- feature_builtInAlerts.all
|
||||
- feature_osquery.all
|
||||
|
@ -352,6 +357,7 @@ threat_intelligence_analyst:
|
|||
- feature_siem.blocklist_all
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
- feature_builtInAlerts.read
|
||||
- feature_osquery.all
|
||||
|
@ -418,6 +424,7 @@ rule_author:
|
|||
- feature_siem.actions_log_management_read
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
- feature_builtInAlerts.all
|
||||
- feature_osquery.all
|
||||
|
@ -488,6 +495,7 @@ soc_manager:
|
|||
- feature_siem.scan_operations_all
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.all
|
||||
- feature_builtInAlerts.all
|
||||
- feature_osquery.all
|
||||
|
@ -546,6 +554,7 @@ detections_admin:
|
|||
- feature_siem.crud_alerts
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.all
|
||||
- feature_builtInAlerts.all
|
||||
- feature_dev_tools.all
|
||||
|
@ -603,6 +612,7 @@ platform_engineer:
|
|||
- feature_siem.actions_log_management_read
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.all
|
||||
- feature_builtInAlerts.all
|
||||
- feature_fleet.all
|
||||
|
@ -674,6 +684,7 @@ endpoint_operations_analyst:
|
|||
- feature_siem.scan_operations_all
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.all
|
||||
- feature_builtInAlerts.all
|
||||
- feature_osquery.all
|
||||
|
@ -747,6 +758,7 @@ endpoint_policy_manager:
|
|||
- feature_siem.blocklist_all # Elastic Defend Policy Management
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.all
|
||||
- feature_builtInAlerts.all
|
||||
- feature_osquery.all
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
"ml": ["read"],
|
||||
"siem": ["read", "read_alerts"],
|
||||
"securitySolutionAssistant": ["all"],
|
||||
"securitySolutionAttackDiscovery": ["all"],
|
||||
"securitySolutionCases": ["read"],
|
||||
"actions": ["read"],
|
||||
"builtInAlerts": ["read"]
|
||||
|
@ -80,6 +81,7 @@
|
|||
"ml": ["read"],
|
||||
"siem": ["read", "read_alerts"],
|
||||
"securitySolutionAssistant": ["all"],
|
||||
"securitySolutionAttackDiscovery": ["all"],
|
||||
"securitySolutionCases": ["read"],
|
||||
"actions": ["read"],
|
||||
"builtInAlerts": ["read"]
|
||||
|
@ -145,6 +147,7 @@
|
|||
],
|
||||
"securitySolutionCases": ["all"],
|
||||
"securitySolutionAssistant": ["all"],
|
||||
"securitySolutionAttackDiscovery": ["all"],
|
||||
"actions": ["read"],
|
||||
"builtInAlerts": ["all"],
|
||||
"osquery": ["all"],
|
||||
|
@ -201,6 +204,7 @@
|
|||
"ml": ["read"],
|
||||
"siem": ["all", "read_alerts", "crud_alerts"],
|
||||
"securitySolutionAssistant": ["all"],
|
||||
"securitySolutionAttackDiscovery": ["all"],
|
||||
"securitySolutionCases": ["all"],
|
||||
"actions": ["read"],
|
||||
"builtInAlerts": ["all"]
|
||||
|
@ -253,6 +257,7 @@
|
|||
"ml": ["read"],
|
||||
"siem": ["all", "read_alerts", "crud_alerts"],
|
||||
"securitySolutionAssistant": ["all"],
|
||||
"securitySolutionAttackDiscovery": ["all"],
|
||||
"securitySolutionCases": ["all"],
|
||||
"actions": ["all"],
|
||||
"builtInAlerts": ["all"]
|
||||
|
@ -300,6 +305,7 @@
|
|||
"ml": ["all"],
|
||||
"siem": ["all", "read_alerts", "crud_alerts"],
|
||||
"securitySolutionAssistant": ["all"],
|
||||
"securitySolutionAttackDiscovery": ["all"],
|
||||
"securitySolutionCases": ["all"],
|
||||
"actions": ["read"],
|
||||
"builtInAlerts": ["all"],
|
||||
|
@ -354,6 +360,7 @@
|
|||
"ml": ["all"],
|
||||
"siem": ["all", "read_alerts", "crud_alerts"],
|
||||
"securitySolutionAssistant": ["all"],
|
||||
"securitySolutionAttackDiscovery": ["all"],
|
||||
"securitySolutionCases": ["all"],
|
||||
"actions": ["all"],
|
||||
"builtInAlerts": ["all"]
|
||||
|
|
|
@ -144,7 +144,7 @@ pageLoadAssetSize:
|
|||
searchprofiler: 67080
|
||||
security: 81771
|
||||
securitySolution: 98429
|
||||
securitySolutionEss: 16573
|
||||
securitySolutionEss: 31781
|
||||
securitySolutionServerless: 62488
|
||||
serverless: 16573
|
||||
serverlessObservability: 68747
|
||||
|
|
|
@ -41,7 +41,7 @@ export const ActionTypeSelectorModal = React.memo(
|
|||
({ actionTypes, actionTypeRegistry, onClose, onSelect, actionTypeSelectorInline }: Props) => {
|
||||
const content = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexGroup justifyContent="center" responsive={false} wrap={true}>
|
||||
{actionTypes?.map((actionType: ActionType) => {
|
||||
const fullAction = actionTypeRegistry.get(actionType.id);
|
||||
return (
|
||||
|
|
|
@ -8,5 +8,6 @@
|
|||
export { securityDefaultProductFeaturesConfig } from './src/security/product_feature_config';
|
||||
export { getCasesDefaultProductFeaturesConfig } from './src/cases/product_feature_config';
|
||||
export { assistantDefaultProductFeaturesConfig } from './src/assistant/product_feature_config';
|
||||
export { attackDiscoveryDefaultProductFeaturesConfig } from './src/attack_discovery/product_feature_config';
|
||||
|
||||
export { createEnabledProductFeaturesConfigMap } from './src/helpers';
|
||||
|
|
|
@ -8,3 +8,4 @@
|
|||
export { getSecurityFeature } from './src/security';
|
||||
export { getCasesFeature } from './src/cases';
|
||||
export { getAssistantFeature } from './src/assistant';
|
||||
export { getAttackDiscoveryFeature } from './src/attack_discovery';
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 { getAttackDiscoveryBaseKibanaFeature } from './kibana_features';
|
||||
import type { ProductFeatureParams } from '../types';
|
||||
|
||||
export const getAttackDiscoveryFeature = (): ProductFeatureParams => ({
|
||||
baseKibanaFeature: getAttackDiscoveryBaseKibanaFeature(),
|
||||
baseKibanaSubFeatureIds: [],
|
||||
subFeaturesMap: new Map(),
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { APP_ID, ATTACK_DISCOVERY_FEATURE_ID } from '../constants';
|
||||
import { type BaseKibanaFeatureConfig } from '../types';
|
||||
|
||||
export const getAttackDiscoveryBaseKibanaFeature = (): BaseKibanaFeatureConfig => ({
|
||||
id: ATTACK_DISCOVERY_FEATURE_ID,
|
||||
name: i18n.translate(
|
||||
'securitySolutionPackages.features.featureRegistry.linkSecuritySolutionAttackDiscoveryTitle',
|
||||
{
|
||||
defaultMessage: 'Attack discovery',
|
||||
}
|
||||
),
|
||||
order: 1100,
|
||||
category: DEFAULT_APP_CATEGORIES.security,
|
||||
app: [ATTACK_DISCOVERY_FEATURE_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
minimumLicense: 'enterprise',
|
||||
privileges: {
|
||||
all: {
|
||||
api: ['elasticAssistant'],
|
||||
app: [ATTACK_DISCOVERY_FEATURE_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
},
|
||||
read: {
|
||||
// No read-only mode currently supported
|
||||
disabled: true,
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { ProductFeatureAttackDiscoveryKey } from '../product_features_keys';
|
||||
import type { ProductFeatureKibanaConfig } from '../types';
|
||||
|
||||
/**
|
||||
* App features privileges configuration for the Attack discovery feature.
|
||||
* These are the configs that are shared between both offering types (ess and serverless).
|
||||
* They can be extended on each offering plugin to register privileges using different way on each offering type.
|
||||
*
|
||||
* Privileges can be added in different ways:
|
||||
* - `privileges`: the privileges that will be added directly into the main Security feature.
|
||||
* - `subFeatureIds`: the ids of the sub-features that will be added into the Security subFeatures entry.
|
||||
* - `subFeaturesPrivileges`: the privileges that will be added into the existing Security subFeature with the privilege `id` specified.
|
||||
*/
|
||||
export const attackDiscoveryDefaultProductFeaturesConfig: Record<
|
||||
ProductFeatureAttackDiscoveryKey,
|
||||
ProductFeatureKibanaConfig
|
||||
> = {
|
||||
[ProductFeatureAttackDiscoveryKey.attackDiscovery]: {
|
||||
privileges: {
|
||||
all: {
|
||||
ui: ['attack-discovery'],
|
||||
},
|
||||
},
|
||||
subFeatureIds: [],
|
||||
},
|
||||
};
|
|
@ -11,6 +11,7 @@ export const SERVER_APP_ID = 'siem' as const;
|
|||
|
||||
export const CASES_FEATURE_ID = 'securitySolutionCases' as const;
|
||||
export const ASSISTANT_FEATURE_ID = 'securitySolutionAssistant' as const;
|
||||
export const ATTACK_DISCOVERY_FEATURE_ID = 'securitySolutionAttackDiscovery' as const;
|
||||
|
||||
// Same as the plugin id defined by Cloud Security Posture
|
||||
export const CLOUD_POSTURE_APP_ID = 'csp' as const;
|
||||
|
|
|
@ -93,17 +93,26 @@ export enum ProductFeatureAssistantKey {
|
|||
assistant = 'assistant',
|
||||
}
|
||||
|
||||
export enum ProductFeatureAttackDiscoveryKey {
|
||||
/**
|
||||
* Enables Attack discovery
|
||||
*/
|
||||
attackDiscovery = 'attack_discovery',
|
||||
}
|
||||
|
||||
// Merges the two enums.
|
||||
export const ProductFeatureKey = {
|
||||
...ProductFeatureSecurityKey,
|
||||
...ProductFeatureCasesKey,
|
||||
...ProductFeatureAssistantKey,
|
||||
...ProductFeatureAttackDiscoveryKey,
|
||||
};
|
||||
// We need to merge the value and the type and export both to replicate how enum works.
|
||||
export type ProductFeatureKeyType =
|
||||
| ProductFeatureSecurityKey
|
||||
| ProductFeatureCasesKey
|
||||
| ProductFeatureAssistantKey;
|
||||
| ProductFeatureAssistantKey
|
||||
| ProductFeatureAttackDiscoveryKey;
|
||||
|
||||
export const ALL_PRODUCT_FEATURE_KEYS = Object.freeze(Object.values(ProductFeatureKey));
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import type {
|
|||
import type { RecursivePartial } from '@kbn/utility-types';
|
||||
import type {
|
||||
ProductFeatureAssistantKey,
|
||||
ProductFeatureAttackDiscoveryKey,
|
||||
ProductFeatureCasesKey,
|
||||
ProductFeatureKeyType,
|
||||
ProductFeatureSecurityKey,
|
||||
|
@ -51,6 +52,11 @@ export type ProductFeaturesAssistantConfig = Map<
|
|||
ProductFeatureKibanaConfig<AssistantSubFeatureId>
|
||||
>;
|
||||
|
||||
export type ProductFeaturesAttackDiscoveryConfig = Map<
|
||||
ProductFeatureAttackDiscoveryKey,
|
||||
ProductFeatureKibanaConfig
|
||||
>;
|
||||
|
||||
export type AppSubFeaturesMap<T extends string = string> = Map<T, SubFeatureConfig>;
|
||||
|
||||
export interface ProductFeatureParams<T extends string = string> {
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { AttackDiscoveryUpsellingPage } from '.';
|
||||
|
||||
const availabilityMessage = 'This feature is available...';
|
||||
const upgradeMessage = 'Please upgrade...';
|
||||
|
||||
const mockActions = <div data-test-subj="mockActions" />;
|
||||
|
||||
jest.mock('@kbn/security-solution-navigation', () => {
|
||||
const original = jest.requireActual('@kbn/security-solution-navigation');
|
||||
return {
|
||||
...original,
|
||||
useNavigation: () => ({
|
||||
navigateTo: jest.fn(),
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('AttackDiscoveryUpsellingPage', () => {
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<AttackDiscoveryUpsellingPage
|
||||
actions={mockActions}
|
||||
availabilityMessage={availabilityMessage}
|
||||
upgradeMessage={upgradeMessage}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the availability message', () => {
|
||||
const attackDiscoveryIsAvailable = screen.getByTestId('availabilityMessage');
|
||||
|
||||
expect(attackDiscoveryIsAvailable).toHaveTextContent(availabilityMessage);
|
||||
});
|
||||
|
||||
it('renders the upgrade message', () => {
|
||||
const pleaseUpgrade = screen.getByTestId('upgradeMessage');
|
||||
|
||||
expect(pleaseUpgrade).toHaveTextContent(upgradeMessage);
|
||||
});
|
||||
|
||||
it('renders the actions', () => {
|
||||
const actions = screen.getByTestId('mockActions');
|
||||
|
||||
expect(actions).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
import { EuiPageHeader, EuiSpacer } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { PageTitle } from './page_title';
|
||||
import { AttackDiscoveryUpsellingSection } from '../../sections/attack_discovery';
|
||||
|
||||
interface Props {
|
||||
actions?: React.ReactNode;
|
||||
availabilityMessage: string;
|
||||
upgradeMessage: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component handles the styling of the _page_ that hosts the `AttackDiscoveryUpsellingSection`
|
||||
*/
|
||||
const AttackDiscoveryUpsellingPageComponent: React.FC<Props> = ({
|
||||
actions,
|
||||
availabilityMessage,
|
||||
upgradeMessage,
|
||||
}) => {
|
||||
const pageTitle = useMemo(() => <PageTitle />, []);
|
||||
|
||||
return (
|
||||
<KibanaPageTemplate restrictWidth={false} contentBorder={false} grow={true}>
|
||||
<KibanaPageTemplate.Section>
|
||||
<EuiPageHeader bottomBorder pageTitle={pageTitle} />
|
||||
<EuiSpacer size="xxl" />
|
||||
<AttackDiscoveryUpsellingSection
|
||||
actions={actions}
|
||||
availabilityMessage={availabilityMessage}
|
||||
upgradeMessage={upgradeMessage}
|
||||
/>
|
||||
</KibanaPageTemplate.Section>
|
||||
</KibanaPageTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
AttackDiscoveryUpsellingPageComponent.displayName = 'AttackDiscoveryUpsellingPage';
|
||||
|
||||
export const AttackDiscoveryUpsellingPage = React.memo(AttackDiscoveryUpsellingPageComponent);
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { PageTitle } from '.';
|
||||
import { ATTACK_DISCOVERY_PAGE_TITLE } from './translations';
|
||||
|
||||
describe('PageTitle', () => {
|
||||
beforeEach(() => {
|
||||
render(<PageTitle />);
|
||||
});
|
||||
|
||||
it('renders the expected title', () => {
|
||||
const attackDiscoveryPageTitle = screen.getByTestId('attackDiscoveryPageTitle');
|
||||
|
||||
expect(attackDiscoveryPageTitle).toHaveTextContent(ATTACK_DISCOVERY_PAGE_TITLE);
|
||||
});
|
||||
|
||||
it('renders the beta badge icon', () => {
|
||||
const betaBadge = screen.getByTestId('betaBadge');
|
||||
|
||||
expect(betaBadge).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { EuiBetaBadge, EuiFlexGroup, EuiFlexItem, EuiTitle, useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import React from 'react';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
const PageTitleComponent: React.FC = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
data-test-subj="pageTitle"
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
wrap={true}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle data-test-subj="attackDiscoveryPageTitle" size="l">
|
||||
<h1>{i18n.ATTACK_DISCOVERY_PAGE_TITLE}</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem
|
||||
css={css`
|
||||
margin: ${euiTheme.size.s} 0 0 ${euiTheme.size.m};
|
||||
`}
|
||||
grow={false}
|
||||
>
|
||||
<EuiBetaBadge
|
||||
color="hollow"
|
||||
data-test-subj="betaBadge"
|
||||
iconType={'beaker'}
|
||||
label={i18n.BETA}
|
||||
tooltipContent={i18n.BETA_TOOLTIP}
|
||||
size="m"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
PageTitleComponent.displayName = 'PageTitle';
|
||||
|
||||
export const PageTitle = React.memo(PageTitleComponent);
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ATTACK_DISCOVERY_PAGE_TITLE = i18n.translate(
|
||||
'securitySolutionPackages.upselling.pages.attackDiscovery.pageTitle.pageTitle',
|
||||
{
|
||||
defaultMessage: 'Attack discovery',
|
||||
}
|
||||
);
|
||||
|
||||
export const BETA = i18n.translate(
|
||||
'securitySolutionPackages.upselling.pages.attackDiscovery.pageTitle.betaBadge',
|
||||
{
|
||||
defaultMessage: 'Technical preview',
|
||||
}
|
||||
);
|
||||
|
||||
export const BETA_TOOLTIP = i18n.translate(
|
||||
'securitySolutionPackages.upselling.pages.attackDiscovery.pageTitle.betaTooltip',
|
||||
{
|
||||
defaultMessage:
|
||||
'This functionality is in technical preview and is subject to change. Please use Attack Discovery with caution in production environments.',
|
||||
}
|
||||
);
|
|
@ -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 React from 'react';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
size?: keyof typeof sizeMap;
|
||||
}
|
||||
|
||||
export const sizeMap = {
|
||||
xl: 64,
|
||||
l: 48,
|
||||
m: 32,
|
||||
s: 24,
|
||||
xs: 16,
|
||||
xxs: 12,
|
||||
};
|
||||
|
||||
/**
|
||||
* Default Elastic AI Assistant logo
|
||||
*
|
||||
* TODO: This may be removed when the logo is added to EUI
|
||||
*/
|
||||
export const AssistantAvatar = ({ className, size = 's' }: Props) => (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
height={sizeMap[size]}
|
||||
role="img"
|
||||
viewBox="0 0 64 64"
|
||||
width={sizeMap[size]}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path fill="#F04E98" d="M36 28h24v36H36V28Z" />
|
||||
<path fill="#00BFB3" d="M4 46c0-9.941 8.059-18 18-18h6v36h-6c-9.941 0-18-8.059-18-18Z" />
|
||||
<path
|
||||
fill="#343741"
|
||||
d="M60 12c0 6.627-5.373 12-12 12s-12-5.373-12-12S41.373 0 48 0s12 5.373 12 12Z"
|
||||
/>
|
||||
<path fill="#FA744E" d="M6 23C6 10.85 15.85 1 28 1v22H6Z" />
|
||||
</svg>
|
||||
);
|
|
@ -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 { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { AttackDiscoveryUpsellingSection } from '.';
|
||||
import { FIND_POTENTIAL_ATTACKS_WITH_AI } from './translations';
|
||||
|
||||
const availabilityMessage = 'Serverless or self-managed-specific availability message';
|
||||
const upgradeMessage = 'Serverless or self-managed-specific upgrade message';
|
||||
|
||||
const mockActions = (
|
||||
<div data-test-subj="mockActions">{'typically call to action buttons or links'}</div>
|
||||
);
|
||||
|
||||
describe('AttackDiscoveryUpsellingSection', () => {
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<AttackDiscoveryUpsellingSection
|
||||
actions={mockActions}
|
||||
availabilityMessage={availabilityMessage}
|
||||
upgradeMessage={upgradeMessage}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the assistant avatar', () => {
|
||||
const assistantAvatar = screen.getByTestId('assistantAvatar');
|
||||
|
||||
expect(assistantAvatar).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the expected upgrade title', () => {
|
||||
const upgradeTitle = screen.getByTestId('upgradeTitle');
|
||||
|
||||
expect(upgradeTitle).toHaveTextContent(FIND_POTENTIAL_ATTACKS_WITH_AI);
|
||||
});
|
||||
|
||||
it('renders the expected availability message', () => {
|
||||
const attackDiscoveryIsAvailable = screen.getByTestId('availabilityMessage');
|
||||
|
||||
expect(attackDiscoveryIsAvailable).toHaveTextContent(availabilityMessage);
|
||||
});
|
||||
|
||||
it('renders the expected upgrade message', () => {
|
||||
const pleaseUpgrade = screen.getByTestId('upgradeMessage');
|
||||
|
||||
expect(pleaseUpgrade).toHaveTextContent(upgradeMessage);
|
||||
});
|
||||
|
||||
it('renders the actions', () => {
|
||||
const actions = screen.getByTestId('mockActions');
|
||||
|
||||
expect(actions).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -5,15 +5,27 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AssistantAvatar, UpgradeButtons, useAssistantContext } from '@kbn/elastic-assistant';
|
||||
import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { AssistantAvatar } from './assistant_avatar/assistant_avatar';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const UpgradeComponent: React.FC = () => {
|
||||
const { http } = useAssistantContext();
|
||||
interface Props {
|
||||
actions?: React.ReactNode;
|
||||
availabilityMessage: string;
|
||||
upgradeMessage: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This `section` component handles (just) the styling of the upselling message
|
||||
* (by itself, without the page wrapper)
|
||||
*/
|
||||
const AttackDiscoveryUpsellingSectionComponent: React.FC<Props> = ({
|
||||
actions,
|
||||
availabilityMessage,
|
||||
upgradeMessage,
|
||||
}) => {
|
||||
const title = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup alignItems="center" direction="column" gutterSize="none">
|
||||
|
@ -38,33 +50,24 @@ const UpgradeComponent: React.FC = () => {
|
|||
() => (
|
||||
<EuiFlexGroup alignItems="center" direction="column" gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText color="subdued" data-test-subj="attackDiscoveryIsAvailable">
|
||||
{i18n.ATTACK_DISCOVERY_IS_AVAILABLE}
|
||||
<EuiText color="subdued" data-test-subj="availabilityMessage">
|
||||
{availabilityMessage}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText color="subdued" data-test-subj="pleaseUpgrade">
|
||||
{i18n.PLEASE_UPGRADE}
|
||||
<EuiText color="subdued" data-test-subj="upgradeMessage">
|
||||
{upgradeMessage}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const actions = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup justifyContent="center" gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<UpgradeButtons basePath={http.basePath.get()} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[http.basePath]
|
||||
[availabilityMessage, upgradeMessage]
|
||||
);
|
||||
|
||||
return <EuiEmptyPrompt actions={actions} body={body} data-test-subj="upgrade" title={title} />;
|
||||
};
|
||||
|
||||
export const Upgrade = React.memo(UpgradeComponent);
|
||||
AttackDiscoveryUpsellingSectionComponent.displayName = 'AttackDiscoveryUpsellingSection';
|
||||
|
||||
export const AttackDiscoveryUpsellingSection = React.memo(AttackDiscoveryUpsellingSectionComponent);
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const FIND_POTENTIAL_ATTACKS_WITH_AI = i18n.translate(
|
||||
'securitySolutionPackages.upselling.sections.attackDiscovery.findPotentialAttacksWithAiTitle',
|
||||
{
|
||||
defaultMessage: 'Find potential attacks with AI',
|
||||
}
|
||||
);
|
|
@ -20,6 +20,7 @@ export { SecurityPageName } from '@kbn/security-solution-navigation';
|
|||
export const APP_ID = 'securitySolution' as const;
|
||||
export const APP_UI_ID = 'securitySolutionUI' as const;
|
||||
export const ASSISTANT_FEATURE_ID = 'securitySolutionAssistant' as const;
|
||||
export const ATTACK_DISCOVERY_FEATURE_ID = 'securitySolutionAttackDiscovery' as const;
|
||||
export const CASES_FEATURE_ID = 'securitySolutionCases' as const;
|
||||
export const SERVER_APP_ID = 'siem' as const;
|
||||
export const APP_NAME = 'Security' as const;
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
"ml": ["read"],
|
||||
"siem": ["read", "read_alerts"],
|
||||
"securitySolutionAssistant": ["none"],
|
||||
"securitySolutionAttackDiscovery": ["none"],
|
||||
"securitySolutionCases": ["read"],
|
||||
"actions": ["read"],
|
||||
"builtInAlerts": ["read"]
|
||||
|
@ -77,6 +78,7 @@
|
|||
"ml": ["read"],
|
||||
"siem": ["all", "read_alerts", "crud_alerts"],
|
||||
"securitySolutionAssistant": ["all"],
|
||||
"securitySolutionAttackDiscovery": ["all"],
|
||||
"securitySolutionCases": ["all"],
|
||||
"actions": ["read"],
|
||||
"builtInAlerts": ["all"]
|
||||
|
@ -125,6 +127,7 @@
|
|||
"ml": ["read"],
|
||||
"siem": ["all", "read_alerts", "crud_alerts"],
|
||||
"securitySolutionAssistant": ["all"],
|
||||
"securitySolutionAttackDiscovery": ["all"],
|
||||
"securitySolutionCases": ["all"],
|
||||
"builtInAlerts": ["all"]
|
||||
},
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { ATTACK_DISCOVERY_FEATURE_ID } from '../../common/constants';
|
||||
import { SERVER_APP_ID } from '../../common';
|
||||
import { links } from './links';
|
||||
|
||||
describe('links', () => {
|
||||
it('for serverless, it specifies capabilities as an AND condition, via a nested array', () => {
|
||||
expect(links.capabilities).toEqual<string[][]>([
|
||||
[`${SERVER_APP_ID}.show`, `${ATTACK_DISCOVERY_FEATURE_ID}.attack-discovery`],
|
||||
]);
|
||||
});
|
||||
|
||||
it('for self managed, it requires an enterprise license', () => {
|
||||
expect(links.licenseType).toEqual('enterprise');
|
||||
});
|
||||
});
|
|
@ -8,11 +8,16 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { ATTACK_DISCOVERY } from '../app/translations';
|
||||
import { ATTACK_DISCOVERY_PATH, SecurityPageName, SERVER_APP_ID } from '../../common/constants';
|
||||
import {
|
||||
ATTACK_DISCOVERY_FEATURE_ID,
|
||||
ATTACK_DISCOVERY_PATH,
|
||||
SecurityPageName,
|
||||
SERVER_APP_ID,
|
||||
} from '../../common/constants';
|
||||
import type { LinkItem } from '../common/links/types';
|
||||
|
||||
export const links: LinkItem = {
|
||||
capabilities: [`${SERVER_APP_ID}.show`],
|
||||
capabilities: [[`${SERVER_APP_ID}.show`, `${ATTACK_DISCOVERY_FEATURE_ID}.attack-discovery`]], // This is an AND condition via the nested array
|
||||
globalNavPosition: 4,
|
||||
globalSearchKeywords: [
|
||||
i18n.translate('xpack.securitySolution.appLinks.attackDiscovery', {
|
||||
|
@ -20,6 +25,7 @@ export const links: LinkItem = {
|
|||
}),
|
||||
],
|
||||
id: SecurityPageName.attackDiscovery,
|
||||
licenseType: 'enterprise',
|
||||
path: ATTACK_DISCOVERY_PATH,
|
||||
title: ATTACK_DISCOVERY,
|
||||
};
|
||||
|
|
|
@ -68,30 +68,6 @@ describe('EmptyPrompt', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when the user does NOT have the assistant privilege', () => {
|
||||
it('disables the generate button when the user does NOT have the assistant privilege', () => {
|
||||
(useAssistantAvailability as jest.Mock).mockReturnValue({
|
||||
hasAssistantPrivilege: false, // <-- the user does NOT have the assistant privilege
|
||||
isAssistantEnabled: true,
|
||||
});
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<EmptyPrompt
|
||||
alertsCount={alertsCount}
|
||||
isLoading={false}
|
||||
isDisabled={false}
|
||||
onGenerate={onGenerate}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
const generateButton = screen.getByTestId('generate');
|
||||
|
||||
expect(generateButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when loading is true', () => {
|
||||
const isLoading = true;
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@ import {
|
|||
import { css } from '@emotion/react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { useAssistantAvailability } from '../../../assistant/use_assistant_availability';
|
||||
import { AnimatedCounter } from './animated_counter';
|
||||
import * as i18n from './translations';
|
||||
|
||||
|
@ -39,7 +38,6 @@ const EmptyPromptComponent: React.FC<Props> = ({
|
|||
onGenerate,
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const { hasAssistantPrivilege } = useAssistantAvailability();
|
||||
const title = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup
|
||||
|
@ -112,7 +110,7 @@ const EmptyPromptComponent: React.FC<Props> = ({
|
|||
);
|
||||
|
||||
const actions = useMemo(() => {
|
||||
const disabled = !hasAssistantPrivilege || isLoading || isDisabled;
|
||||
const disabled = isLoading || isDisabled;
|
||||
|
||||
return (
|
||||
<EuiToolTip
|
||||
|
@ -129,7 +127,7 @@ const EmptyPromptComponent: React.FC<Props> = ({
|
|||
</EuiButton>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}, [hasAssistantPrivilege, isDisabled, isLoading, onGenerate]);
|
||||
}, [isDisabled, isLoading, onGenerate]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
|
|
|
@ -91,32 +91,6 @@ describe('Header', () => {
|
|||
expect(onGenerate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('disables the generate button when the user does NOT have the assistant privilege', () => {
|
||||
(useAssistantAvailability as jest.Mock).mockReturnValue({
|
||||
hasAssistantPrivilege: false,
|
||||
isAssistantEnabled: true,
|
||||
});
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<Header
|
||||
stats={null}
|
||||
connectorId="testConnectorId"
|
||||
connectorsAreConfigured={true}
|
||||
isDisabledActions={false}
|
||||
isLoading={false}
|
||||
onCancel={jest.fn()}
|
||||
onConnectorIdSelected={jest.fn()}
|
||||
onGenerate={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
const generate = screen.getByTestId('generate');
|
||||
|
||||
expect(generate).toBeDisabled();
|
||||
});
|
||||
|
||||
it('displays the cancel button when loading', () => {
|
||||
const isLoading = true;
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|||
|
||||
import type { AttackDiscoveryStats } from '@kbn/elastic-assistant-common';
|
||||
import { StatusBell } from './status_bell';
|
||||
import { useAssistantAvailability } from '../../../assistant/use_assistant_availability';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface Props {
|
||||
|
@ -38,9 +37,8 @@ const HeaderComponent: React.FC<Props> = ({
|
|||
onCancel,
|
||||
stats,
|
||||
}) => {
|
||||
const { hasAssistantPrivilege } = useAssistantAvailability();
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const disabled = !hasAssistantPrivilege || connectorId == null;
|
||||
const disabled = connectorId == null;
|
||||
|
||||
const [didCancel, setDidCancel] = useState(false);
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ import { mockCasesContext } from '@kbn/cases-plugin/public/mocks/mock_cases_cont
|
|||
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
|
||||
import { createFilterManagerMock } from '@kbn/data-plugin/public/query/filter_manager/filter_manager.mock';
|
||||
import { createStubDataView } from '@kbn/data-views-plugin/common/data_view.stub';
|
||||
import type { AssistantAvailability } from '@kbn/elastic-assistant';
|
||||
import { UpsellingService } from '@kbn/security-solution-upselling/service';
|
||||
import { Router } from '@kbn/shared-ux-router';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
@ -17,7 +16,6 @@ import React from 'react';
|
|||
import { useLocalStorage } from 'react-use';
|
||||
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import { MockAssistantProvider } from '../../common/mock/mock_assistant_provider';
|
||||
import { ATTACK_DISCOVERY_PATH } from '../../../common/constants';
|
||||
import { mockHistory } from '../../common/utils/route/mocks';
|
||||
import { AttackDiscoveryPage } from '.';
|
||||
|
@ -551,52 +549,4 @@ describe('AttackDiscovery', () => {
|
|||
expect(screen.queryByTestId('upgrade')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user does not have an Enterprise license', () => {
|
||||
const assistantUnavailable: AssistantAvailability = {
|
||||
hasAssistantPrivilege: false,
|
||||
hasConnectorsAllPrivilege: true,
|
||||
hasConnectorsReadPrivilege: true,
|
||||
hasUpdateAIAssistantAnonymization: false,
|
||||
isAssistantEnabled: false, // <-- non-Enterprise license
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<Router history={historyMock}>
|
||||
<UpsellingProvider upsellingService={mockUpselling}>
|
||||
<MockAssistantProvider assistantAvailability={assistantUnavailable}>
|
||||
<AttackDiscoveryPage />
|
||||
</MockAssistantProvider>
|
||||
</UpsellingProvider>
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
});
|
||||
|
||||
it('does NOT render the animated logo', () => {
|
||||
expect(screen.queryByTestId('animatedLogo')).toBeNull();
|
||||
});
|
||||
|
||||
it('does NOT render the header', () => {
|
||||
expect(screen.queryByTestId('header')).toBeNull();
|
||||
});
|
||||
|
||||
it('does NOT render the summary', () => {
|
||||
expect(screen.queryByTestId('summary')).toBeNull();
|
||||
});
|
||||
|
||||
it('does NOT render attack discoveries', () => {
|
||||
expect(screen.queryAllByTestId('attackDiscovery')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does NOT render the loading callout', () => {
|
||||
expect(screen.queryByTestId('loadingCallout')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders the upgrade call to action', () => {
|
||||
expect(screen.getByTestId('upgrade')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,7 +18,6 @@ import { uniq } from 'lodash/fp';
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
|
||||
import { SecurityRoutePageWrapper } from '../../common/components/security_route_page_wrapper';
|
||||
import { SecurityPageName } from '../../../common/constants';
|
||||
import { HeaderPage } from '../../common/components/header_page';
|
||||
import { useSpaceId } from '../../common/hooks/use_space_id';
|
||||
|
@ -35,17 +34,12 @@ import { EmptyStates } from './empty_states';
|
|||
import { LoadingCallout } from './loading_callout';
|
||||
import { PageTitle } from './page_title';
|
||||
import { Summary } from './summary';
|
||||
import { Upgrade } from './upgrade';
|
||||
import { useAttackDiscovery } from '../use_attack_discovery';
|
||||
|
||||
const AttackDiscoveryPageComponent: React.FC = () => {
|
||||
const spaceId = useSpaceId() ?? 'default';
|
||||
|
||||
const {
|
||||
assistantAvailability: { isAssistantEnabled },
|
||||
http,
|
||||
knowledgeBase,
|
||||
} = useAssistantContext();
|
||||
const { http, knowledgeBase } = useAssistantContext();
|
||||
const { data: aiConnectors } = useLoadConnectors({
|
||||
http,
|
||||
});
|
||||
|
@ -144,18 +138,10 @@ const AttackDiscoveryPageComponent: React.FC = () => {
|
|||
}, [aiConnectors]);
|
||||
|
||||
const animatedLogo = useMemo(() => <EuiLoadingLogo logo="logoSecurity" size="xl" />, []);
|
||||
|
||||
const connectorsAreConfigured = aiConnectors != null && aiConnectors.length > 0;
|
||||
const attackDiscoveriesCount = selectedConnectorAttackDiscoveries.length;
|
||||
|
||||
if (!isAssistantEnabled) {
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="xxl" />
|
||||
<Upgrade />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
css={css`
|
||||
|
@ -165,10 +151,7 @@ const AttackDiscoveryPageComponent: React.FC = () => {
|
|||
`}
|
||||
data-test-subj="fullHeightContainer"
|
||||
>
|
||||
<SecurityRoutePageWrapper
|
||||
data-test-subj="attackDiscoveryPage"
|
||||
pageName={SecurityPageName.attackDiscovery}
|
||||
>
|
||||
<div data-test-subj="attackDiscoveryPage">
|
||||
<HeaderPage border title={pageTitle}>
|
||||
<Header
|
||||
connectorId={connectorId}
|
||||
|
@ -252,7 +235,7 @@ const AttackDiscoveryPageComponent: React.FC = () => {
|
|||
</>
|
||||
)}
|
||||
<SpyRoute pageName={SecurityPageName.attackDiscovery} />
|
||||
</SecurityRoutePageWrapper>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -15,7 +15,13 @@ const PageTitleComponent: React.FC = () => {
|
|||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" data-test-subj="pageTitle" gutterSize="none">
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
data-test-subj="pageTitle"
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
wrap={true}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle data-test-subj="attackDiscoveryPageTitle" size="l">
|
||||
<h1>{i18n.ATTACK_DISCOVERY_PAGE_TITLE}</h1>
|
||||
|
@ -23,14 +29,10 @@ const PageTitleComponent: React.FC = () => {
|
|||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
css={css`
|
||||
vertical-align: middle;
|
||||
padding-left: ${euiTheme.size.m};
|
||||
* {
|
||||
vertical-align: middle;
|
||||
}
|
||||
margin: ${euiTheme.size.m} 0 0 ${euiTheme.size.m};
|
||||
`}
|
||||
grow={false}
|
||||
>
|
||||
<EuiBetaBadge
|
||||
iconType={'beaker'}
|
||||
|
@ -39,7 +41,10 @@ const PageTitleComponent: React.FC = () => {
|
|||
size="m"
|
||||
color="hollow"
|
||||
css={css`
|
||||
margin-bottom: ${euiTheme.size.s};
|
||||
.euiBetaBadge__icon {
|
||||
position: relative;
|
||||
top: 5px;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
/*
|
||||
* 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 { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { Upgrade } from '.';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import {
|
||||
ATTACK_DISCOVERY_IS_AVAILABLE,
|
||||
FIND_POTENTIAL_ATTACKS_WITH_AI,
|
||||
PLEASE_UPGRADE,
|
||||
} from './translations';
|
||||
|
||||
describe('Upgrade', () => {
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<Upgrade />
|
||||
</TestProviders>
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the assistant avatar', () => {
|
||||
const assistantAvatar = screen.getByTestId('assistantAvatar');
|
||||
|
||||
expect(assistantAvatar).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the expected upgrade title', () => {
|
||||
const upgradeTitle = screen.getByTestId('upgradeTitle');
|
||||
|
||||
expect(upgradeTitle).toHaveTextContent(FIND_POTENTIAL_ATTACKS_WITH_AI);
|
||||
});
|
||||
|
||||
it('renders the attack discovery availability text', () => {
|
||||
const attackDiscoveryIsAvailable = screen.getByTestId('attackDiscoveryIsAvailable');
|
||||
|
||||
expect(attackDiscoveryIsAvailable).toHaveTextContent(ATTACK_DISCOVERY_IS_AVAILABLE);
|
||||
});
|
||||
|
||||
it('renders the please upgrade text', () => {
|
||||
const pleaseUpgrade = screen.getByTestId('pleaseUpgrade');
|
||||
|
||||
expect(pleaseUpgrade).toHaveTextContent(PLEASE_UPGRADE);
|
||||
});
|
||||
|
||||
it('renders the upgrade subscription plans (docs) link', () => {
|
||||
const upgradeDocs = screen.getByRole('link', { name: 'Subscription plans' });
|
||||
|
||||
expect(upgradeDocs).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the upgrade Manage license call to action', () => {
|
||||
const upgradeCta = screen.getByRole('link', { name: 'Manage license' });
|
||||
|
||||
expect(upgradeCta).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -1,36 +0,0 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const FIND_POTENTIAL_ATTACKS_WITH_AI = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.upgrade.findPotentialAttacksWithAiTitle',
|
||||
{
|
||||
defaultMessage: 'Find potential attacks with AI',
|
||||
}
|
||||
);
|
||||
|
||||
export const ATTACK_DISCOVERY_IS_AVAILABLE = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.upgrade.attackDiscoveryIsAvailable',
|
||||
{
|
||||
defaultMessage: 'Your license does not support Attack discovery.',
|
||||
}
|
||||
);
|
||||
|
||||
export const PLEASE_UPGRADE = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.upgrade.pleaseUpgradeMessage',
|
||||
{
|
||||
defaultMessage: 'Please upgrade your license to use this feature.',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPGRADE = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.upgrade.upgradeButton',
|
||||
{
|
||||
defaultMessage: 'Upgrade',
|
||||
}
|
||||
);
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { TrackApplicationView } from '@kbn/usage-collection-plugin/public';
|
||||
import { AttackDiscoveryPage } from './pages';
|
||||
|
||||
import type { SecuritySubPluginRoutes } from '../app/types';
|
||||
|
@ -17,11 +16,9 @@ import { SecurityRoutePageWrapper } from '../common/components/security_route_pa
|
|||
|
||||
export const AttackDiscoveryRoutes = () => (
|
||||
<PluginTemplateWrapper>
|
||||
<TrackApplicationView viewId={SecurityPageName.attackDiscovery}>
|
||||
<SecurityRoutePageWrapper pageName={SecurityPageName.attackDiscovery}>
|
||||
<AttackDiscoveryPage />
|
||||
</SecurityRoutePageWrapper>
|
||||
</TrackApplicationView>
|
||||
<SecurityRoutePageWrapper pageName={SecurityPageName.attackDiscovery}>
|
||||
<AttackDiscoveryPage />
|
||||
</SecurityRoutePageWrapper>
|
||||
</PluginTemplateWrapper>
|
||||
);
|
||||
|
||||
|
|
|
@ -5,15 +5,24 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { useLoadConnectors } from '@kbn/elastic-assistant';
|
||||
import { useFetchAnonymizationFields } from '@kbn/elastic-assistant/impl/assistant/api/anonymization_fields/use_fetch_anonymization_fields';
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import React from 'react';
|
||||
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { usePollApi } from '../hooks/use_poll_api';
|
||||
import { useAttackDiscovery } from '.';
|
||||
import { ERROR_GENERATING_ATTACK_DISCOVERIES } from '../pages/translations';
|
||||
import { useKibana as mockUseKibana } from '../../common/lib/kibana/__mocks__';
|
||||
|
||||
jest.mock('../../assistant/use_assistant_availability', () => ({
|
||||
useAssistantAvailability: jest.fn(() => ({
|
||||
hasAssistantPrivilege: true,
|
||||
isAssistantEnabled: true,
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'@kbn/elastic-assistant/impl/assistant/api/anonymization_fields/use_fetch_anonymization_fields'
|
||||
);
|
||||
|
@ -40,10 +49,10 @@ jest.mock('@kbn/elastic-assistant', () => ({
|
|||
latestAlerts: 20,
|
||||
},
|
||||
}),
|
||||
useLoadConnectors: () => ({
|
||||
useLoadConnectors: jest.fn(() => ({
|
||||
isFetched: true,
|
||||
data: mockConnectors,
|
||||
}),
|
||||
})),
|
||||
}));
|
||||
const mockAttackDiscoveryPost = {
|
||||
timestamp: '2024-06-13T17:50:59.409Z',
|
||||
|
@ -224,4 +233,26 @@ describe('useAttackDiscovery', () => {
|
|||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.lastUpdated).toEqual(null);
|
||||
});
|
||||
|
||||
describe('when zero connectors are configured', () => {
|
||||
beforeEach(() => {
|
||||
(useLoadConnectors as jest.Mock).mockReturnValue({
|
||||
isFetched: true,
|
||||
data: [], // <-- zero connectors configured
|
||||
});
|
||||
|
||||
renderHook(() => useAttackDiscovery({ connectorId: 'test-id', setLoadingConnectorId }));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(useLoadConnectors as jest.Mock).mockReturnValue({
|
||||
isFetched: true,
|
||||
data: mockConnectors,
|
||||
});
|
||||
});
|
||||
|
||||
it('does NOT call pollApi when zero connectors are configured', () => {
|
||||
expect(mockPollApi.pollApi).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -109,7 +109,12 @@ export const useAttackDiscovery = ({
|
|||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (connectorId != null && connectorId !== '') {
|
||||
if (
|
||||
connectorId != null &&
|
||||
connectorId !== '' &&
|
||||
aiConnectors != null &&
|
||||
aiConnectors.length > 0
|
||||
) {
|
||||
pollApi();
|
||||
setLoadingConnectorId?.(connectorId);
|
||||
setAlertsContextCount(null);
|
||||
|
@ -120,7 +125,7 @@ export const useAttackDiscovery = ({
|
|||
setGenerationIntervals([]);
|
||||
setPollStatus(null);
|
||||
}
|
||||
}, [pollApi, connectorId, setLoadingConnectorId, setPollStatus]);
|
||||
}, [aiConnectors, connectorId, pollApi, setLoadingConnectorId, setPollStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (pollStatus === 'running') {
|
||||
|
|
|
@ -65,6 +65,7 @@ viewer:
|
|||
- feature_siem.endpoint_list_read
|
||||
- feature_securitySolutionCases.read
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
- feature_builtInAlerts.read
|
||||
- feature_osquery.read
|
||||
|
@ -143,6 +144,7 @@ editor:
|
|||
- feature_siem.file_operations_all
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
- feature_builtInAlerts.all
|
||||
- feature_osquery.all
|
||||
|
@ -191,6 +193,7 @@ t1_analyst:
|
|||
- feature_siem.endpoint_list_read
|
||||
- feature_securitySolutionCases.read
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
- feature_builtInAlerts.read
|
||||
- feature_osquery.read
|
||||
|
@ -245,6 +248,7 @@ t2_analyst:
|
|||
- feature_siem.endpoint_list_read
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
- feature_builtInAlerts.read
|
||||
- feature_osquery.read
|
||||
|
@ -314,6 +318,7 @@ t3_analyst:
|
|||
- feature_siem.scan_operations_all
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
- feature_builtInAlerts.all
|
||||
- feature_osquery.all
|
||||
|
@ -370,6 +375,7 @@ threat_intelligence_analyst:
|
|||
- feature_siem.blocklist_all
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
- feature_builtInAlerts.read
|
||||
- feature_osquery.all
|
||||
|
@ -437,6 +443,7 @@ rule_author:
|
|||
- feature_siem.actions_log_management_read
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
- feature_builtInAlerts.all
|
||||
- feature_osquery.all
|
||||
|
@ -508,6 +515,7 @@ soc_manager:
|
|||
- feature_siem.scan_operations_all
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.all
|
||||
- feature_builtInAlerts.all
|
||||
- feature_osquery.all
|
||||
|
@ -567,6 +575,7 @@ detections_admin:
|
|||
- feature_siem.crud_alerts
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.all
|
||||
- feature_builtInAlerts.all
|
||||
- feature_dev_tools.all
|
||||
|
@ -625,6 +634,7 @@ platform_engineer:
|
|||
- feature_siem.actions_log_management_read
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.all
|
||||
- feature_builtInAlerts.all
|
||||
- feature_fleet.all
|
||||
|
@ -697,6 +707,7 @@ endpoint_operations_analyst:
|
|||
- feature_siem.scan_operations_all
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.all
|
||||
- feature_builtInAlerts.all
|
||||
- feature_osquery.all
|
||||
|
@ -763,6 +774,7 @@ endpoint_policy_manager:
|
|||
- feature_siem.blocklist_all # Elastic Defend Policy Management
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.all
|
||||
- feature_builtInAlerts.all
|
||||
- feature_osquery.all
|
||||
|
|
|
@ -31,6 +31,11 @@ jest.mock('@kbn/security-solution-features/product_features', () => ({
|
|||
baseKibanaSubFeatureIds: [],
|
||||
subFeaturesMap: new Map(),
|
||||
})),
|
||||
getAttackDiscoveryFeature: jest.fn(() => ({
|
||||
baseKibanaFeature: {},
|
||||
baseKibanaSubFeatureIds: [],
|
||||
subFeaturesMap: new Map(),
|
||||
})),
|
||||
}));
|
||||
|
||||
export const createProductFeaturesServiceMock = (
|
||||
|
@ -103,6 +108,25 @@ export const createProductFeaturesServiceMock = (
|
|||
])
|
||||
)
|
||||
),
|
||||
attackDiscovery: jest.fn().mockReturnValue(
|
||||
new Map(
|
||||
enabledFeatureKeys.map((key) => [
|
||||
key,
|
||||
{
|
||||
privileges: {
|
||||
all: {
|
||||
ui: ['entity-analytics'],
|
||||
api: [`test-entity-analytics`],
|
||||
},
|
||||
read: {
|
||||
ui: ['entity-analytics'],
|
||||
api: [`test-entity-analytics`],
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
)
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ const productFeature = {
|
|||
};
|
||||
const mockGetFeature = jest.fn().mockReturnValue(productFeature);
|
||||
jest.mock('@kbn/security-solution-features/product_features', () => ({
|
||||
getAttackDiscoveryFeature: () => mockGetFeature(),
|
||||
getAssistantFeature: () => mockGetFeature(),
|
||||
getCasesFeature: () => mockGetFeature(),
|
||||
getSecurityFeature: () => mockGetFeature(),
|
||||
|
@ -54,8 +55,8 @@ describe('ProductFeaturesService', () => {
|
|||
const experimentalFeatures = {} as ExperimentalFeatures;
|
||||
new ProductFeaturesService(loggerMock.create(), experimentalFeatures);
|
||||
|
||||
expect(mockGetFeature).toHaveBeenCalledTimes(3);
|
||||
expect(MockedProductFeatures).toHaveBeenCalledTimes(3);
|
||||
expect(mockGetFeature).toHaveBeenCalledTimes(4);
|
||||
expect(MockedProductFeatures).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
it('should init all ProductFeatures when initialized', () => {
|
||||
|
@ -86,8 +87,10 @@ describe('ProductFeaturesService', () => {
|
|||
const mockSecurityConfig = new Map() as ProductFeaturesConfig<SecuritySubFeatureId>;
|
||||
const mockCasesConfig = new Map() as ProductFeaturesConfig<CasesSubFeatureId>;
|
||||
const mockAssistantConfig = new Map() as ProductFeaturesConfig<AssistantSubFeatureId>;
|
||||
const mockAttackDiscoveryConfig = new Map() as ProductFeaturesConfig;
|
||||
|
||||
const configurator: ProductFeaturesConfigurator = {
|
||||
attackDiscovery: jest.fn(() => mockAttackDiscoveryConfig),
|
||||
security: jest.fn(() => mockSecurityConfig),
|
||||
cases: jest.fn(() => mockCasesConfig),
|
||||
securityAssistant: jest.fn(() => mockAssistantConfig),
|
||||
|
@ -97,6 +100,7 @@ describe('ProductFeaturesService', () => {
|
|||
expect(configurator.security).toHaveBeenCalled();
|
||||
expect(configurator.cases).toHaveBeenCalled();
|
||||
expect(configurator.securityAssistant).toHaveBeenCalled();
|
||||
expect(configurator.attackDiscovery).toHaveBeenCalled();
|
||||
|
||||
expect(MockedProductFeatures.mock.instances[0].setConfig).toHaveBeenCalledWith(
|
||||
mockSecurityConfig
|
||||
|
@ -105,6 +109,9 @@ describe('ProductFeaturesService', () => {
|
|||
expect(MockedProductFeatures.mock.instances[2].setConfig).toHaveBeenCalledWith(
|
||||
mockAssistantConfig
|
||||
);
|
||||
expect(MockedProductFeatures.mock.instances[3].setConfig).toHaveBeenCalledWith(
|
||||
mockAttackDiscoveryConfig
|
||||
);
|
||||
});
|
||||
|
||||
it('should return isEnabled for enabled features', () => {
|
||||
|
@ -127,8 +134,12 @@ describe('ProductFeaturesService', () => {
|
|||
const mockAssistantConfig = new Map([
|
||||
[ProductFeatureKey.assistant, {}],
|
||||
]) as ProductFeaturesConfig<AssistantSubFeatureId>;
|
||||
const mockAttackDiscoveryConfig = new Map([
|
||||
[ProductFeatureKey.attackDiscovery, {}],
|
||||
]) as ProductFeaturesConfig;
|
||||
|
||||
const configurator: ProductFeaturesConfigurator = {
|
||||
attackDiscovery: jest.fn(() => mockAttackDiscoveryConfig),
|
||||
security: jest.fn(() => mockSecurityConfig),
|
||||
cases: jest.fn(() => mockCasesConfig),
|
||||
securityAssistant: jest.fn(() => mockAssistantConfig),
|
||||
|
@ -139,6 +150,7 @@ describe('ProductFeaturesService', () => {
|
|||
expect(productFeaturesService.isEnabled(ProductFeatureKey.endpointExceptions)).toEqual(true);
|
||||
expect(productFeaturesService.isEnabled(ProductFeatureKey.casesConnectors)).toEqual(true);
|
||||
expect(productFeaturesService.isEnabled(ProductFeatureKey.assistant)).toEqual(true);
|
||||
expect(productFeaturesService.isEnabled(ProductFeatureKey.attackDiscovery)).toEqual(true);
|
||||
expect(productFeaturesService.isEnabled(ProductFeatureKey.externalRuleActions)).toEqual(false);
|
||||
});
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import type { FeaturesPluginSetup } from '@kbn/features-plugin/server';
|
|||
import type { ProductFeatureKeyType } from '@kbn/security-solution-features';
|
||||
import {
|
||||
getAssistantFeature,
|
||||
getAttackDiscoveryFeature,
|
||||
getCasesFeature,
|
||||
getSecurityFeature,
|
||||
} from '@kbn/security-solution-features/product_features';
|
||||
|
@ -31,6 +32,7 @@ export class ProductFeaturesService {
|
|||
private securityProductFeatures: ProductFeatures;
|
||||
private casesProductFeatures: ProductFeatures;
|
||||
private securityAssistantProductFeatures: ProductFeatures;
|
||||
private attackDiscoveryProductFeatures: ProductFeatures;
|
||||
private productFeatures?: Set<ProductFeatureKeyType>;
|
||||
|
||||
constructor(
|
||||
|
@ -67,12 +69,21 @@ export class ProductFeaturesService {
|
|||
assistantFeature.baseKibanaFeature,
|
||||
assistantFeature.baseKibanaSubFeatureIds
|
||||
);
|
||||
|
||||
const attackDiscoveryFeature = getAttackDiscoveryFeature();
|
||||
this.attackDiscoveryProductFeatures = new ProductFeatures(
|
||||
this.logger,
|
||||
attackDiscoveryFeature.subFeaturesMap,
|
||||
attackDiscoveryFeature.baseKibanaFeature,
|
||||
attackDiscoveryFeature.baseKibanaSubFeatureIds
|
||||
);
|
||||
}
|
||||
|
||||
public init(featuresSetup: FeaturesPluginSetup) {
|
||||
this.securityProductFeatures.init(featuresSetup);
|
||||
this.casesProductFeatures.init(featuresSetup);
|
||||
this.securityAssistantProductFeatures.init(featuresSetup);
|
||||
this.attackDiscoveryProductFeatures.init(featuresSetup);
|
||||
}
|
||||
|
||||
public setProductFeaturesConfigurator(configurator: ProductFeaturesConfigurator) {
|
||||
|
@ -85,11 +96,15 @@ export class ProductFeaturesService {
|
|||
const securityAssistantProductFeaturesConfig = configurator.securityAssistant();
|
||||
this.securityAssistantProductFeatures.setConfig(securityAssistantProductFeaturesConfig);
|
||||
|
||||
const attackDiscoveryProductFeaturesConfig = configurator.attackDiscovery();
|
||||
this.attackDiscoveryProductFeatures.setConfig(attackDiscoveryProductFeaturesConfig);
|
||||
|
||||
this.productFeatures = new Set<ProductFeatureKeyType>(
|
||||
Object.freeze([
|
||||
...securityProductFeaturesConfig.keys(),
|
||||
...casesProductFeaturesConfig.keys(),
|
||||
...securityAssistantProductFeaturesConfig.keys(),
|
||||
...attackDiscoveryProductFeaturesConfig.keys(),
|
||||
]) as readonly ProductFeatureKeyType[]
|
||||
);
|
||||
}
|
||||
|
@ -107,7 +122,8 @@ export class ProductFeaturesService {
|
|||
return (
|
||||
this.securityProductFeatures.isActionRegistered(action) ||
|
||||
this.casesProductFeatures.isActionRegistered(action) ||
|
||||
this.securityAssistantProductFeatures.isActionRegistered(action)
|
||||
this.securityAssistantProductFeatures.isActionRegistered(action) ||
|
||||
this.attackDiscoveryProductFeatures.isActionRegistered(action)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import type {
|
|||
} from '@kbn/security-solution-features/keys';
|
||||
|
||||
export interface ProductFeaturesConfigurator {
|
||||
attackDiscovery: () => ProductFeaturesConfig;
|
||||
security: () => ProductFeaturesConfig<SecuritySubFeatureId>;
|
||||
cases: () => ProductFeaturesConfig<CasesSubFeatureId>;
|
||||
securityAssistant: () => ProductFeaturesConfig<AssistantSubFeatureId>;
|
||||
|
|
|
@ -24,3 +24,9 @@ export const EntityAnalyticsUpsellingPageLazy = lazy(() =>
|
|||
default: EntityAnalyticsUpsellingPageESS,
|
||||
}))
|
||||
);
|
||||
|
||||
export const AttackDiscoveryUpsellingPageLazy = lazy(() =>
|
||||
import('./pages/attack_discovery').then(({ AttackDiscoveryUpsellingPageESS }) => ({
|
||||
default: AttackDiscoveryUpsellingPageESS,
|
||||
}))
|
||||
);
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
jest.mock('../../../common/services', () => ({
|
||||
useKibana: jest.fn(() => ({
|
||||
services: {
|
||||
application: {
|
||||
getUrlForApp: jest
|
||||
.fn()
|
||||
.mockReturnValue('http://localhost:5601/app/management/stack/license_management'),
|
||||
},
|
||||
http: {
|
||||
basePath: {
|
||||
get: () => 'some-base-path',
|
||||
},
|
||||
},
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
import { AttackDiscoveryUpsellingPageESS } from '.';
|
||||
|
||||
describe('AttackDiscoveryUpsellingPageESS', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
render(<AttackDiscoveryUpsellingPageESS />);
|
||||
});
|
||||
|
||||
it('renders the expected ESS-specific availability message', () => {
|
||||
const attackDiscoveryIsAvailable = screen.getByTestId('availabilityMessage');
|
||||
|
||||
expect(attackDiscoveryIsAvailable).toHaveTextContent(i18n.AVAILABILITY_MESSAGE);
|
||||
});
|
||||
|
||||
it('renders the expected ESS-specific upgrade message', () => {
|
||||
const pleaseUpgrade = screen.getByTestId('upgradeMessage');
|
||||
|
||||
expect(pleaseUpgrade).toHaveTextContent(i18n.UPGRADE_MESSAGE);
|
||||
});
|
||||
|
||||
it('renders the ESS-specific actions', () => {
|
||||
const actions = screen.getByTestId('essActions');
|
||||
|
||||
expect(actions).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { AttackDiscoveryUpsellingPage } from '@kbn/security-solution-upselling/pages/attack_discovery';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { UpgradeActions } from './upgrade_actions';
|
||||
import * as i18n from './translations';
|
||||
|
||||
/**
|
||||
* This component passes self-managed-specific upgrade actions and `i18n` to
|
||||
* the platform agnostic `AttackDiscoveryUpsellingPage` component.
|
||||
*/
|
||||
const AttackDiscoveryUpsellingPageESSComponent: React.FC = () => {
|
||||
const actions = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup data-test-subj="essActions" justifyContent="center" gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<UpgradeActions />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<AttackDiscoveryUpsellingPage
|
||||
actions={actions}
|
||||
availabilityMessage={i18n.AVAILABILITY_MESSAGE}
|
||||
upgradeMessage={i18n.UPGRADE_MESSAGE}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
AttackDiscoveryUpsellingPageESSComponent.displayName = 'AttackDiscoveryUpsellingPageESS';
|
||||
|
||||
export const AttackDiscoveryUpsellingPageESS = React.memo(AttackDiscoveryUpsellingPageESSComponent);
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const AVAILABILITY_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolutionEss.upselling.pages.attackDiscovery.availabilityMessage',
|
||||
{
|
||||
defaultMessage: 'Your license does not support Attack discovery.',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPGRADE_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolutionEss.upselling.pages.attackDiscovery.upgradeMessage',
|
||||
{
|
||||
defaultMessage: 'Please upgrade your license to use this feature.',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { UpgradeActions } from '.';
|
||||
|
||||
jest.mock('../../../../common/services', () => ({
|
||||
useKibana: jest.fn().mockReturnValue({
|
||||
services: {
|
||||
application: {
|
||||
getUrlForApp: jest
|
||||
.fn()
|
||||
.mockReturnValue('http://localhost:5601/app/management/stack/license_management'),
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('UpgradeActions', () => {
|
||||
beforeEach(() => {
|
||||
render(<UpgradeActions />);
|
||||
});
|
||||
|
||||
describe('upgrade docs button', () => {
|
||||
it('renders the expected button text', () => {
|
||||
expect(screen.getByTestId('upgradeDocs')).toHaveTextContent(i18n.UPGRADE_DOCS);
|
||||
});
|
||||
|
||||
it('opens the link in a new tab', () => {
|
||||
expect(screen.getByTestId('upgradeDocs')).toHaveAttribute('target', '_blank');
|
||||
});
|
||||
});
|
||||
|
||||
describe('upgrade call to action button', () => {
|
||||
it('renders the expected button text', () => {
|
||||
expect(screen.getByTestId('upgradeCta')).toHaveTextContent(i18n.UPGRADE_CTA);
|
||||
});
|
||||
|
||||
it('opens the license management page in a new tab', () => {
|
||||
expect(screen.getByTestId('upgradeCta')).toHaveAttribute('target', '_blank');
|
||||
});
|
||||
|
||||
it('links to the license management page', () => {
|
||||
expect(screen.getByTestId('upgradeCta')).toHaveAttribute(
|
||||
'href',
|
||||
'http://localhost:5601/app/management/stack/license_management'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import { useKibana } from '../../../../common/services';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const UpgradeActionsComponent = () => {
|
||||
const { services } = useKibana();
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
data-test-subj="upgradeButtons"
|
||||
gutterSize="s"
|
||||
justifyContent="spaceAround"
|
||||
wrap={true}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="upgradeDocs"
|
||||
href="https://www.elastic.co/subscriptions"
|
||||
iconType="popout"
|
||||
iconSide="right"
|
||||
target="_blank"
|
||||
>
|
||||
{i18n.UPGRADE_DOCS}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="upgradeCta"
|
||||
href={services.application.getUrlForApp('management', {
|
||||
path: 'stack/license_management',
|
||||
})}
|
||||
iconType="gear"
|
||||
target="_blank"
|
||||
>
|
||||
{i18n.UPGRADE_CTA}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export const UpgradeActions = React.memo(UpgradeActionsComponent);
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const UPGRADE_CTA = i18n.translate(
|
||||
'xpack.securitySolutionEss.upselling.pages.attackDiscovery.upgrade.upgradeTitle',
|
||||
{
|
||||
defaultMessage: 'Manage license',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPGRADE_DOCS = i18n.translate(
|
||||
'xpack.securitySolutionEss.upselling.pages.attackDiscovery.upgrade.upgradeButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Subscription plans',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 { SecurityPageName } from '@kbn/security-solution-plugin/common';
|
||||
|
||||
import { upsellingPages } from './register_upsellings';
|
||||
|
||||
describe('upsellingPages', () => {
|
||||
it('registers the Attack discovery page with the expected minimum license for self managed', () => {
|
||||
const attackDiscoveryPage = upsellingPages.find(
|
||||
({ pageName }) => pageName === SecurityPageName.attackDiscovery
|
||||
);
|
||||
|
||||
expect(attackDiscoveryPage?.minimumLicenseRequired).toEqual('enterprise');
|
||||
});
|
||||
});
|
|
@ -25,6 +25,7 @@ import type React from 'react';
|
|||
import type { Services } from '../common/services';
|
||||
import { withServicesProvider } from '../common/services';
|
||||
import {
|
||||
AttackDiscoveryUpsellingPageLazy,
|
||||
EntityAnalyticsUpsellingPageLazy,
|
||||
EntityAnalyticsUpsellingSectionLazy,
|
||||
} from './lazy_upselling';
|
||||
|
@ -92,6 +93,11 @@ export const upsellingPages: UpsellingPages = [
|
|||
minimumLicenseRequired: 'platinum',
|
||||
component: EntityAnalyticsUpsellingPageLazy,
|
||||
},
|
||||
{
|
||||
pageName: SecurityPageName.attackDiscovery,
|
||||
minimumLicenseRequired: 'enterprise',
|
||||
component: AttackDiscoveryUpsellingPageLazy,
|
||||
},
|
||||
];
|
||||
|
||||
// Upsellings for sections, linked by arbitrary ids
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 {
|
||||
ProductFeatureKeys,
|
||||
ProductFeatureKibanaConfig,
|
||||
ProductFeaturesAttackDiscoveryConfig,
|
||||
} from '@kbn/security-solution-features';
|
||||
import {
|
||||
attackDiscoveryDefaultProductFeaturesConfig,
|
||||
createEnabledProductFeaturesConfigMap,
|
||||
} from '@kbn/security-solution-features/config';
|
||||
import type { ProductFeatureAttackDiscoveryKey } from '@kbn/security-solution-features/keys';
|
||||
|
||||
/**
|
||||
* Maps the ProductFeatures keys to Kibana privileges that will be merged
|
||||
* into the base privileges config for the Security app.
|
||||
*
|
||||
* Privileges can be added in different ways:
|
||||
* - `privileges`: the privileges that will be added directly into the main Attack discovery feature.
|
||||
* - `subFeatureIds`: the ids of the sub-features that will be added into the Attack discovery subFeatures entry.
|
||||
* - `subFeaturesPrivileges`: the privileges that will be added into the existing Attack discovery subFeature with the privilege `id` specified.
|
||||
*/
|
||||
const attackDiscoveryProductFeaturesConfig: Record<
|
||||
ProductFeatureAttackDiscoveryKey,
|
||||
ProductFeatureKibanaConfig
|
||||
> = {
|
||||
...attackDiscoveryDefaultProductFeaturesConfig,
|
||||
// ess-specific app features configs here
|
||||
};
|
||||
|
||||
export const getAttackDiscoveryProductFeaturesConfigurator =
|
||||
(enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesAttackDiscoveryConfig =>
|
||||
createEnabledProductFeaturesConfigMap(
|
||||
attackDiscoveryProductFeaturesConfig,
|
||||
enabledProductFeatureKeys
|
||||
);
|
|
@ -10,11 +10,13 @@ import type { ProductFeaturesConfigurator } from '@kbn/security-solution-plugin/
|
|||
import { getCasesProductFeaturesConfigurator } from './cases_product_features_config';
|
||||
import { getSecurityProductFeaturesConfigurator } from './security_product_features_config';
|
||||
import { getSecurityAssistantProductFeaturesConfigurator } from './assistant_product_features_config';
|
||||
import { getAttackDiscoveryProductFeaturesConfigurator } from './attack_discovery_product_features_config';
|
||||
|
||||
export const getProductProductFeaturesConfigurator = (
|
||||
enabledProductFeatureKeys: ProductFeatureKeys
|
||||
): ProductFeaturesConfigurator => {
|
||||
return {
|
||||
attackDiscovery: getAttackDiscoveryProductFeaturesConfigurator(enabledProductFeatureKeys),
|
||||
security: getSecurityProductFeaturesConfigurator(enabledProductFeatureKeys),
|
||||
cases: getCasesProductFeaturesConfigurator(enabledProductFeatureKeys),
|
||||
securityAssistant: getSecurityAssistantProductFeaturesConfigurator(enabledProductFeatureKeys),
|
||||
|
|
|
@ -22,6 +22,7 @@ export const PLI_PRODUCT_FEATURES: PliProductFeatures = {
|
|||
complete: [
|
||||
ProductFeatureKey.advancedInsights,
|
||||
ProductFeatureKey.assistant,
|
||||
ProductFeatureKey.attackDiscovery,
|
||||
ProductFeatureKey.investigationGuide,
|
||||
ProductFeatureKey.investigationGuideInteractions,
|
||||
ProductFeatureKey.threatIntelligence,
|
||||
|
|
|
@ -40,3 +40,11 @@ export const EntityAnalyticsUpsellingSectionLazy = withSuspenseUpsell(
|
|||
)
|
||||
)
|
||||
);
|
||||
|
||||
export const AttackDiscoveryUpsellingPageLazy = withSuspenseUpsell(
|
||||
lazy(() =>
|
||||
import('./pages/attack_discovery').then(({ AttackDiscoveryUpsellingPageServerless }) => ({
|
||||
default: AttackDiscoveryUpsellingPageServerless,
|
||||
}))
|
||||
)
|
||||
);
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
jest.mock('../../../common/services', () => ({
|
||||
useKibana: jest.fn(() => ({
|
||||
services: {
|
||||
http: {
|
||||
basePath: {
|
||||
get: () => 'some-base-path',
|
||||
},
|
||||
},
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
import { AttackDiscoveryUpsellingPageServerless } from '.';
|
||||
|
||||
describe('AttackDiscoveryUpsellingPageServerless', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
render(<AttackDiscoveryUpsellingPageServerless />);
|
||||
});
|
||||
|
||||
it('renders the expected serverless-specific availability message', () => {
|
||||
const attackDiscoveryIsAvailable = screen.getByTestId('availabilityMessage');
|
||||
|
||||
expect(attackDiscoveryIsAvailable).toHaveTextContent(i18n.AVAILABILITY_MESSAGE);
|
||||
});
|
||||
|
||||
it('renders the expected serverless-specific upgrade message', () => {
|
||||
const pleaseUpgrade = screen.getByTestId('upgradeMessage');
|
||||
|
||||
expect(pleaseUpgrade).toHaveTextContent(i18n.UPGRADE_MESSAGE);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { AttackDiscoveryUpsellingPage } from '@kbn/security-solution-upselling/pages/attack_discovery';
|
||||
import React from 'react';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
/**
|
||||
* This component passes serverless-specific `i18n` to the platform agnostic
|
||||
* `AttackDiscoveryUpsellingPage` component.
|
||||
*/
|
||||
const AttackDiscoveryUpsellingPageServerlessComponent: React.FC = () => {
|
||||
return (
|
||||
<AttackDiscoveryUpsellingPage
|
||||
availabilityMessage={i18n.AVAILABILITY_MESSAGE}
|
||||
upgradeMessage={i18n.UPGRADE_MESSAGE}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
AttackDiscoveryUpsellingPageServerlessComponent.displayName =
|
||||
'AttackDiscoveryUpsellingPageServerless';
|
||||
|
||||
export const AttackDiscoveryUpsellingPageServerless = React.memo(
|
||||
AttackDiscoveryUpsellingPageServerlessComponent
|
||||
);
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const AVAILABILITY_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolutionServerless.upselling.pages.attackDiscovery.availabilityMessage',
|
||||
{
|
||||
defaultMessage: 'Your product tier does not support Attack discovery.',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPGRADE_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolutionServerless.upselling.pages.attackDiscovery.upgradeMessage',
|
||||
{
|
||||
defaultMessage: 'Please upgrade your product tier to use this feature.',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { SecurityPageName } from '@kbn/security-solution-plugin/common';
|
||||
import { ProductFeatureKey } from '@kbn/security-solution-features/keys';
|
||||
|
||||
import { upsellingPages } from './upsellings';
|
||||
|
||||
describe('upsellingPages', () => {
|
||||
it('registers the Attack discovery page with the Attack discovery PLI', () => {
|
||||
const attackDiscoveryPage = upsellingPages.find(
|
||||
({ pageName }) => pageName === SecurityPageName.attackDiscovery
|
||||
);
|
||||
|
||||
expect(attackDiscoveryPage?.pli).toEqual(ProductFeatureKey.attackDiscovery);
|
||||
});
|
||||
});
|
|
@ -25,6 +25,7 @@ import {
|
|||
} from './sections/endpoint_management';
|
||||
import { getProductTypeByPLI } from './hooks/use_product_type_by_pli';
|
||||
import {
|
||||
AttackDiscoveryUpsellingPageLazy,
|
||||
EndpointExceptionsDetailsUpsellingLazy,
|
||||
EntityAnalyticsUpsellingPageLazy,
|
||||
EntityAnalyticsUpsellingSectionLazy,
|
||||
|
@ -76,6 +77,11 @@ export const upsellingPages: UpsellingPages = [
|
|||
<EndpointExceptionsDetailsUpsellingLazy requiredPLI={ProductFeatureKey.endpointExceptions} />
|
||||
),
|
||||
},
|
||||
{
|
||||
pageName: SecurityPageName.attackDiscovery,
|
||||
pli: ProductFeatureKey.attackDiscovery,
|
||||
component: () => <AttackDiscoveryUpsellingPageLazy />,
|
||||
},
|
||||
];
|
||||
|
||||
const entityAnalyticsProductType = getProductTypeByPLI(ProductFeatureKey.advancedInsights) ?? '';
|
||||
|
|
|
@ -63,6 +63,7 @@ export class SecuritySolutionServerlessPlugin
|
|||
|
||||
// Register product features
|
||||
const enabledProductFeatures = getProductProductFeatures(this.config.productTypes);
|
||||
|
||||
registerProductFeatures(pluginsSetup, enabledProductFeatures, this.config);
|
||||
|
||||
// Register telemetry events
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 {
|
||||
ProductFeatureKeys,
|
||||
ProductFeatureKibanaConfig,
|
||||
ProductFeaturesAttackDiscoveryConfig,
|
||||
} from '@kbn/security-solution-features';
|
||||
import {
|
||||
attackDiscoveryDefaultProductFeaturesConfig,
|
||||
createEnabledProductFeaturesConfigMap,
|
||||
} from '@kbn/security-solution-features/config';
|
||||
import type { ProductFeatureAttackDiscoveryKey } from '@kbn/security-solution-features/keys';
|
||||
|
||||
/**
|
||||
* Maps the ProductFeatures keys to Kibana privileges that will be merged
|
||||
* into the base privileges config for the app.
|
||||
*
|
||||
* Privileges can be added in different ways:
|
||||
* - `privileges`: the privileges that will be added directly into the main Attack discovery feature.
|
||||
* - `subFeatureIds`: the ids of the sub-features that will be added into the Attack discovery subFeatures entry.
|
||||
* - `subFeaturesPrivileges`: the privileges that will be added into the existing Attack discovery subFeature with the privilege `id` specified.
|
||||
*/
|
||||
const attackDiscoveryProductFeaturesConfig: Record<
|
||||
ProductFeatureAttackDiscoveryKey,
|
||||
ProductFeatureKibanaConfig
|
||||
> = {
|
||||
...attackDiscoveryDefaultProductFeaturesConfig,
|
||||
// serverless-specific app features configs here
|
||||
};
|
||||
|
||||
export const getAttackDiscoveryProductFeaturesConfigurator =
|
||||
(enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesAttackDiscoveryConfig =>
|
||||
createEnabledProductFeaturesConfigMap(
|
||||
attackDiscoveryProductFeaturesConfig,
|
||||
enabledProductFeatureKeys
|
||||
);
|
|
@ -9,6 +9,7 @@ import type { Logger } from '@kbn/logging';
|
|||
|
||||
import { ProductFeatureKey } from '@kbn/security-solution-features/keys';
|
||||
import type { ProductFeatureKeys } from '@kbn/security-solution-features';
|
||||
import { getAttackDiscoveryProductFeaturesConfigurator } from './attack_discovery_product_features_config';
|
||||
import { getCasesProductFeaturesConfigurator } from './cases_product_features_config';
|
||||
import { getSecurityProductFeaturesConfigurator } from './security_product_features_config';
|
||||
import { getSecurityAssistantProductFeaturesConfigurator } from './assistant_product_features_config';
|
||||
|
@ -32,6 +33,7 @@ export const registerProductFeatures = (
|
|||
|
||||
// register product features for the main security solution product features service
|
||||
pluginsSetup.securitySolution.setProductFeaturesConfigurator({
|
||||
attackDiscovery: getAttackDiscoveryProductFeaturesConfigurator(enabledProductFeatureKeys),
|
||||
security: getSecurityProductFeaturesConfigurator(
|
||||
enabledProductFeatureKeys,
|
||||
config.experimentalFeatures
|
||||
|
|
|
@ -35882,10 +35882,6 @@
|
|||
"xpack.securitySolution.attackDiscovery.tour.video": "Regardez la vidéo de présentation",
|
||||
"xpack.securitySolution.attackDiscovery.tour.videoStep.desc": "Plongez dans les découvertes d'attaques axées sur les données et rationalisez votre flux de travail grâce à notre technologie d'IA intuitive, conçue pour accroître instantanément votre productivité.",
|
||||
"xpack.securitySolution.attackDiscovery.tour.videoStep.title": "Démarrez la découverte des attaques",
|
||||
"xpack.securitySolution.attackDiscovery.upgrade.attackDiscoveryIsAvailable": "Votre licence ne prend pas en charge la découverte d'attaques.",
|
||||
"xpack.securitySolution.attackDiscovery.upgrade.findPotentialAttacksWithAiTitle": "Trouvez les attaques potentielles grâce à l'IA",
|
||||
"xpack.securitySolution.attackDiscovery.upgrade.pleaseUpgradeMessage": "Veuillez mettre votre licence à niveau pour bénéficier de cette fonctionnalité.",
|
||||
"xpack.securitySolution.attackDiscovery.upgrade.upgradeButton": "Mettre à niveau",
|
||||
"xpack.securitySolution.auditd.abortedAuditStartupDescription": "démarrage de l'audit abandonné",
|
||||
"xpack.securitySolution.auditd.accessErrorDescription": "erreur d'accès",
|
||||
"xpack.securitySolution.auditd.accessPermissionDescription": "autorisation d'accès",
|
||||
|
|
|
@ -35866,10 +35866,6 @@
|
|||
"xpack.securitySolution.attackDiscovery.tour.video": "概要動画を視聴",
|
||||
"xpack.securitySolution.attackDiscovery.tour.videoStep.desc": "データ主導のAttack Discoveryを導入し、生産性を即時に高めるために設計されたElasticの直感的なAI技術でワークフローを合理化しましょう。",
|
||||
"xpack.securitySolution.attackDiscovery.tour.videoStep.title": "攻撃の検出を開始",
|
||||
"xpack.securitySolution.attackDiscovery.upgrade.attackDiscoveryIsAvailable": "ご使用のライセンスはAttack Discoveryをサポートしていません。",
|
||||
"xpack.securitySolution.attackDiscovery.upgrade.findPotentialAttacksWithAiTitle": "AIを利用して潜在的な攻撃を検出",
|
||||
"xpack.securitySolution.attackDiscovery.upgrade.pleaseUpgradeMessage": "この機能を使用するには、ライセンスをアップグレードしてください。",
|
||||
"xpack.securitySolution.attackDiscovery.upgrade.upgradeButton": "アップグレード",
|
||||
"xpack.securitySolution.auditd.abortedAuditStartupDescription": "中断された監査のスタートアップ",
|
||||
"xpack.securitySolution.auditd.accessErrorDescription": "アクセスエラー",
|
||||
"xpack.securitySolution.auditd.accessPermissionDescription": "アクセス権限",
|
||||
|
|
|
@ -35907,10 +35907,6 @@
|
|||
"xpack.securitySolution.attackDiscovery.tour.video": "观看概述视频",
|
||||
"xpack.securitySolution.attackDiscovery.tour.videoStep.desc": "深入了解数据驱动式 Attack Discovery,并利用旨在即时提高生产力的直观式 AI 技术精简您的工作流。",
|
||||
"xpack.securitySolution.attackDiscovery.tour.videoStep.title": "开始发现攻击",
|
||||
"xpack.securitySolution.attackDiscovery.upgrade.attackDiscoveryIsAvailable": "您的许可证不支持 Attack Discovery。",
|
||||
"xpack.securitySolution.attackDiscovery.upgrade.findPotentialAttacksWithAiTitle": "利用 AI 发现潜在攻击",
|
||||
"xpack.securitySolution.attackDiscovery.upgrade.pleaseUpgradeMessage": "请升级许可证以使用此功能。",
|
||||
"xpack.securitySolution.attackDiscovery.upgrade.upgradeButton": "升级",
|
||||
"xpack.securitySolution.auditd.abortedAuditStartupDescription": "已中止审计启动",
|
||||
"xpack.securitySolution.auditd.accessErrorDescription": "访问错误",
|
||||
"xpack.securitySolution.auditd.accessPermissionDescription": "访问权限",
|
||||
|
|
|
@ -128,6 +128,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'siem',
|
||||
'slo',
|
||||
'securitySolutionAssistant',
|
||||
'securitySolutionAttackDiscovery',
|
||||
'securitySolutionCases',
|
||||
'fleet',
|
||||
'fleetv2',
|
||||
|
|
|
@ -78,6 +78,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'minimal_read',
|
||||
'update_anonymization',
|
||||
],
|
||||
securitySolutionAttackDiscovery: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
securitySolutionCases: [
|
||||
'all',
|
||||
'read',
|
||||
|
|
|
@ -44,6 +44,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
ml: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
siem: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
securitySolutionAssistant: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
securitySolutionAttackDiscovery: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
securitySolutionCases: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
fleetv2: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
fleet: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
|
@ -161,6 +162,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'minimal_read',
|
||||
'update_anonymization',
|
||||
],
|
||||
securitySolutionAttackDiscovery: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
securitySolutionCases: [
|
||||
'all',
|
||||
'read',
|
||||
|
|
|
@ -64,6 +64,7 @@ export const secAll: Role = {
|
|||
feature: {
|
||||
siem: ['all'],
|
||||
securitySolutionAssistant: ['all'],
|
||||
securitySolutionAttackDiscovery: ['all'],
|
||||
securitySolutionCases: ['all'],
|
||||
actions: ['all'],
|
||||
actionsSimulators: ['all'],
|
||||
|
@ -96,6 +97,7 @@ export const secReadCasesAll: Role = {
|
|||
feature: {
|
||||
siem: ['read'],
|
||||
securitySolutionAssistant: ['all'],
|
||||
securitySolutionAttackDiscovery: ['all'],
|
||||
securitySolutionCases: ['all'],
|
||||
actions: ['all'],
|
||||
actionsSimulators: ['all'],
|
||||
|
@ -128,6 +130,7 @@ export const secAllCasesOnlyReadDelete: Role = {
|
|||
feature: {
|
||||
siem: ['all'],
|
||||
securitySolutionAssistant: ['all'],
|
||||
securitySolutionAttackDiscovery: ['all'],
|
||||
securitySolutionCases: ['cases_read', 'cases_delete'],
|
||||
actions: ['all'],
|
||||
actionsSimulators: ['all'],
|
||||
|
@ -160,6 +163,7 @@ export const secAllCasesNoDelete: Role = {
|
|||
feature: {
|
||||
siem: ['all'],
|
||||
securitySolutionAssistant: ['all'],
|
||||
securitySolutionAttackDiscovery: ['all'],
|
||||
securitySolutionCases: ['minimal_all'],
|
||||
actions: ['all'],
|
||||
actionsSimulators: ['all'],
|
||||
|
|
|
@ -82,6 +82,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
siem: 0,
|
||||
securitySolutionCases: 0,
|
||||
securitySolutionAssistant: 0,
|
||||
securitySolutionAttackDiscovery: 0,
|
||||
discover: 0,
|
||||
visualize: 0,
|
||||
dashboard: 0,
|
||||
|
|
|
@ -83,7 +83,8 @@ export default function navLinksTests({ getService }: FtrProviderContext) {
|
|||
'appSearch',
|
||||
'workplaceSearch',
|
||||
'guidedOnboardingFeature',
|
||||
'securitySolutionAssistant'
|
||||
'securitySolutionAssistant',
|
||||
'securitySolutionAttackDiscovery'
|
||||
)
|
||||
);
|
||||
break;
|
||||
|
|
|
@ -46,6 +46,7 @@ viewer:
|
|||
- feature_siem.endpoint_list_read
|
||||
- feature_securitySolutionCases.read
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
- feature_builtInAlerts.read
|
||||
- feature_osquery.read
|
||||
|
@ -124,6 +125,7 @@ editor:
|
|||
- feature_siem.file_operations_all
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
- feature_builtInAlerts.all
|
||||
- feature_osquery.all
|
||||
|
@ -172,6 +174,7 @@ t1_analyst:
|
|||
- feature_siem.endpoint_list_read
|
||||
- feature_securitySolutionCases.read
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
- feature_builtInAlerts.read
|
||||
- feature_osquery.read
|
||||
|
@ -226,6 +229,7 @@ t2_analyst:
|
|||
- feature_siem.endpoint_list_read
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
- feature_builtInAlerts.read
|
||||
- feature_osquery.read
|
||||
|
@ -295,6 +299,7 @@ t3_analyst:
|
|||
- feature_siem.scan_operations_all
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
- feature_builtInAlerts.all
|
||||
- feature_osquery.all
|
||||
|
@ -351,6 +356,7 @@ threat_intelligence_analyst:
|
|||
- feature_siem.blocklist_all
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
- feature_builtInAlerts.read
|
||||
- feature_osquery.all
|
||||
|
@ -418,6 +424,7 @@ rule_author:
|
|||
- feature_siem.actions_log_management_read
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
- feature_builtInAlerts.all
|
||||
- feature_osquery.all
|
||||
|
@ -489,6 +496,7 @@ soc_manager:
|
|||
- feature_siem.scan_operations_all
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.all
|
||||
- feature_builtInAlerts.all
|
||||
- feature_osquery.all
|
||||
|
@ -548,6 +556,7 @@ detections_admin:
|
|||
- feature_siem.crud_alerts
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.all
|
||||
- feature_builtInAlerts.all
|
||||
- feature_dev_tools.all
|
||||
|
@ -606,6 +615,7 @@ platform_engineer:
|
|||
- feature_siem.actions_log_management_read
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.all
|
||||
- feature_builtInAlerts.all
|
||||
- feature_fleet.all
|
||||
|
@ -678,6 +688,7 @@ endpoint_operations_analyst:
|
|||
- feature_siem.scan_operations_all
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.all
|
||||
- feature_builtInAlerts.all
|
||||
- feature_osquery.all
|
||||
|
@ -744,6 +755,7 @@ endpoint_policy_manager:
|
|||
- feature_siem.blocklist_all # Elastic Defend Policy Management
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.all
|
||||
- feature_builtInAlerts.all
|
||||
- feature_osquery.all
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue