[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:

![rbac](https://github.com/user-attachments/assets/2ca3de6e-3e87-401f-8a06-0eb06d36d081)

_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

![serverless_essentials_before](https://github.com/user-attachments/assets/90e8f433-896d-40a3-b095-8f0cca0f7073)

_Above: Before the fix, an upgrade CTA is NOT displayed for the serverless essentials product tier_

#### After

![serverless_essentials_after](https://github.com/user-attachments/assets/4cdd146e-afac-4f3e-925b-4786e1908312)

_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:

![privelages_required](https://github.com/user-attachments/assets/d282609e-5400-4ba9-8130-de5e10f8973d)

#### Self managed BASIC

![self_managed_basic_after](https://github.com/user-attachments/assets/048b2a3b-9e2d-4b95-a697-c739ea2dc5bb)

#### Self managed PLATINUM

![self_managed_platinum_after](https://github.com/user-attachments/assets/d7c49551-a8cf-4afb-b3bf-c3243e892219)

#### Self managed TRIAL

![self_managed_trial_after](https://github.com/user-attachments/assets/d5cc03a9-97aa-4c78-a5f5-92e5af3a85ac)

#### Self managed ENTERPRISE

![self_managed_enterprise_after](https://github.com/user-attachments/assets/a849b534-7e07-4481-9641-c48dee126466)

#### Serverless ESSENTIALS

![serverless_essentials_after](https://github.com/user-attachments/assets/4cdd146e-afac-4f3e-925b-4786e1908312)

#### Serverless COMPLETE

![serverless_complete_after](https://github.com/user-attachments/assets/8cab60c3-dea6-4d7a-b86a-b2cd11c9b4dd)

## 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:

![serverless_essentials_after](https://github.com/user-attachments/assets/4cdd146e-afac-4f3e-925b-4786e1908312)

- 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:

![serverless_essentials_before](https://github.com/user-attachments/assets/90e8f433-896d-40a3-b095-8f0cca0f7073)

### 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:
Andrew Macri 2024-08-13 11:46:20 -04:00 committed by GitHub
parent 571fe047c1
commit 8ee04937fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
73 changed files with 1192 additions and 289 deletions

View file

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

View file

@ -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"]

View file

@ -144,7 +144,7 @@ pageLoadAssetSize:
searchprofiler: 67080
security: 81771
securitySolution: 98429
securitySolutionEss: 16573
securitySolutionEss: 31781
securitySolutionServerless: 62488
serverless: 16573
serverlessObservability: 68747

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import 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>
);

View file

@ -0,0 +1,61 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { 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();
});
});

View file

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

View file

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

View file

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

View file

@ -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"]
},

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -24,3 +24,9 @@ export const EntityAnalyticsUpsellingPageLazy = lazy(() =>
default: EntityAnalyticsUpsellingPageESS,
}))
);
export const AttackDiscoveryUpsellingPageLazy = lazy(() =>
import('./pages/attack_discovery').then(({ AttackDiscoveryUpsellingPageESS }) => ({
default: AttackDiscoveryUpsellingPageESS,
}))
);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -22,6 +22,7 @@ export const PLI_PRODUCT_FEATURES: PliProductFeatures = {
complete: [
ProductFeatureKey.advancedInsights,
ProductFeatureKey.assistant,
ProductFeatureKey.attackDiscovery,
ProductFeatureKey.investigationGuide,
ProductFeatureKey.investigationGuideInteractions,
ProductFeatureKey.threatIntelligence,

View file

@ -40,3 +40,11 @@ export const EntityAnalyticsUpsellingSectionLazy = withSuspenseUpsell(
)
)
);
export const AttackDiscoveryUpsellingPageLazy = withSuspenseUpsell(
lazy(() =>
import('./pages/attack_discovery').then(({ AttackDiscoveryUpsellingPageServerless }) => ({
default: AttackDiscoveryUpsellingPageServerless,
}))
)
);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -63,6 +63,7 @@ export class SecuritySolutionServerlessPlugin
// Register product features
const enabledProductFeatures = getProductProductFeatures(this.config.productTypes);
registerProductFeatures(pluginsSetup, enabledProductFeatures, this.config);
// Register telemetry events

View file

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

View file

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

View file

@ -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",

View file

@ -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": "アクセス権限",

View file

@ -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": "访问权限",

View file

@ -128,6 +128,7 @@ export default function ({ getService }: FtrProviderContext) {
'siem',
'slo',
'securitySolutionAssistant',
'securitySolutionAttackDiscovery',
'securitySolutionCases',
'fleet',
'fleetv2',

View file

@ -78,6 +78,7 @@ export default function ({ getService }: FtrProviderContext) {
'minimal_read',
'update_anonymization',
],
securitySolutionAttackDiscovery: ['all', 'read', 'minimal_all', 'minimal_read'],
securitySolutionCases: [
'all',
'read',

View file

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

View file

@ -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'],

View file

@ -82,6 +82,7 @@ export default function ({ getService }: FtrProviderContext) {
siem: 0,
securitySolutionCases: 0,
securitySolutionAssistant: 0,
securitySolutionAttackDiscovery: 0,
discover: 0,
visualize: 0,
dashboard: 0,

View file

@ -83,7 +83,8 @@ export default function navLinksTests({ getService }: FtrProviderContext) {
'appSearch',
'workplaceSearch',
'guidedOnboardingFeature',
'securitySolutionAssistant'
'securitySolutionAssistant',
'securitySolutionAttackDiscovery'
)
);
break;

View file

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