From 1b167d9dc23a9e0e8e47992a37563ca89ccf3c7d Mon Sep 17 00:00:00 2001 From: Jan Monschke Date: Mon, 20 Jan 2025 14:09:16 +0100 Subject: [PATCH] [SecuritySolution] Breaking out timeline & note privileges (#201780) ## Summary Epic: https://github.com/elastic/security-team/issues/7998 In this PR we're breaking out the `timeline` and `notes` features into their own feature privilege definition. Previously, access to both features was granted implicitly through the `siem` feature. However, we found that this level of access control is not sufficient for all clients who wanted a more fine-grained way to grant access to parts of security solution. In order to break out `timeline` and `notes` from `siem`, we had to deprecate it feature privilege definition for. That is why you'll find plenty of changes of `siem` to `siemV2` in this PR. We're making use of the feature privilege's `replacedBy` functionality, allowing for a seamless migration of deprecated roles. This means that roles that previously granted `siem.all` are now granted `siemV2.all`, `timeline.all` and `notes.all` (same for `*.read`). Existing users are not impacted and should all still have the correct access. We added tests to make sure this is working as expected. Alongside the `ui` privileges, this PR also adds dedicated API tags. Those tags haven been added to the new and previous version of the privilege definitions to allow for a clean migration: ```mermaid flowchart LR subgraph v1 A(siem) --> Y(all) A --> X(read) Y -->|api| W(timeline_write / timeline_read / notes_read / notes_write) X -->|api| V(timeline_read /notes_read) end subgraph v2 A-->|replacedBy| C[siemV2] A-->|replacedBy| E[timeline] A-->|replacedBy| G[notes] E --> L(all) E --> M(read) L -->|api| N(timeline_write / timeline_read) M -->|api| P(timeline_read) G --> Q(all) G --> I(read) Q -->|api| R(notes_write / notes_read) I -->|api| S(notes_read) end ``` ### Visual changes #### Hidden/disabled elements Most of the changes are happening "under" the hood and are only expressed in case a user has a role with `timeline.none` or `notes.none`. This would hide and/or disable elements that would usually allow them to interact with either timeline or the notes feature (within timeline or the event flyout currently). As an example, this is how the hover actions look for a user with and without timeline access: | With timeline access | Without timeline access | | --- | --- | | Screenshot 2024-12-18 at 17 22 49 | Screenshot 2024-12-18 at 17 23 29 | #### Roles Another visible change of this PR is the addition of `Timeline` and `Notes` in the edit-role screen: | Before | After | | ------- | ------ | | Screenshot 2024-12-12 at 16 31 43 | Screenshot 2024-12-12 at 16 32 53 | We made sure that for migrated roles that hard `security.all` selected, this screen correctly shows `security.all`, `timeline.all` and `notes.all` after the privilege migration. #### Timeline toast There are tons of places in security solution where `Investigate / Add to timeline` are shown. We did our best to disable all of these actions but there is no guarantee that this PR catches all the places where we link to timeline (actions). One layer of extra protection is that the API endpoints don't give access to timelines to users without the correct privileges. Another one is a Redux middleware that makes sure timelines cannot be shown in missed cases. The following toast will be shown instead of the timeline: Screenshot 2024-12-19 at 10 34 23 ### Changes to predefined security roles All predefined security roles have been updated to grant the new privileges (in ESS and serverless). In accordance with the migration, all roles with `siem.all` have been assigned `siemV2.all`, `timeline.all` and `notes.all` (and `*.read` respectively). ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: PhilippeOberti Co-authored-by: Steph Milovic --- config/serverless.security.yml | 2 +- .../project_roles/security/roles.yml | 236 +++--- .../serverless_resources/security_roles.json | 28 +- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../paths/api@alerting@rule_types.yaml | 10 +- .../plugins/shared/fleet/common/authz.test.ts | 6 +- .../plugins/shared/fleet/common/authz.ts | 6 +- .../shared/fleet/common/constants/authz.ts | 2 +- .../security/packages/features/config.ts | 2 + .../packages/features/product_features.ts | 4 +- .../packages/features/src/constants.ts | 5 + .../packages/features/src/notes/index.ts | 16 + .../features/src/notes/kibana_features.ts | 53 ++ .../src/notes/product_feature_config.ts | 37 + .../features/src/product_features_keys.ts | 20 +- .../packages/features/src/security/index.ts | 20 +- .../security/v1_features/kibana_features.ts | 181 +++++ .../v1_features/kibana_sub_features.ts | 755 ++++++++++++++++++ .../{ => v2_features}/kibana_features.ts | 13 +- .../{ => v2_features}/kibana_sub_features.ts | 12 +- .../packages/features/src/timeline/index.ts | 16 + .../features/src/timeline/kibana_features.ts | 53 ++ .../src/timeline/product_feature_config.ts | 37 + .../security/packages/features/src/types.ts | 12 + .../public/pages/rules/rules.test.tsx | 2 +- .../pages/rules/rules_container.test.tsx | 2 +- .../security_solution/common/constants.ts | 4 +- .../plugins/security_solution/common/index.ts | 1 + .../common/test/ess_roles.json | 122 ++- .../security_solution/common/test/index.ts | 2 + .../common/types/header_actions/index.ts | 1 + .../cell_action/add_to_timeline.test.ts | 48 ++ .../cell_action/add_to_timeline.ts | 12 +- .../investigate_in_new_timeline.test.ts | 46 ++ .../investigate_in_new_timeline.ts | 8 +- .../discover/add_to_timeline.test.ts | 48 ++ .../discover/add_to_timeline.ts | 4 +- .../lens/add_to_timeline.test.ts | 22 +- .../add_to_timeline/lens/add_to_timeline.ts | 3 + .../public/app/app_routes.test.tsx | 16 +- .../public/app/app_routes.tsx | 10 +- .../links/sections/assets_links.ts | 6 +- .../links/sections/investigations_links.ts | 4 +- .../links/sections/ml_links.ts | 4 +- .../assistant/send_to_timeline/index.tsx | 9 +- .../public/attack_discovery/links.test.ts | 4 +- .../public/attack_discovery/links.ts | 6 +- .../attack_discovery/pages/index.test.tsx | 6 +- .../public/cases/pages/index.tsx | 9 +- .../public/cloud_defend/links.ts | 4 +- .../public/cloud_security_posture/links.ts | 4 +- .../control_columns/row_action/index.test.tsx | 40 + .../control_columns/row_action/index.tsx | 9 +- .../investigate_in_timeline_button.test.tsx | 21 + .../investigate_in_timeline_button.tsx | 22 +- .../events_tab/events_query_tab_body.test.tsx | 35 +- .../events_tab/events_query_tab_body.tsx | 24 +- .../header_actions/actions.test.tsx | 26 +- .../components/header_actions/actions.tsx | 3 +- .../add_note_icon_item.test.tsx | 10 +- .../header_actions/add_note_icon_item.tsx | 46 +- .../header_actions/pin_event_action.test.tsx | 4 +- .../header_actions/pin_event_action.tsx | 4 +- .../components/markdown_editor/editor.tsx | 7 +- .../markdown_editor/plugins/index.ts | 3 + .../plugins/timeline/plugin.tsx | 4 +- .../plugins/timeline/processor.tsx | 7 +- .../endpoint/use_endpoint_privileges.test.ts | 2 +- .../user_privileges_context.tsx | 42 +- .../common/lib/kibana/kibana_react.mock.ts | 8 +- .../public/common/links/links.test.tsx | 30 +- .../public/common/mock/create_store.ts | 5 +- .../public/common/mock/test_providers.tsx | 2 +- .../public/common/store/store.ts | 4 +- .../public/common/utils/notes_capabilities.ts | 13 + .../utils/timeline/use_show_timeline.test.tsx | 29 +- .../utils/timeline/use_show_timeline.tsx | 8 +- .../timeline/use_show_timeline_for_path.ts | 8 +- .../common/utils/timeline_capabilities.ts | 13 + .../public/dashboards/links.ts | 4 +- .../step_rule_actions/index.test.tsx | 2 +- .../step_define_rule/index.test.tsx | 5 + .../components/step_define_rule/index.tsx | 6 +- .../bulk_actions/use_bulk_actions.tsx | 7 +- .../alert_context_menu.test.tsx | 2 +- .../investigate_in_timeline_action.test.tsx | 22 + .../investigate_in_timeline_action.tsx | 7 +- .../use_investigate_in_timeline.test.tsx | 18 + .../use_investigate_in_timeline.tsx | 28 +- .../components/user_info/index.test.tsx | 4 +- .../alerts/use_alerts_privileges.test.tsx | 2 + .../use_actions_column.tsx | 15 +- .../public/detections/links.ts | 4 +- .../detection_engine.test.tsx | 4 +- .../use_risk_input_actions_panels.test.tsx | 26 + .../hooks/use_risk_input_actions_panels.tsx | 61 +- .../security_solution/public/explore/links.ts | 10 +- .../explore/network/pages/network.test.tsx | 2 +- .../attach_to_active_timeline.test.tsx | 2 +- .../left/components/notes_details.test.tsx | 6 +- .../left/components/notes_details.tsx | 4 +- .../components/prevalence_details.test.tsx | 54 +- .../left/components/prevalence_details.tsx | 54 +- .../left/components/test_ids.ts | 4 + .../flyout/document_details/left/index.tsx | 14 +- .../right/components/notes.test.tsx | 6 +- .../right/components/notes.tsx | 31 +- .../components/alert_count_insight.test.tsx | 37 +- .../shared/components/alert_count_insight.tsx | 46 +- .../components/take_action_dropdown.test.tsx | 29 +- .../shared/components/test_ids.ts | 6 +- .../security_solution/public/helper_hooks.tsx | 2 +- .../security_solution/public/helpers.test.tsx | 20 +- .../security_solution/public/helpers.tsx | 4 +- .../public/helpers_access.test.ts | 46 ++ .../public/helpers_access.ts | 15 + .../artifact_tabs_in_policy_details.cy.ts | 2 +- .../endpoints_rbac_mocked_data.cy.ts | 4 +- .../cypress/e2e/rbac/endpoint_role_rbac.cy.ts | 2 +- ...point_role_rbac_with_space_awareness.cy.ts | 4 +- .../role_with_artifact_read_privilege.ts | 4 +- .../screens/stack_management/role_page.ts | 12 +- .../public/management/links.ts | 8 +- .../view/ingest_manager_integration/mocks.tsx | 2 +- .../notes/components/notes_list.test.tsx | 6 +- .../public/notes/components/notes_list.tsx | 4 +- .../components/open_timeline_button.test.tsx | 15 + .../notes/components/open_timeline_button.tsx | 6 + .../notes/hooks/use_fetch_notes.test.ts | 19 + .../public/notes/hooks/use_fetch_notes.ts | 8 +- .../security_solution/public/notes/links.ts | 10 +- .../public/onboarding/links.ts | 4 +- .../alerts_by_status.test.tsx | 19 + .../alerts_by_status/alerts_by_status.tsx | 21 +- .../components/sidebar/sidebar.test.tsx | 64 +- .../overview/components/sidebar/sidebar.tsx | 14 +- .../public/overview/links.ts | 10 +- .../security_solution/public/plugin.tsx | 6 +- .../security_solution/public/rules/links.ts | 8 +- .../public/siem_migrations/links.ts | 4 +- .../sourcerer/containers/hooks.test.tsx | 2 +- .../public/threat_intelligence/links.ts | 4 +- .../public/threat_intelligence/routes.tsx | 12 +- .../threat_intelligence/translations.ts | 15 - .../add_to_favorites/index.test.tsx | 26 + .../components/add_to_favorites/index.tsx | 7 +- .../actions/save_timeline_button.test.tsx | 10 +- .../modal/actions/save_timeline_button.tsx | 2 +- .../timelines/components/notes/old_notes.tsx | 4 +- .../components/notes/save_timeline.test.tsx | 2 +- .../components/open_timeline/index.test.tsx | 43 +- .../components/open_timeline/index.tsx | 14 +- .../note_previews/index.test.tsx | 41 + .../open_timeline/note_previews/index.tsx | 31 +- .../open_timeline/open_timeline.test.tsx | 20 +- .../open_timeline/open_timeline.tsx | 8 +- .../open_timeline_modal/index.test.tsx | 6 + .../open_timeline/timelines_table/index.tsx | 6 +- .../timeline/properties/notes_flyout.tsx | 6 +- .../timeline/tabs/eql/index.test.tsx | 9 + .../components/timeline/tabs/esql/index.tsx | 7 +- .../components/timeline/tabs/index.test.tsx | 34 + .../components/timeline/tabs/index.tsx | 10 +- .../timeline/tabs/notes/index.test.tsx | 3 +- .../components/timeline/tabs/notes/index.tsx | 4 +- .../timeline/tabs/query/index.test.tsx | 2 + .../tabs/session/use_session_view.test.tsx | 2 +- .../use_timeline_control_columns.test.tsx | 125 ++- .../shared/use_timeline_control_columns.tsx | 18 +- .../data_table/control_column_cell_render.tsx | 4 +- .../timelines/containers/index.test.tsx | 12 +- .../public/timelines/links.ts | 11 +- .../public/timelines/pages/index.tsx | 28 +- .../timelines/pages/timelines_page.test.tsx | 46 +- .../public/timelines/pages/timelines_page.tsx | 32 +- .../create_timeline_middlewares.ts | 2 + .../middlewares/timeline_changed.test.ts | 6 +- .../middlewares/timeline_privileges.test.ts | 63 ++ .../store/middlewares/timeline_privileges.ts | 44 + .../common/roles_users/detections_engineer.ts | 4 +- .../endpoint_operations_analyst.ts | 4 +- .../endpoint_security_policy_manager.ts | 8 +- .../endpoint/common/roles_users/hunter.ts | 4 +- .../common/roles_users/platform_engineer.ts | 4 +- .../common/roles_users/rule_author.ts | 4 +- .../es_serverless_resources/roles.yml | 386 ++++----- .../common/roles_users/soc_manager.ts | 4 +- .../endpoint/common/roles_users/t1_analyst.ts | 4 +- .../endpoint/common/roles_users/t2_analyst.ts | 4 +- .../endpoint/common/roles_users/t3_analyst.ts | 4 +- .../threat_intelligence_analyst.ts | 4 +- .../with_artifact_read_privileges_role.ts | 4 +- .../roles_users/with_response_actions_role.ts | 6 +- .../without_response_actions_role.ts | 4 +- .../lib/product_features_service/mocks.ts | 49 ++ .../product_features_service.test.ts | 15 +- .../product_features_service.ts | 65 +- .../security_saved_objects.ts | 15 +- .../lib/product_features_service/types.ts | 2 + .../clean_draft_timelines/index.ts | 2 +- .../get_draft_timelines/index.ts | 2 +- .../lib/timeline/routes/notes/delete_note.ts | 2 +- .../lib/timeline/routes/notes/get_notes.ts | 2 +- .../lib/timeline/routes/notes/persist_note.ts | 2 +- .../pinned_events/persist_pinned_event.ts | 2 +- .../install_prepackaged_timelines/index.ts | 2 +- .../routes/timelines/copy_timeline/index.ts | 2 +- .../timelines/create_timelines/index.ts | 2 +- .../timelines/export_timelines/index.ts | 2 +- .../routes/timelines/get_timeline/index.ts | 2 +- .../routes/timelines/get_timelines/index.ts | 2 +- .../timelines/import_timelines/index.ts | 2 +- .../routes/timelines/patch_timelines/index.ts | 2 +- .../timelines/persist_favorite/index.ts | 2 +- .../timelines/resolve_timeline/index.ts | 2 +- .../security_solution/server/saved_objects.ts | 15 + .../server/product_features/index.ts | 4 + .../notes_product_features_config.ts | 37 + .../timeline_product_features_config.ts | 38 + .../server/product_features/index.ts | 4 + .../notes_product_features_config.ts | 37 + .../timeline_product_features_config.ts | 37 + .../public/mocks/mock_security_context.tsx | 1 + .../public/mocks/test_providers.tsx | 17 +- .../indicators/components/table/table.tsx | 11 +- .../components/add_to_timeline.test.tsx | 13 + .../timeline/components/add_to_timeline.tsx | 15 +- .../investigate_in_timeline.test.tsx | 11 + .../components/investigate_in_timeline.tsx | 9 +- .../threat_intelligence/public/types.ts | 5 + .../apis/cloud_security_posture/helper.ts | 2 +- .../apis/features/features/features.ts | 7 +- .../apis/security/privileges.ts | 27 + .../apis/security/privileges_basic.ts | 30 + .../routes/helper/user_roles_utilites.ts | 6 +- .../fleet_api_integration/apis/test_users.ts | 20 +- .../tests/features/deprecated_features.ts | 2 + .../config/privileges/roles.ts | 225 ++++++ .../config/privileges/users.ts | 89 +++ ...rity_solution_edr_workflows_roles_users.ts | 6 +- .../services/security_solution_ess_utils.ts | 60 +- .../security_solution_serverless_utils.ts | 29 +- .../config/services/types.ts | 42 + .../document_level_security.ts | 4 +- .../asset_criticality_privileges.ts | 2 +- .../risk_engine_privileges.ts | 2 +- .../entries/utils/auth/roles.ts | 16 +- .../investigation/timeline/tests/index.ts | 2 + .../timeline/tests/notes_privileges.ts | 119 +++ .../timeline/tests/timeline_privileges.ts | 274 +++++++ .../test_suites/investigation/utils/notes.ts | 12 + .../investigation/utils/timelines.ts | 80 +- .../lists/read_list_privileges.ts | 2 +- .../investigations/timelines/privileges.cy.ts | 65 ++ .../cypress/screens/search_bar.ts | 6 + .../cypress/tasks/privileges.ts | 16 +- x-pack/test/session_view/basic/tests/index.ts | 2 +- .../common/suites/create.ts | 3 + .../common/suites/get.ts | 3 + .../common/suites/get_all.ts | 3 + .../spaces_only/telemetry/telemetry.ts | 3 + .../platform_security/authorization.ts | 222 ++--- .../project_controller_security_roles.yml | 386 ++++----- 265 files changed, 5389 insertions(+), 1209 deletions(-) create mode 100644 x-pack/solutions/security/packages/features/src/notes/index.ts create mode 100644 x-pack/solutions/security/packages/features/src/notes/kibana_features.ts create mode 100644 x-pack/solutions/security/packages/features/src/notes/product_feature_config.ts create mode 100644 x-pack/solutions/security/packages/features/src/security/v1_features/kibana_features.ts create mode 100644 x-pack/solutions/security/packages/features/src/security/v1_features/kibana_sub_features.ts rename x-pack/solutions/security/packages/features/src/security/{ => v2_features}/kibana_features.ts (93%) rename x-pack/solutions/security/packages/features/src/security/{ => v2_features}/kibana_sub_features.ts (98%) create mode 100644 x-pack/solutions/security/packages/features/src/timeline/index.ts create mode 100644 x-pack/solutions/security/packages/features/src/timeline/kibana_features.ts create mode 100644 x-pack/solutions/security/packages/features/src/timeline/product_feature_config.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/common/utils/notes_capabilities.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline_capabilities.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/helpers_access.test.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/helpers_access.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/threat_intelligence/translations.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/timelines/store/middlewares/timeline_privileges.test.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/public/timelines/store/middlewares/timeline_privileges.ts create mode 100644 x-pack/solutions/security/plugins/security_solution_ess/server/product_features/notes_product_features_config.ts create mode 100644 x-pack/solutions/security/plugins/security_solution_ess/server/product_features/timeline_product_features_config.ts create mode 100644 x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/notes_product_features_config.ts create mode 100644 x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/timeline_product_features_config.ts create mode 100644 x-pack/test/security_solution_api_integration/config/privileges/roles.ts create mode 100644 x-pack/test/security_solution_api_integration/config/privileges/users.ts create mode 100644 x-pack/test/security_solution_api_integration/test_suites/investigation/timeline/tests/notes_privileges.ts create mode 100644 x-pack/test/security_solution_api_integration/test_suites/investigation/timeline/tests/timeline_privileges.ts create mode 100644 x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/privileges.cy.ts diff --git a/config/serverless.security.yml b/config/serverless.security.yml index f748c3ea5b7b2..79fe63a8c9d5c 100644 --- a/config/serverless.security.yml +++ b/config/serverless.security.yml @@ -24,7 +24,7 @@ xpack.features.overrides: category: "security" order: 1101 ### Security's feature privileges are fine-tuned to grant access to Discover, Dashboard, Maps, and Visualize apps. - siem: + siemV2: privileges: ### Security's `All` feature privilege should implicitly grant `All` access to Discover, Dashboard, Maps, and ### Visualize features. diff --git a/packages/kbn-es/src/serverless_resources/project_roles/security/roles.yml b/packages/kbn-es/src/serverless_resources/project_roles/security/roles.yml index 4993c4552eb88..0a4bc4878023e 100644 --- a/packages/kbn-es/src/serverless_resources/project_roles/security/roles.yml +++ b/packages/kbn-es/src/serverless_resources/project_roles/security/roles.yml @@ -43,12 +43,14 @@ viewer: - application: 'kibana-.kibana' privileges: - feature_ml.read - - feature_siem.read - - feature_siem.read_alerts - - feature_siem.endpoint_list_read + - feature_siemV2.read + - feature_siemV2.read_alerts + - feature_siemV2.endpoint_list_read - feature_securitySolutionCasesV2.read - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.read + - feature_securitySolutionNotes.read - feature_actions.read - feature_builtInAlerts.read - feature_osquery.read @@ -113,22 +115,24 @@ editor: - application: 'kibana-.kibana' privileges: - feature_ml.read - - feature_siem.all - - feature_siem.read_alerts - - feature_siem.crud_alerts - - feature_siem.endpoint_list_all - - feature_siem.trusted_applications_all - - feature_siem.event_filters_all - - feature_siem.host_isolation_exceptions_all - - feature_siem.blocklist_all - - feature_siem.policy_management_read # Elastic Defend Policy Management - - feature_siem.host_isolation_all - - feature_siem.process_operations_all - - feature_siem.actions_log_management_all # Response actions history - - feature_siem.file_operations_all + - feature_siemV2.all + - feature_siemV2.read_alerts + - feature_siemV2.crud_alerts + - feature_siemV2.endpoint_list_all + - feature_siemV2.trusted_applications_all + - feature_siemV2.event_filters_all + - feature_siemV2.host_isolation_exceptions_all + - feature_siemV2.blocklist_all + - feature_siemV2.policy_management_read # Elastic Defend Policy Management + - feature_siemV2.host_isolation_all + - feature_siemV2.process_operations_all + - feature_siemV2.actions_log_management_all # Response actions history + - feature_siemV2.file_operations_all - feature_securitySolutionCasesV2.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.all + - feature_securitySolutionNotes.all - feature_actions.read - feature_builtInAlerts.all - feature_osquery.all @@ -174,12 +178,14 @@ t1_analyst: - application: 'kibana-.kibana' privileges: - feature_ml.read - - feature_siem.read - - feature_siem.read_alerts - - feature_siem.endpoint_list_read + - feature_siemV2.read + - feature_siemV2.read_alerts + - feature_siemV2.endpoint_list_read - feature_securitySolutionCasesV2.read - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.read + - feature_securitySolutionNotes.read - feature_actions.read - feature_builtInAlerts.read - feature_osquery.read @@ -229,12 +235,14 @@ t2_analyst: - application: 'kibana-.kibana' privileges: - feature_ml.read - - feature_siem.read - - feature_siem.read_alerts - - feature_siem.endpoint_list_read + - feature_siemV2.read + - feature_siemV2.read_alerts + - feature_siemV2.endpoint_list_read - feature_securitySolutionCasesV2.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.read + - feature_securitySolutionNotes.read - feature_actions.read - feature_builtInAlerts.read - feature_osquery.read @@ -289,24 +297,26 @@ t3_analyst: - application: 'kibana-.kibana' privileges: - feature_ml.read - - feature_siem.all - - feature_siem.read_alerts - - feature_siem.crud_alerts - - feature_siem.endpoint_list_all - - feature_siem.trusted_applications_all - - feature_siem.event_filters_all - - feature_siem.host_isolation_exceptions_all - - feature_siem.blocklist_all - - feature_siem.policy_management_read # Elastic Defend Policy Management - - feature_siem.host_isolation_all - - feature_siem.process_operations_all - - feature_siem.actions_log_management_all # Response actions history - - feature_siem.file_operations_all - - feature_siem.scan_operations_all - - feature_siem.workflow_insights_all + - feature_siemV2.all + - feature_siemV2.read_alerts + - feature_siemV2.crud_alerts + - feature_siemV2.endpoint_list_all + - feature_siemV2.trusted_applications_all + - feature_siemV2.event_filters_all + - feature_siemV2.host_isolation_exceptions_all + - feature_siemV2.blocklist_all + - feature_siemV2.policy_management_read # Elastic Defend Policy Management + - feature_siemV2.host_isolation_all + - feature_siemV2.process_operations_all + - feature_siemV2.actions_log_management_all # Response actions history + - feature_siemV2.file_operations_all + - feature_siemV2.scan_operations_all + - feature_siemV2.workflow_insights_all - feature_securitySolutionCasesV2.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.all + - feature_securitySolutionNotes.all - feature_actions.read - feature_builtInAlerts.all - feature_osquery.all @@ -363,12 +373,14 @@ threat_intelligence_analyst: - application: 'kibana-.kibana' privileges: - feature_ml.read - - feature_siem.all - - feature_siem.endpoint_list_read - - feature_siem.blocklist_all + - feature_siemV2.all + - feature_siemV2.endpoint_list_read + - feature_siemV2.blocklist_all - feature_securitySolutionCasesV2.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.all + - feature_securitySolutionNotes.all - feature_actions.read - feature_builtInAlerts.read - feature_osquery.all @@ -424,20 +436,22 @@ rule_author: - application: 'kibana-.kibana' privileges: - feature_ml.read - - feature_siem.all - - feature_siem.read_alerts - - feature_siem.crud_alerts - - feature_siem.policy_management_all - - feature_siem.endpoint_list_all - - feature_siem.trusted_applications_all - - feature_siem.event_filters_all - - feature_siem.host_isolation_exceptions_read - - feature_siem.blocklist_all # Elastic Defend Policy Management - - feature_siem.actions_log_management_read - - feature_siem.workflow_insights_all + - feature_siemV2.all + - feature_siemV2.read_alerts + - feature_siemV2.crud_alerts + - feature_siemV2.policy_management_all + - feature_siemV2.endpoint_list_all + - feature_siemV2.trusted_applications_all + - feature_siemV2.event_filters_all + - feature_siemV2.host_isolation_exceptions_read + - feature_siemV2.blocklist_all # Elastic Defend Policy Management + - feature_siemV2.actions_log_management_read + - feature_siemV2.workflow_insights_all - feature_securitySolutionCasesV2.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.all + - feature_securitySolutionNotes.all - feature_actions.read - feature_builtInAlerts.all - feature_osquery.all @@ -493,25 +507,27 @@ soc_manager: - application: 'kibana-.kibana' privileges: - feature_ml.read - - feature_siem.all - - feature_siem.read_alerts - - feature_siem.crud_alerts - - feature_siem.policy_management_all - - feature_siem.endpoint_list_all - - feature_siem.trusted_applications_all - - feature_siem.event_filters_all - - feature_siem.host_isolation_exceptions_all - - feature_siem.blocklist_all - - feature_siem.host_isolation_all - - feature_siem.process_operations_all - - feature_siem.actions_log_management_all - - feature_siem.file_operations_all - - feature_siem.execute_operations_all - - feature_siem.scan_operations_all - - feature_siem.workflow_insights_all + - feature_siemV2.all + - feature_siemV2.read_alerts + - feature_siemV2.crud_alerts + - feature_siemV2.policy_management_all + - feature_siemV2.endpoint_list_all + - feature_siemV2.trusted_applications_all + - feature_siemV2.event_filters_all + - feature_siemV2.host_isolation_exceptions_all + - feature_siemV2.blocklist_all + - feature_siemV2.host_isolation_all + - feature_siemV2.process_operations_all + - feature_siemV2.actions_log_management_all + - feature_siemV2.file_operations_all + - feature_siemV2.execute_operations_all + - feature_siemV2.scan_operations_all + - feature_siemV2.workflow_insights_all - feature_securitySolutionCasesV2.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.all + - feature_securitySolutionNotes.all - feature_actions.all - feature_builtInAlerts.all - feature_osquery.all @@ -566,12 +582,14 @@ detections_admin: - application: 'kibana-.kibana' privileges: - feature_ml.all - - feature_siem.all - - feature_siem.read_alerts - - feature_siem.crud_alerts + - feature_siemV2.all + - feature_siemV2.read_alerts + - feature_siemV2.crud_alerts - feature_securitySolutionCasesV2.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.all + - feature_securitySolutionNotes.all - feature_actions.all - feature_builtInAlerts.all - feature_dev_tools.all @@ -618,20 +636,22 @@ platform_engineer: - application: 'kibana-.kibana' privileges: - feature_ml.all - - feature_siem.all - - feature_siem.read_alerts - - feature_siem.crud_alerts - - feature_siem.policy_management_all - - feature_siem.endpoint_list_all - - feature_siem.trusted_applications_all - - feature_siem.event_filters_all - - feature_siem.host_isolation_exceptions_all - - feature_siem.blocklist_all # Elastic Defend Policy Management - - feature_siem.actions_log_management_read - - feature_siem.workflow_insights_all + - feature_siemV2.all + - feature_siemV2.read_alerts + - feature_siemV2.crud_alerts + - feature_siemV2.policy_management_all + - feature_siemV2.endpoint_list_all + - feature_siemV2.trusted_applications_all + - feature_siemV2.event_filters_all + - feature_siemV2.host_isolation_exceptions_all + - feature_siemV2.blocklist_all # Elastic Defend Policy Management + - feature_siemV2.actions_log_management_read + - feature_siemV2.workflow_insights_all - feature_securitySolutionCasesV2.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.all + - feature_securitySolutionNotes.all - feature_actions.all - feature_builtInAlerts.all - feature_fleet.all @@ -688,24 +708,26 @@ endpoint_operations_analyst: - application: 'kibana-.kibana' privileges: - feature_ml.read - - feature_siem.all - - feature_siem.read_alerts - - feature_siem.policy_management_all - - feature_siem.endpoint_list_all - - feature_siem.trusted_applications_all - - feature_siem.event_filters_all - - feature_siem.host_isolation_exceptions_all - - feature_siem.blocklist_all - - feature_siem.host_isolation_all - - feature_siem.process_operations_all - - feature_siem.actions_log_management_all - - feature_siem.file_operations_all - - feature_siem.execute_operations_all - - feature_siem.scan_operations_all - - feature_siem.workflow_insights_all + - feature_siemV2.all + - feature_siemV2.read_alerts + - feature_siemV2.policy_management_all + - feature_siemV2.endpoint_list_all + - feature_siemV2.trusted_applications_all + - feature_siemV2.event_filters_all + - feature_siemV2.host_isolation_exceptions_all + - feature_siemV2.blocklist_all + - feature_siemV2.host_isolation_all + - feature_siemV2.process_operations_all + - feature_siemV2.actions_log_management_all + - feature_siemV2.file_operations_all + - feature_siemV2.execute_operations_all + - feature_siemV2.scan_operations_all + - feature_siemV2.workflow_insights_all - feature_securitySolutionCasesV2.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.all + - feature_securitySolutionNotes.all - feature_actions.all - feature_builtInAlerts.all - feature_osquery.all @@ -769,19 +791,21 @@ endpoint_policy_manager: - application: 'kibana-.kibana' privileges: - feature_ml.all - - feature_siem.all - - feature_siem.read_alerts - - feature_siem.crud_alerts - - feature_siem.policy_management_all - - feature_siem.endpoint_list_all - - feature_siem.trusted_applications_all - - feature_siem.event_filters_all - - feature_siem.host_isolation_exceptions_all - - feature_siem.blocklist_all # Elastic Defend Policy Management - - feature_siem.workflow_insights_all + - feature_siemV2.all + - feature_siemV2.read_alerts + - feature_siemV2.crud_alerts + - feature_siemV2.policy_management_all + - feature_siemV2.endpoint_list_all + - feature_siemV2.trusted_applications_all + - feature_siemV2.event_filters_all + - feature_siemV2.host_isolation_exceptions_all + - feature_siemV2.blocklist_all # Elastic Defend Policy Management + - feature_siemV2.workflow_insights_all - feature_securitySolutionCasesV2.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.all + - feature_securitySolutionNotes.all - feature_actions.all - feature_builtInAlerts.all - feature_osquery.all diff --git a/packages/kbn-es/src/serverless_resources/security_roles.json b/packages/kbn-es/src/serverless_resources/security_roles.json index 424cb898a4f96..6f85d0655dfb6 100644 --- a/packages/kbn-es/src/serverless_resources/security_roles.json +++ b/packages/kbn-es/src/serverless_resources/security_roles.json @@ -32,10 +32,12 @@ { "feature": { "ml": ["read"], - "siem": ["read", "read_alerts"], + "siemV2": ["read", "read_alerts"], "securitySolutionAssistant": ["all"], "securitySolutionAttackDiscovery": ["all"], "securitySolutionCasesV2": ["read"], + "securitySolutionTimeline": ["read"], + "securitySolutionNotes": ["read"], "actions": ["read"], "builtInAlerts": ["read"] }, @@ -79,10 +81,12 @@ { "feature": { "ml": ["read"], - "siem": ["read", "read_alerts"], + "siemV2": ["read", "read_alerts"], "securitySolutionAssistant": ["all"], "securitySolutionAttackDiscovery": ["all"], "securitySolutionCasesV2": ["read"], + "securitySolutionTimeline": ["read"], + "securitySolutionNotes": ["read"], "actions": ["read"], "builtInAlerts": ["read"] }, @@ -135,7 +139,7 @@ { "feature": { "ml": ["read"], - "siem": [ + "siemV2": [ "all", "read_alerts", "crud_alerts", @@ -153,6 +157,8 @@ "securitySolutionCasesV2": ["all"], "securitySolutionAssistant": ["all"], "securitySolutionAttackDiscovery": ["all"], + "securitySolutionTimeline": ["all"], + "securitySolutionNotes": ["all"], "actions": ["read"], "builtInAlerts": ["all"], "osquery": ["all"], @@ -207,10 +213,12 @@ { "feature": { "ml": ["read"], - "siem": ["all", "read_alerts", "crud_alerts"], + "siemV2": ["all", "read_alerts", "crud_alerts"], "securitySolutionAssistant": ["all"], "securitySolutionAttackDiscovery": ["all"], "securitySolutionCasesV2": ["all"], + "securitySolutionTimeline": ["all"], + "securitySolutionNotes": ["all"], "actions": ["read"], "builtInAlerts": ["all"] }, @@ -260,10 +268,12 @@ { "feature": { "ml": ["read"], - "siem": ["all", "read_alerts", "crud_alerts"], + "siemV2": ["all", "read_alerts", "crud_alerts"], "securitySolutionAssistant": ["all"], "securitySolutionAttackDiscovery": ["all"], "securitySolutionCasesV2": ["all"], + "securitySolutionTimeline": ["all"], + "securitySolutionNotes": ["all"], "actions": ["all"], "builtInAlerts": ["all"] }, @@ -308,10 +318,12 @@ { "feature": { "ml": ["all"], - "siem": ["all", "read_alerts", "crud_alerts"], + "siemV2": ["all", "read_alerts", "crud_alerts"], "securitySolutionAssistant": ["all"], "securitySolutionAttackDiscovery": ["all"], "securitySolutionCasesV2": ["all"], + "securitySolutionTimeline": ["all"], + "securitySolutionNotes": ["all"], "actions": ["read"], "builtInAlerts": ["all"], "dev_tools": ["all"] @@ -363,10 +375,12 @@ { "feature": { "ml": ["all"], - "siem": ["all", "read_alerts", "crud_alerts"], + "siemV2": ["all", "read_alerts", "crud_alerts"], "securitySolutionAssistant": ["all"], "securitySolutionAttackDiscovery": ["all"], "securitySolutionCasesV2": ["all"], + "securitySolutionTimeline": ["all"], + "securitySolutionNotes": ["all"], "actions": ["all"], "builtInAlerts": ["all"] }, diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index f905c77b9caf6..28cca16be8e87 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -40087,7 +40087,6 @@ "xpack.securitySolution.system.withResultDescription": "avec le résultat", "xpack.securitySolution.tables.rowItemHelper.moreDescription": "plus non affiché", "xpack.securitySolution.tables.rowItemHelper.overflowButtonDescription": "+ {count} de plus", - "xpack.securitySolution.threatIntelligence.investigateInTimelineTitle": "Investiguer dans la chronologie", "xpack.securitySolution.threatMatch.andDescription": "AND", "xpack.securitySolution.threatMatch.fieldDescription": "Champ", "xpack.securitySolution.threatMatch.fieldPlaceholderDescription": "Rechercher", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index 0b843ce3741e4..81dc2bd001f14 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -39947,7 +39947,6 @@ "xpack.securitySolution.system.withResultDescription": "結果付き", "xpack.securitySolution.tables.rowItemHelper.moreDescription": "行は表示されていません", "xpack.securitySolution.tables.rowItemHelper.overflowButtonDescription": "他{count}件", - "xpack.securitySolution.threatIntelligence.investigateInTimelineTitle": "タイムラインで調査", "xpack.securitySolution.threatMatch.andDescription": "AND", "xpack.securitySolution.threatMatch.fieldDescription": "フィールド", "xpack.securitySolution.threatMatch.fieldPlaceholderDescription": "検索", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index f436c50494879..c6d69d18c41fd 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -39366,7 +39366,6 @@ "xpack.securitySolution.system.withResultDescription": ",结果为", "xpack.securitySolution.tables.rowItemHelper.moreDescription": "未显示", "xpack.securitySolution.tables.rowItemHelper.overflowButtonDescription": "另外 {count} 个", - "xpack.securitySolution.threatIntelligence.investigateInTimelineTitle": "在时间线中调查", "xpack.securitySolution.threatMatch.andDescription": "且", "xpack.securitySolution.threatMatch.fieldDescription": "字段", "xpack.securitySolution.threatMatch.fieldPlaceholderDescription": "搜索", diff --git a/x-pack/platform/plugins/shared/alerting/docs/openapi/paths/api@alerting@rule_types.yaml b/x-pack/platform/plugins/shared/alerting/docs/openapi/paths/api@alerting@rule_types.yaml index 378a8e0a7127c..e97477e5c11fe 100644 --- a/x-pack/platform/plugins/shared/alerting/docs/openapi/paths/api@alerting@rule_types.yaml +++ b/x-pack/platform/plugins/shared/alerting/docs/openapi/paths/api@alerting@rule_types.yaml @@ -71,7 +71,7 @@ get: description: type: string name: - type: string + type: string alerts: type: object description: > @@ -119,7 +119,7 @@ get: description: > A secondary alias. It is typically used to support the signals alias for detection rules. - shouldWrite: + shouldWrite: type: boolean description: > Indicates whether the rule should write out alerts as data. @@ -212,7 +212,7 @@ get: all: type: boolean read: - type: boolean + type: boolean category: type: string description: The rule category, which is used by features such as category-specific maintenance windows. @@ -234,7 +234,7 @@ get: description: Indicates whether the rule type has custom mappings for the alert data. has_fields_for_a_a_d: type: boolean - id: + id: description: The unique identifier for the rule type. type: string is_exportable: @@ -270,4 +270,4 @@ get: content: application/json: schema: - $ref: '../components/schemas/401_response.yaml' \ No newline at end of file + $ref: '../components/schemas/401_response.yaml' diff --git a/x-pack/platform/plugins/shared/fleet/common/authz.test.ts b/x-pack/platform/plugins/shared/fleet/common/authz.test.ts index 28a01e72992b6..abe2b8c8d22d2 100644 --- a/x-pack/platform/plugins/shared/fleet/common/authz.test.ts +++ b/x-pack/platform/plugins/shared/fleet/common/authz.test.ts @@ -69,7 +69,7 @@ describe('fleet authz', () => { navLinks: {}, management: {}, catalogue: {}, - siem: endpointCapabilities, + siemV2: endpointCapabilities, transform: transformCapabilities, }); @@ -95,7 +95,7 @@ describe('fleet authz', () => { navLinks: {}, management: {}, catalogue: {}, - siem: endpointExceptionsCapabilities, + siemV2: endpointExceptionsCapabilities, }); expect(actual).toEqual(expected); @@ -120,7 +120,7 @@ describe('fleet authz', () => { navLinks: {}, management: {}, catalogue: {}, - siem: endpointCapabilities, + siemV2: endpointCapabilities, }); expect(actual).toEqual(expected); diff --git a/x-pack/platform/plugins/shared/fleet/common/authz.ts b/x-pack/platform/plugins/shared/fleet/common/authz.ts index 409c5eac01d65..d93d4338a107f 100644 --- a/x-pack/platform/plugins/shared/fleet/common/authz.ts +++ b/x-pack/platform/plugins/shared/fleet/common/authz.ts @@ -178,7 +178,7 @@ export function calculatePackagePrivilegesFromCapabilities( (acc, [privilege, { privilegeName }]) => { acc[privilege] = { executePackageAction: - (capabilities.siem && (capabilities.siem[privilegeName] as boolean)) || false, + (capabilities.siemV2 && (capabilities.siemV2[privilegeName] as boolean)) || false, }; return acc; }, @@ -208,14 +208,14 @@ export function calculatePackagePrivilegesFromCapabilities( export function calculateEndpointExceptionsPrivilegesFromCapabilities( capabilities: Capabilities | undefined ): FleetAuthz['endpointExceptionsPrivileges'] { - if (!capabilities || !capabilities.siem) { + if (!capabilities || !capabilities.siemV2) { return; } const endpointExceptionsActions = Object.keys(ENDPOINT_EXCEPTIONS_PRIVILEGES).reduce< Record >((acc, privilegeName) => { - acc[privilegeName] = (capabilities.siem[privilegeName] as boolean) || false; + acc[privilegeName] = (capabilities.siemV2[privilegeName] as boolean) || false; return acc; }, {}); diff --git a/x-pack/platform/plugins/shared/fleet/common/constants/authz.ts b/x-pack/platform/plugins/shared/fleet/common/constants/authz.ts index 8cfa1d8f854ea..81928bfeb5060 100644 --- a/x-pack/platform/plugins/shared/fleet/common/constants/authz.ts +++ b/x-pack/platform/plugins/shared/fleet/common/constants/authz.ts @@ -8,7 +8,7 @@ import { deepFreeze } from '@kbn/std'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; -const SECURITY_SOLUTION_APP_ID = 'siem'; +const SECURITY_SOLUTION_APP_ID = 'siemV2'; export interface PrivilegeMapObject { appId: string; diff --git a/x-pack/solutions/security/packages/features/config.ts b/x-pack/solutions/security/packages/features/config.ts index 9fa14ab0ba7c2..945623502029e 100644 --- a/x-pack/solutions/security/packages/features/config.ts +++ b/x-pack/solutions/security/packages/features/config.ts @@ -9,5 +9,7 @@ export { securityDefaultProductFeaturesConfig } from './src/security/product_fea 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 { timelineDefaultProductFeaturesConfig } from './src/timeline/product_feature_config'; +export { notesDefaultProductFeaturesConfig } from './src/notes/product_feature_config'; export { createEnabledProductFeaturesConfigMap } from './src/helpers'; diff --git a/x-pack/solutions/security/packages/features/product_features.ts b/x-pack/solutions/security/packages/features/product_features.ts index 67d61f21fae5e..1f55fd51bae1d 100644 --- a/x-pack/solutions/security/packages/features/product_features.ts +++ b/x-pack/solutions/security/packages/features/product_features.ts @@ -5,7 +5,9 @@ * 2.0. */ -export { getSecurityFeature } from './src/security'; +export { getSecurityFeature, getSecurityV2Feature } from './src/security'; export { getCasesFeature, getCasesV2Feature } from './src/cases'; export { getAssistantFeature } from './src/assistant'; export { getAttackDiscoveryFeature } from './src/attack_discovery'; +export { getTimelineFeature } from './src/timeline'; +export { getNotesFeature } from './src/notes'; diff --git a/x-pack/solutions/security/packages/features/src/constants.ts b/x-pack/solutions/security/packages/features/src/constants.ts index c6acab28c4860..9615dc0d278b1 100644 --- a/x-pack/solutions/security/packages/features/src/constants.ts +++ b/x-pack/solutions/security/packages/features/src/constants.ts @@ -9,6 +9,9 @@ export const APP_ID = 'securitySolution' as const; export const SERVER_APP_ID = 'siem' as const; +// New version created in 8.18. It was previously `SERVER_APP_ID`. +export const SECURITY_FEATURE_ID_V2 = 'siemV2' as const; + /** * @deprecated deprecated in 8.17. Use CASE_FEATURE_ID_V2 instead */ @@ -21,6 +24,8 @@ export const SECURITY_SOLUTION_CASES_APP_ID = 'securitySolutionCases' as const; export const ASSISTANT_FEATURE_ID = 'securitySolutionAssistant' as const; export const ATTACK_DISCOVERY_FEATURE_ID = 'securitySolutionAttackDiscovery' as const; +export const TIMELINE_FEATURE_ID = 'securitySolutionTimeline' as const; +export const NOTES_FEATURE_ID = 'securitySolutionNotes' as const; // Same as the plugin id defined by Cloud Security Posture export const CLOUD_POSTURE_APP_ID = 'csp' as const; diff --git a/x-pack/solutions/security/packages/features/src/notes/index.ts b/x-pack/solutions/security/packages/features/src/notes/index.ts new file mode 100644 index 0000000000000..c6ec57b39bb8c --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/notes/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { getNotesBaseKibanaFeature } from './kibana_features'; +import type { ProductFeatureParams } from '../types'; +import type { SecurityFeatureParams } from '../security/types'; + +export const getNotesFeature = (params: SecurityFeatureParams): ProductFeatureParams => ({ + baseKibanaFeature: getNotesBaseKibanaFeature(params), + baseKibanaSubFeatureIds: [], + subFeaturesMap: new Map(), +}); diff --git a/x-pack/solutions/security/packages/features/src/notes/kibana_features.ts b/x-pack/solutions/security/packages/features/src/notes/kibana_features.ts new file mode 100644 index 0000000000000..b418a13183ea7 --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/notes/kibana_features.ts @@ -0,0 +1,53 @@ +/* + * 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 { KibanaFeatureScope } from '@kbn/features-plugin/common'; + +import { APP_ID, NOTES_FEATURE_ID } from '../constants'; +import { type BaseKibanaFeatureConfig } from '../types'; +import type { SecurityFeatureParams } from '../security/types'; + +export const getNotesBaseKibanaFeature = ( + params: SecurityFeatureParams +): BaseKibanaFeatureConfig => ({ + id: NOTES_FEATURE_ID, + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.linkSecuritySolutionNotesTitle', + { + defaultMessage: 'Notes', + } + ), + order: 1100, + category: DEFAULT_APP_CATEGORIES.security, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], + app: [NOTES_FEATURE_ID, 'kibana'], + catalogue: [APP_ID], + privileges: { + all: { + app: [NOTES_FEATURE_ID, 'kibana'], + catalogue: [APP_ID], + savedObject: { + all: params.savedObjects, + read: params.savedObjects, + }, + ui: ['read', 'crud'], + api: ['notes_read', 'notes_write'], + }, + read: { + app: [NOTES_FEATURE_ID, 'kibana'], + catalogue: [APP_ID], + savedObject: { + all: [], + read: params.savedObjects, + }, + ui: ['read'], + api: ['notes_read'], + }, + }, +}); diff --git a/x-pack/solutions/security/packages/features/src/notes/product_feature_config.ts b/x-pack/solutions/security/packages/features/src/notes/product_feature_config.ts new file mode 100644 index 0000000000000..596b0af69b62e --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/notes/product_feature_config.ts @@ -0,0 +1,37 @@ +/* + * 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 { ProductFeatureNotesFeatureKey } from '../product_features_keys'; +import type { ProductFeatureKibanaConfig } from '../types'; + +/** + * App features privileges configuration for the notes 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 notesDefaultProductFeaturesConfig: Record< + ProductFeatureNotesFeatureKey, + ProductFeatureKibanaConfig +> = { + [ProductFeatureNotesFeatureKey.notes]: { + privileges: { + all: { + api: ['notes_read', 'notes_write'], + ui: ['read', 'crud'], + }, + read: { + api: ['notes_read'], + ui: ['read'], + }, + }, + }, +}; diff --git a/x-pack/solutions/security/packages/features/src/product_features_keys.ts b/x-pack/solutions/security/packages/features/src/product_features_keys.ts index bbc92ae0d978e..b8260762773a8 100644 --- a/x-pack/solutions/security/packages/features/src/product_features_keys.ts +++ b/x-pack/solutions/security/packages/features/src/product_features_keys.ts @@ -114,19 +114,37 @@ export enum ProductFeatureAttackDiscoveryKey { attackDiscovery = 'attack_discovery', } +export enum ProductFeatureTimelineFeatureKey { + /** + * Enables Timeline + */ + timeline = 'timeline', +} + +export enum ProductFeatureNotesFeatureKey { + /** + * Enables Notes + */ + notes = 'notes', +} + // Merges the two enums. export const ProductFeatureKey = { ...ProductFeatureSecurityKey, ...ProductFeatureCasesKey, ...ProductFeatureAssistantKey, ...ProductFeatureAttackDiscoveryKey, + ...ProductFeatureTimelineFeatureKey, + ...ProductFeatureNotesFeatureKey, }; // We need to merge the value and the type and export both to replicate how enum works. export type ProductFeatureKeyType = | ProductFeatureSecurityKey | ProductFeatureCasesKey | ProductFeatureAssistantKey - | ProductFeatureAttackDiscoveryKey; + | ProductFeatureAttackDiscoveryKey + | ProductFeatureTimelineFeatureKey + | ProductFeatureNotesFeatureKey; export const ALL_PRODUCT_FEATURE_KEYS = Object.freeze(Object.values(ProductFeatureKey)); diff --git a/x-pack/solutions/security/packages/features/src/security/index.ts b/x-pack/solutions/security/packages/features/src/security/index.ts index e59b0662dd975..910710a4b9f28 100644 --- a/x-pack/solutions/security/packages/features/src/security/index.ts +++ b/x-pack/solutions/security/packages/features/src/security/index.ts @@ -6,13 +6,21 @@ */ import type { SecuritySubFeatureId } from '../product_features_keys'; import type { ProductFeatureParams } from '../types'; -import { getSecurityBaseKibanaFeature } from './kibana_features'; +import { getSecurityBaseKibanaFeature } from './v1_features/kibana_features'; import { getSecuritySubFeaturesMap, getSecurityBaseKibanaSubFeatureIds, -} from './kibana_sub_features'; +} from './v1_features/kibana_sub_features'; +import { getSecurityV2BaseKibanaFeature } from './v2_features/kibana_features'; +import { + getSecurityV2SubFeaturesMap, + getSecurityV2BaseKibanaSubFeatureIds, +} from './v2_features/kibana_sub_features'; import type { SecurityFeatureParams } from './types'; +/** + * @deprecated Use getSecurityV2Feature instead + */ export const getSecurityFeature = ( params: SecurityFeatureParams ): ProductFeatureParams => ({ @@ -20,3 +28,11 @@ export const getSecurityFeature = ( baseKibanaSubFeatureIds: getSecurityBaseKibanaSubFeatureIds(params), subFeaturesMap: getSecuritySubFeaturesMap(params), }); + +export const getSecurityV2Feature = ( + params: SecurityFeatureParams +): ProductFeatureParams => ({ + baseKibanaFeature: getSecurityV2BaseKibanaFeature(params), + baseKibanaSubFeatureIds: getSecurityV2BaseKibanaSubFeatureIds(params), + subFeaturesMap: getSecurityV2SubFeaturesMap(params), +}); diff --git a/x-pack/solutions/security/packages/features/src/security/v1_features/kibana_features.ts b/x-pack/solutions/security/packages/features/src/security/v1_features/kibana_features.ts new file mode 100644 index 0000000000000..84a2f71bb32ab --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/security/v1_features/kibana_features.ts @@ -0,0 +1,181 @@ +/* + * 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'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; + +import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; +import { + EQL_RULE_TYPE_ID, + ESQL_RULE_TYPE_ID, + INDICATOR_RULE_TYPE_ID, + ML_RULE_TYPE_ID, + NEW_TERMS_RULE_TYPE_ID, + QUERY_RULE_TYPE_ID, + SAVED_QUERY_RULE_TYPE_ID, + THRESHOLD_RULE_TYPE_ID, +} from '@kbn/securitysolution-rules'; +import { + APP_ID, + SERVER_APP_ID, + LEGACY_NOTIFICATIONS_ID, + CLOUD_POSTURE_APP_ID, + CLOUD_DEFEND_APP_ID, + SECURITY_FEATURE_ID_V2, + TIMELINE_FEATURE_ID, + NOTES_FEATURE_ID, +} from '../../constants'; +import type { SecurityFeatureParams } from '../types'; +import type { BaseKibanaFeatureConfig } from '../../types'; + +const SECURITY_RULE_TYPES = [ + LEGACY_NOTIFICATIONS_ID, + ESQL_RULE_TYPE_ID, + EQL_RULE_TYPE_ID, + INDICATOR_RULE_TYPE_ID, + ML_RULE_TYPE_ID, + QUERY_RULE_TYPE_ID, + SAVED_QUERY_RULE_TYPE_ID, + THRESHOLD_RULE_TYPE_ID, + NEW_TERMS_RULE_TYPE_ID, +]; + +const alertingFeatures = SECURITY_RULE_TYPES.map((ruleTypeId) => ({ + ruleTypeId, + consumers: [SERVER_APP_ID], +})); + +export const getSecurityBaseKibanaFeature = ({ + savedObjects, +}: SecurityFeatureParams): BaseKibanaFeatureConfig => ({ + deprecated: { + notice: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.linkSecuritySolutionSecurity.deprecationMessage', + { + defaultMessage: 'The {currentId} permissions are deprecated, please see {idV2}.', + values: { + currentId: SERVER_APP_ID, + idV2: SECURITY_FEATURE_ID_V2, + }, + } + ), + }, + + id: SERVER_APP_ID, + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.linkSecuritySolutionTitleDeprecated', + { + defaultMessage: 'Security (Deprecated)', + } + ), + order: 1100, + category: DEFAULT_APP_CATEGORIES.security, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], + app: [APP_ID, CLOUD_POSTURE_APP_ID, CLOUD_DEFEND_APP_ID, 'kibana'], + catalogue: [APP_ID], + management: { + insightsAndAlerting: ['triggersActions'], + }, + alerting: alertingFeatures, + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.securityGroupDescription', + { + defaultMessage: + "Each sub-feature privilege in this group must be assigned individually. Global assignment is only supported if your pricing plan doesn't allow individual feature privileges.", + } + ), + privileges: { + all: { + replacedBy: { + default: [ + { feature: TIMELINE_FEATURE_ID, privileges: ['all'] }, + { feature: NOTES_FEATURE_ID, privileges: ['all'] }, + { feature: SECURITY_FEATURE_ID_V2, privileges: ['all'] }, + ], + minimal: [ + { feature: TIMELINE_FEATURE_ID, privileges: ['all'] }, + { feature: NOTES_FEATURE_ID, privileges: ['all'] }, + { feature: SECURITY_FEATURE_ID_V2, privileges: ['minimal_all'] }, + ], + }, + app: [APP_ID, CLOUD_POSTURE_APP_ID, CLOUD_DEFEND_APP_ID, 'kibana'], + catalogue: [APP_ID], + api: [ + APP_ID, + 'lists-all', + 'lists-read', + 'lists-summary', + 'rac', + 'cloud-security-posture-all', + 'cloud-security-posture-read', + 'cloud-defend-all', + 'cloud-defend-read', + 'timeline_write', + 'timeline_read', + 'notes_write', + 'notes_read', + ], + savedObject: { + all: ['alert', ...savedObjects], + read: [], + }, + alerting: { + rule: { + all: alertingFeatures, + }, + alert: { + all: alertingFeatures, + }, + }, + management: { + insightsAndAlerting: ['triggersActions'], + }, + ui: ['show', 'crud'], + }, + read: { + replacedBy: { + default: [ + { feature: TIMELINE_FEATURE_ID, privileges: ['read'] }, + { feature: NOTES_FEATURE_ID, privileges: ['read'] }, + { feature: SECURITY_FEATURE_ID_V2, privileges: ['read'] }, + ], + minimal: [ + { feature: TIMELINE_FEATURE_ID, privileges: ['read'] }, + { feature: NOTES_FEATURE_ID, privileges: ['read'] }, + { feature: SECURITY_FEATURE_ID_V2, privileges: ['minimal_read'] }, + ], + }, + app: [APP_ID, CLOUD_POSTURE_APP_ID, CLOUD_DEFEND_APP_ID, 'kibana'], + catalogue: [APP_ID], + api: [ + APP_ID, + 'lists-read', + 'rac', + 'cloud-security-posture-read', + 'cloud-defend-read', + 'timeline_read', + 'notes_read', + ], + savedObject: { + all: [], + read: [...savedObjects], + }, + alerting: { + rule: { + read: alertingFeatures, + }, + alert: { + all: alertingFeatures, + }, + }, + management: { + insightsAndAlerting: ['triggersActions'], + }, + ui: ['show'], + }, + }, +}); diff --git a/x-pack/solutions/security/packages/features/src/security/v1_features/kibana_sub_features.ts b/x-pack/solutions/security/packages/features/src/security/v1_features/kibana_sub_features.ts new file mode 100644 index 0000000000000..8d3c9b4a36a2c --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/security/v1_features/kibana_sub_features.ts @@ -0,0 +1,755 @@ +/* + * 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'; +import type { SubFeatureConfig } from '@kbn/features-plugin/common'; +import { EXCEPTION_LIST_NAMESPACE_AGNOSTIC } from '@kbn/securitysolution-list-constants'; +import { + ProductFeaturesPrivilegeId, + ProductFeaturesPrivileges, +} from '../../product_features_privileges'; + +import { SecuritySubFeatureId } from '../../product_features_keys'; +import { APP_ID, SECURITY_FEATURE_ID_V2 } from '../../constants'; +import type { SecurityFeatureParams } from '../types'; + +const endpointListSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointList.privilegesTooltip', + { + defaultMessage: 'All Spaces is required for Endpoint List access.', + } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointList', + { + defaultMessage: 'Endpoint List', + } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointList.description', + { + defaultMessage: + 'Displays all hosts running Elastic Defend and their relevant integration details.', + } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + replacedBy: [{ feature: SECURITY_FEATURE_ID_V2, privileges: ['endpoint_list_all'] }], + api: [`${APP_ID}-writeEndpointList`, `${APP_ID}-readEndpointList`], + id: 'endpoint_list_all', + includeIn: 'none', + name: 'All', + savedObject: { + all: [], + read: [], + }, + ui: ['writeEndpointList', 'readEndpointList'], + }, + { + replacedBy: [{ feature: SECURITY_FEATURE_ID_V2, privileges: ['endpoint_list_read'] }], + api: [`${APP_ID}-readEndpointList`], + id: 'endpoint_list_read', + includeIn: 'none', + name: 'Read', + savedObject: { + all: [], + read: [], + }, + ui: ['readEndpointList'], + }, + ], + }, + ], +}); + +const trustedApplicationsSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedApplications.privilegesTooltip', + { + defaultMessage: 'All Spaces is required for Trusted Applications access.', + } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedApplications', + { + defaultMessage: 'Trusted Applications', + } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.trustedApplications.description', + { + defaultMessage: + 'Helps mitigate conflicts with other software, usually other antivirus or endpoint security applications.', + } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + replacedBy: [ + { feature: SECURITY_FEATURE_ID_V2, privileges: ['trusted_applications_all'] }, + ], + api: [ + 'lists-all', + 'lists-read', + 'lists-summary', + `${APP_ID}-writeTrustedApplications`, + `${APP_ID}-readTrustedApplications`, + ], + id: 'trusted_applications_all', + includeIn: 'none', + name: 'All', + savedObject: { + all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC], + read: [], + }, + ui: ['writeTrustedApplications', 'readTrustedApplications'], + }, + { + replacedBy: [ + { feature: SECURITY_FEATURE_ID_V2, privileges: ['trusted_applications_read'] }, + ], + api: ['lists-read', 'lists-summary', `${APP_ID}-readTrustedApplications`], + id: 'trusted_applications_read', + includeIn: 'none', + name: 'Read', + savedObject: { + all: [], + read: [], + }, + ui: ['readTrustedApplications'], + }, + ], + }, + ], +}); +const hostIsolationExceptionsBasicSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolationExceptions.privilegesTooltip', + { + defaultMessage: 'All Spaces is required for Host Isolation Exceptions access.', + } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolationExceptions', + { + defaultMessage: 'Host Isolation Exceptions', + } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolationExceptions.description', + { + defaultMessage: + 'Add specific IP addresses that isolated hosts are still allowed to communicate with, even when isolated from the rest of the network.', + } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + replacedBy: [ + { feature: SECURITY_FEATURE_ID_V2, privileges: ['host_isolation_exceptions_all'] }, + ], + api: [ + 'lists-all', + 'lists-read', + 'lists-summary', + `${APP_ID}-deleteHostIsolationExceptions`, + `${APP_ID}-readHostIsolationExceptions`, + ], + id: 'host_isolation_exceptions_all', + includeIn: 'none', + name: 'All', + savedObject: { + all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC], + read: [], + }, + ui: ['readHostIsolationExceptions', 'deleteHostIsolationExceptions'], + }, + { + replacedBy: [ + { feature: SECURITY_FEATURE_ID_V2, privileges: ['host_isolation_exceptions_read'] }, + ], + api: ['lists-read', 'lists-summary', `${APP_ID}-readHostIsolationExceptions`], + id: 'host_isolation_exceptions_read', + includeIn: 'none', + name: 'Read', + savedObject: { + all: [], + read: [], + }, + ui: ['readHostIsolationExceptions'], + }, + ], + }, + ], +}); +const blocklistSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.blockList.privilegesTooltip', + { + defaultMessage: 'All Spaces is required for Blocklist access.', + } + ), + name: i18n.translate('securitySolutionPackages.features.featureRegistry.subFeatures.blockList', { + defaultMessage: 'Blocklist', + }), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.blockList.description', + { + defaultMessage: + 'Extend Elastic Defend’s protection against malicious processes and protect against potentially harmful applications.', + } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + replacedBy: [{ feature: SECURITY_FEATURE_ID_V2, privileges: ['blocklist_all'] }], + api: [ + 'lists-all', + 'lists-read', + 'lists-summary', + `${APP_ID}-writeBlocklist`, + `${APP_ID}-readBlocklist`, + ], + id: 'blocklist_all', + includeIn: 'none', + name: 'All', + savedObject: { + all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC], + read: [], + }, + ui: ['writeBlocklist', 'readBlocklist'], + }, + { + replacedBy: [{ feature: SECURITY_FEATURE_ID_V2, privileges: ['blocklist_read'] }], + api: ['lists-read', 'lists-summary', `${APP_ID}-readBlocklist`], + id: 'blocklist_read', + includeIn: 'none', + name: 'Read', + savedObject: { + all: [], + read: [], + }, + ui: ['readBlocklist'], + }, + ], + }, + ], +}); +const eventFiltersSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.eventFilters.privilegesTooltip', + { + defaultMessage: 'All Spaces is required for Event Filters access.', + } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.eventFilters', + { + defaultMessage: 'Event Filters', + } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.eventFilters.description', + { + defaultMessage: + 'Filter out endpoint events that you do not need or want stored in Elasticsearch.', + } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + replacedBy: [{ feature: SECURITY_FEATURE_ID_V2, privileges: ['event_filters_all'] }], + api: [ + 'lists-all', + 'lists-read', + 'lists-summary', + `${APP_ID}-writeEventFilters`, + `${APP_ID}-readEventFilters`, + ], + id: 'event_filters_all', + includeIn: 'none', + name: 'All', + savedObject: { + all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC], + read: [], + }, + ui: ['writeEventFilters', 'readEventFilters'], + }, + { + replacedBy: [{ feature: SECURITY_FEATURE_ID_V2, privileges: ['event_filters_read'] }], + api: ['lists-read', 'lists-summary', `${APP_ID}-readEventFilters`], + id: 'event_filters_read', + includeIn: 'none', + name: 'Read', + savedObject: { + all: [], + read: [], + }, + ui: ['readEventFilters'], + }, + ], + }, + ], +}); +const policyManagementSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.policyManagement.privilegesTooltip', + { + defaultMessage: 'All Spaces is required for Policy Management access.', + } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.policyManagement', + { + defaultMessage: 'Elastic Defend Policy Management', + } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.policyManagement.description', + { + defaultMessage: + 'Access the Elastic Defend integration policy to configure protections, event collection, and advanced policy features.', + } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + replacedBy: [{ feature: SECURITY_FEATURE_ID_V2, privileges: ['policy_management_all'] }], + api: [`${APP_ID}-writePolicyManagement`, `${APP_ID}-readPolicyManagement`], + id: 'policy_management_all', + includeIn: 'none', + name: 'All', + savedObject: { + all: ['policy-settings-protection-updates-note'], + read: [], + }, + ui: ['writePolicyManagement', 'readPolicyManagement'], + }, + { + replacedBy: [{ feature: SECURITY_FEATURE_ID_V2, privileges: ['policy_management_read'] }], + api: [`${APP_ID}-readPolicyManagement`], + id: 'policy_management_read', + includeIn: 'none', + name: 'Read', + savedObject: { + all: [], + read: ['policy-settings-protection-updates-note'], + }, + ui: ['readPolicyManagement'], + }, + ], + }, + ], +}); + +const responseActionsHistorySubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.responseActionsHistory.privilegesTooltip', + { + defaultMessage: 'All Spaces is required for Response Actions History access.', + } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.responseActionsHistory', + { + defaultMessage: 'Response Actions History', + } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.responseActionsHistory.description', + { + defaultMessage: 'Access the history of response actions performed on endpoints.', + } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + replacedBy: [ + { feature: SECURITY_FEATURE_ID_V2, privileges: ['actions_log_management_all'] }, + ], + api: [`${APP_ID}-writeActionsLogManagement`, `${APP_ID}-readActionsLogManagement`], + id: 'actions_log_management_all', + includeIn: 'none', + name: 'All', + savedObject: { + all: [], + read: [], + }, + ui: ['writeActionsLogManagement', 'readActionsLogManagement'], + }, + { + replacedBy: [ + { feature: SECURITY_FEATURE_ID_V2, privileges: ['actions_log_management_read'] }, + ], + api: [`${APP_ID}-readActionsLogManagement`], + id: 'actions_log_management_read', + includeIn: 'none', + name: 'Read', + savedObject: { + all: [], + read: [], + }, + ui: ['readActionsLogManagement'], + }, + ], + }, + ], +}); +const hostIsolationSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolation.privilegesTooltip', + { + defaultMessage: 'All Spaces is required for Host Isolation access.', + } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolation', + { + defaultMessage: 'Host Isolation', + } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.hostIsolation.description', + { defaultMessage: 'Perform the "isolate" and "release" response actions.' } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + replacedBy: [{ feature: SECURITY_FEATURE_ID_V2, privileges: ['host_isolation_all'] }], + api: [`${APP_ID}-writeHostIsolationRelease`], + id: 'host_isolation_all', + includeIn: 'none', + name: 'All', + savedObject: { + all: [], + read: [], + }, + ui: ['writeHostIsolationRelease'], + }, + ], + }, + ], +}); + +const processOperationsSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.processOperations.privilegesTooltip', + { + defaultMessage: 'All Spaces is required for Process Operations access.', + } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.processOperations', + { + defaultMessage: 'Process Operations', + } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.processOperations.description', + { + defaultMessage: 'Perform process-related response actions in the response console.', + } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + replacedBy: [{ feature: SECURITY_FEATURE_ID_V2, privileges: ['process_operations_all'] }], + api: [`${APP_ID}-writeProcessOperations`], + id: 'process_operations_all', + includeIn: 'none', + name: 'All', + savedObject: { + all: [], + read: [], + }, + ui: ['writeProcessOperations'], + }, + ], + }, + ], +}); +const fileOperationsSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.fileOperations.privilegesTooltip', + { + defaultMessage: 'All Spaces is required for File Operations access.', + } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.fileOperations', + { + defaultMessage: 'File Operations', + } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.fileOperations.description', + { + defaultMessage: 'Perform file-related response actions in the response console.', + } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + replacedBy: [{ feature: SECURITY_FEATURE_ID_V2, privileges: ['file_operations_all'] }], + api: [`${APP_ID}-writeFileOperations`], + id: 'file_operations_all', + includeIn: 'none', + name: 'All', + savedObject: { + all: [], + read: [], + }, + ui: ['writeFileOperations'], + }, + ], + }, + ], +}); + +// execute operations are not available in 8.7, +// but will be available in 8.8 +const executeActionSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.executeOperations.privilegesTooltip', + { + defaultMessage: 'All Spaces is required for Execute Operations access.', + } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.executeOperations', + { + defaultMessage: 'Execute Operations', + } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.executeOperations.description', + { + defaultMessage: 'Perform script execution response actions in the response console.', + } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + replacedBy: [{ feature: SECURITY_FEATURE_ID_V2, privileges: ['execute_operations_all'] }], + api: [`${APP_ID}-writeExecuteOperations`], + id: 'execute_operations_all', + includeIn: 'none', + name: 'All', + savedObject: { + all: [], + read: [], + }, + ui: ['writeExecuteOperations'], + }, + ], + }, + ], +}); + +// 8.15 feature +const scanActionSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.scanOperations.privilegesTooltip', + { + defaultMessage: 'All Spaces is required for Scan Operations access.', + } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.scanOperations', + { + defaultMessage: 'Scan Operations', + } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.scanOperations.description', + { + defaultMessage: 'Perform folder scan response actions in the response console.', + } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + replacedBy: [{ feature: SECURITY_FEATURE_ID_V2, privileges: ['scan_operations_all'] }], + + api: [`${APP_ID}-writeScanOperations`], + id: 'scan_operations_all', + includeIn: 'none', + name: 'All', + savedObject: { + all: [], + read: [], + }, + ui: ['writeScanOperations'], + }, + ], + }, + ], +}); + +const endpointExceptionsSubFeature = (): SubFeatureConfig => ({ + requireAllSpaces: true, + privilegesTooltip: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointExceptions.privilegesTooltip', + { + defaultMessage: 'All Spaces is required for Endpoint Exceptions access.', + } + ), + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointExceptions', + { + defaultMessage: 'Endpoint Exceptions', + } + ), + description: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.subFeatures.endpointExceptions.description', + { + defaultMessage: 'Use Endpoint Exceptions (this is a test sub-feature).', + } + ), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + replacedBy: [ + { feature: SECURITY_FEATURE_ID_V2, privileges: ['endpoint_exceptions_all'] }, + ], + id: 'endpoint_exceptions_all', + includeIn: 'all', + name: 'All', + savedObject: { + all: [], + read: [], + }, + ...ProductFeaturesPrivileges[ProductFeaturesPrivilegeId.endpointExceptions].all, + }, + { + replacedBy: [ + { feature: SECURITY_FEATURE_ID_V2, privileges: ['endpoint_exceptions_read'] }, + ], + id: 'endpoint_exceptions_read', + includeIn: 'read', + name: 'Read', + savedObject: { + all: [], + read: [], + }, + ...ProductFeaturesPrivileges[ProductFeaturesPrivilegeId.endpointExceptions].read, + }, + ], + }, + ], +}); + +/** + * Sub-features that will always be available for Security + * regardless of the product type. + */ +export const getSecurityBaseKibanaSubFeatureIds = ( + { experimentalFeatures }: SecurityFeatureParams // currently un-used, but left here as a convenience for possible future use +): SecuritySubFeatureId[] => [SecuritySubFeatureId.hostIsolation]; + +/** + * Defines all the Security Assistant subFeatures available. + * The order of the subFeatures is the order they will be displayed + */ + +export const getSecuritySubFeaturesMap = ({ + experimentalFeatures, +}: SecurityFeatureParams): Map => { + const enableSpaceAwarenessIfNeeded = (subFeature: SubFeatureConfig): SubFeatureConfig => { + if (experimentalFeatures.endpointManagementSpaceAwarenessEnabled) { + subFeature.requireAllSpaces = false; + subFeature.privilegesTooltip = undefined; + } + + return subFeature; + }; + + const securitySubFeaturesList: Array<[SecuritySubFeatureId, SubFeatureConfig]> = [ + [SecuritySubFeatureId.endpointList, enableSpaceAwarenessIfNeeded(endpointListSubFeature())], + [ + SecuritySubFeatureId.endpointExceptions, + enableSpaceAwarenessIfNeeded(endpointExceptionsSubFeature()), + ], + [ + SecuritySubFeatureId.trustedApplications, + enableSpaceAwarenessIfNeeded(trustedApplicationsSubFeature()), + ], + [ + SecuritySubFeatureId.hostIsolationExceptionsBasic, + enableSpaceAwarenessIfNeeded(hostIsolationExceptionsBasicSubFeature()), + ], + [SecuritySubFeatureId.blocklist, enableSpaceAwarenessIfNeeded(blocklistSubFeature())], + [SecuritySubFeatureId.eventFilters, enableSpaceAwarenessIfNeeded(eventFiltersSubFeature())], + [ + SecuritySubFeatureId.policyManagement, + enableSpaceAwarenessIfNeeded(policyManagementSubFeature()), + ], + [ + SecuritySubFeatureId.responseActionsHistory, + enableSpaceAwarenessIfNeeded(responseActionsHistorySubFeature()), + ], + [SecuritySubFeatureId.hostIsolation, enableSpaceAwarenessIfNeeded(hostIsolationSubFeature())], + [ + SecuritySubFeatureId.processOperations, + enableSpaceAwarenessIfNeeded(processOperationsSubFeature()), + ], + [SecuritySubFeatureId.fileOperations, enableSpaceAwarenessIfNeeded(fileOperationsSubFeature())], + [SecuritySubFeatureId.executeAction, enableSpaceAwarenessIfNeeded(executeActionSubFeature())], + [SecuritySubFeatureId.scanAction, enableSpaceAwarenessIfNeeded(scanActionSubFeature())], + ]; + + // Use the following code to add feature based on feature flag + // if (experimentalFeatures.featureFlagName) { + // securitySubFeaturesList.push([SecuritySubFeatureId.featureId, featureSubFeature]); + // } + + const securitySubFeaturesMap = new Map( + securitySubFeaturesList + ); + + return Object.freeze(securitySubFeaturesMap); +}; diff --git a/x-pack/solutions/security/packages/features/src/security/kibana_features.ts b/x-pack/solutions/security/packages/features/src/security/v2_features/kibana_features.ts similarity index 93% rename from x-pack/solutions/security/packages/features/src/security/kibana_features.ts rename to x-pack/solutions/security/packages/features/src/security/v2_features/kibana_features.ts index 458b8f6fd1f1f..1037cd356699e 100644 --- a/x-pack/solutions/security/packages/features/src/security/kibana_features.ts +++ b/x-pack/solutions/security/packages/features/src/security/v2_features/kibana_features.ts @@ -19,15 +19,16 @@ import { SAVED_QUERY_RULE_TYPE_ID, THRESHOLD_RULE_TYPE_ID, } from '@kbn/securitysolution-rules'; -import type { BaseKibanaFeatureConfig } from '../types'; import { APP_ID, - SERVER_APP_ID, + SECURITY_FEATURE_ID_V2, LEGACY_NOTIFICATIONS_ID, CLOUD_POSTURE_APP_ID, CLOUD_DEFEND_APP_ID, -} from '../constants'; -import type { SecurityFeatureParams } from './types'; + SERVER_APP_ID, +} from '../../constants'; +import type { SecurityFeatureParams } from '../types'; +import type { BaseKibanaFeatureConfig } from '../../types'; const SECURITY_RULE_TYPES = [ LEGACY_NOTIFICATIONS_ID, @@ -46,10 +47,10 @@ const alertingFeatures = SECURITY_RULE_TYPES.map((ruleTypeId) => ({ consumers: [SERVER_APP_ID], })); -export const getSecurityBaseKibanaFeature = ({ +export const getSecurityV2BaseKibanaFeature = ({ savedObjects, }: SecurityFeatureParams): BaseKibanaFeatureConfig => ({ - id: SERVER_APP_ID, + id: SECURITY_FEATURE_ID_V2, name: i18n.translate( 'securitySolutionPackages.features.featureRegistry.linkSecuritySolutionTitle', { diff --git a/x-pack/solutions/security/packages/features/src/security/kibana_sub_features.ts b/x-pack/solutions/security/packages/features/src/security/v2_features/kibana_sub_features.ts similarity index 98% rename from x-pack/solutions/security/packages/features/src/security/kibana_sub_features.ts rename to x-pack/solutions/security/packages/features/src/security/v2_features/kibana_sub_features.ts index 51e15f67433dd..d4a20c92bc74c 100644 --- a/x-pack/solutions/security/packages/features/src/security/kibana_sub_features.ts +++ b/x-pack/solutions/security/packages/features/src/security/v2_features/kibana_sub_features.ts @@ -11,11 +11,11 @@ import { EXCEPTION_LIST_NAMESPACE_AGNOSTIC } from '@kbn/securitysolution-list-co import { ProductFeaturesPrivilegeId, ProductFeaturesPrivileges, -} from '../product_features_privileges'; +} from '../../product_features_privileges'; -import { SecuritySubFeatureId } from '../product_features_keys'; -import { APP_ID } from '../constants'; -import type { SecurityFeatureParams } from './types'; +import { SecuritySubFeatureId } from '../../product_features_keys'; +import { APP_ID } from '../../constants'; +import type { SecurityFeatureParams } from '../types'; const endpointListSubFeature = (): SubFeatureConfig => ({ requireAllSpaces: true, @@ -701,7 +701,7 @@ const endpointExceptionsSubFeature = (): SubFeatureConfig => ({ * Sub-features that will always be available for Security * regardless of the product type. */ -export const getSecurityBaseKibanaSubFeatureIds = ( +export const getSecurityV2BaseKibanaSubFeatureIds = ( { experimentalFeatures }: SecurityFeatureParams // currently un-used, but left here as a convenience for possible future use ): SecuritySubFeatureId[] => [SecuritySubFeatureId.hostIsolation]; @@ -710,7 +710,7 @@ export const getSecurityBaseKibanaSubFeatureIds = ( * The order of the subFeatures is the order they will be displayed */ -export const getSecuritySubFeaturesMap = ({ +export const getSecurityV2SubFeaturesMap = ({ experimentalFeatures, }: SecurityFeatureParams): Map => { const enableSpaceAwarenessIfNeeded = (subFeature: SubFeatureConfig): SubFeatureConfig => { diff --git a/x-pack/solutions/security/packages/features/src/timeline/index.ts b/x-pack/solutions/security/packages/features/src/timeline/index.ts new file mode 100644 index 0000000000000..62042881ec6f2 --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/timeline/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { getTimelineBaseKibanaFeature } from './kibana_features'; +import type { ProductFeatureParams } from '../types'; +import type { SecurityFeatureParams } from '../security/types'; + +export const getTimelineFeature = (params: SecurityFeatureParams): ProductFeatureParams => ({ + baseKibanaFeature: getTimelineBaseKibanaFeature(params), + baseKibanaSubFeatureIds: [], + subFeaturesMap: new Map(), +}); diff --git a/x-pack/solutions/security/packages/features/src/timeline/kibana_features.ts b/x-pack/solutions/security/packages/features/src/timeline/kibana_features.ts new file mode 100644 index 0000000000000..5c9fbfecda570 --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/timeline/kibana_features.ts @@ -0,0 +1,53 @@ +/* + * 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 { KibanaFeatureScope } from '@kbn/features-plugin/common'; + +import { APP_ID, TIMELINE_FEATURE_ID } from '../constants'; +import { type BaseKibanaFeatureConfig } from '../types'; +import type { SecurityFeatureParams } from '../security/types'; + +export const getTimelineBaseKibanaFeature = ( + params: SecurityFeatureParams +): BaseKibanaFeatureConfig => ({ + id: TIMELINE_FEATURE_ID, + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.linkSecuritySolutionTimelineTitle', + { + defaultMessage: 'Timeline', + } + ), + order: 1100, + category: DEFAULT_APP_CATEGORIES.security, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], + app: [TIMELINE_FEATURE_ID, 'kibana'], + catalogue: [APP_ID], + privileges: { + all: { + app: [TIMELINE_FEATURE_ID, 'kibana'], + catalogue: [APP_ID], + savedObject: { + all: params.savedObjects, + read: params.savedObjects, + }, + ui: ['read', 'crud'], + api: ['timeline_read', 'timeline_write'], + }, + read: { + app: [TIMELINE_FEATURE_ID, 'kibana'], + catalogue: [APP_ID], + savedObject: { + all: [], + read: params.savedObjects, + }, + ui: ['read'], + api: ['timeline_read'], + }, + }, +}); diff --git a/x-pack/solutions/security/packages/features/src/timeline/product_feature_config.ts b/x-pack/solutions/security/packages/features/src/timeline/product_feature_config.ts new file mode 100644 index 0000000000000..dd442014bf6fe --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/timeline/product_feature_config.ts @@ -0,0 +1,37 @@ +/* + * 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 { ProductFeatureTimelineFeatureKey } from '../product_features_keys'; +import type { ProductFeatureKibanaConfig } from '../types'; + +/** + * App features privileges configuration for the timeline 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 timelineDefaultProductFeaturesConfig: Record< + ProductFeatureTimelineFeatureKey, + ProductFeatureKibanaConfig +> = { + [ProductFeatureTimelineFeatureKey.timeline]: { + privileges: { + all: { + api: ['timeline_read', 'timeline_write'], + ui: ['read', 'crud'], + }, + read: { + api: ['timeline_read'], + ui: ['read'], + }, + }, + }, +}; diff --git a/x-pack/solutions/security/packages/features/src/types.ts b/x-pack/solutions/security/packages/features/src/types.ts index 17e4869fa66f1..e180851a62cee 100644 --- a/x-pack/solutions/security/packages/features/src/types.ts +++ b/x-pack/solutions/security/packages/features/src/types.ts @@ -20,6 +20,8 @@ import type { AssistantSubFeatureId, CasesSubFeatureId, SecuritySubFeatureId, + ProductFeatureTimelineFeatureKey, + ProductFeatureNotesFeatureKey, } from './product_features_keys'; export type { ProductFeatureKeyType }; @@ -57,6 +59,16 @@ export type ProductFeaturesAttackDiscoveryConfig = Map< ProductFeatureKibanaConfig >; +export type ProductFeaturesTimelineConfig = Map< + ProductFeatureTimelineFeatureKey, + ProductFeatureKibanaConfig +>; + +export type ProductFeaturesNotesConfig = Map< + ProductFeatureNotesFeatureKey, + ProductFeatureKibanaConfig +>; + export type AppSubFeaturesMap = Map; export interface ProductFeatureParams { diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/rules/rules.test.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/rules/rules.test.tsx index ff427e7809454..e65bee10243d6 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/rules/rules.test.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/rules/rules.test.tsx @@ -47,7 +47,7 @@ const getTestComponent = ...coreStart.application, capabilities: { ...coreStart.application.capabilities, - siem: { crud: true }, + siemV2: { crud: true }, }, }, }; diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/rules/rules_container.test.tsx b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/rules/rules_container.test.tsx index 4718dbae911ae..a5c5a534db189 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/rules/rules_container.test.tsx +++ b/x-pack/solutions/security/plugins/cloud_security_posture/public/pages/rules/rules_container.test.tsx @@ -47,7 +47,7 @@ const getWrapper = ...coreStart.application, capabilities: { ...coreStart.application.capabilities, - siem: { crud: canUpdate }, + siemV2: { crud: canUpdate }, }, }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/common/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/constants.ts index 159dae027faee..aa3f2d0d5c096 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/constants.ts @@ -23,7 +23,10 @@ export const ASSET_INVENTORY_FEATURE_ID = 'securitySolutionAssetInventory' as co export const ASSISTANT_FEATURE_ID = 'securitySolutionAssistant' as const; export const ATTACK_DISCOVERY_FEATURE_ID = 'securitySolutionAttackDiscovery' as const; export const CASES_FEATURE_ID = 'securitySolutionCasesV2' as const; +export const TIMELINE_FEATURE_ID = 'securitySolutionTimeline' as const; +export const NOTES_FEATURE_ID = 'securitySolutionNotes' as const; export const SERVER_APP_ID = 'siem' as const; +export const SECURITY_FEATURE_ID = 'siemV2' as const; export const APP_NAME = 'Security' as const; export const APP_ICON = 'securityAnalyticsApp' as const; export const APP_ICON_SOLUTION = 'logoSecurity' as const; @@ -67,7 +70,6 @@ export const ENDPOINT_METRICS_INDEX = '.ds-metrics-endpoint.metrics-*' as const; export const DEFAULT_RULE_REFRESH_INTERVAL_ON = true as const; export const DEFAULT_RULE_REFRESH_INTERVAL_VALUE = 60000 as const; // ms export const DEFAULT_RULE_NOTIFICATION_QUERY_SIZE = 100 as const; -export const SECURITY_FEATURE_ID = 'Security' as const; export const SECURITY_TAG_NAME = 'Security Solution' as const; export const SECURITY_TAG_DESCRIPTION = 'Security Solution auto-generated tag' as const; export const DEFAULT_SPACE_ID = 'default' as const; diff --git a/x-pack/solutions/security/plugins/security_solution/common/index.ts b/x-pack/solutions/security/plugins/security_solution/common/index.ts index 7c240b39554a6..7bd4c019a6d15 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/index.ts @@ -10,6 +10,7 @@ export { APP_ID, CASES_FEATURE_ID, SERVER_APP_ID, + SECURITY_FEATURE_ID, APP_PATH, MANAGE_PATH, ADD_DATA_PATH, diff --git a/x-pack/solutions/security/plugins/security_solution/common/test/ess_roles.json b/x-pack/solutions/security/plugins/security_solution/common/test/ess_roles.json index 361d5d4321756..6259330cc2899 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/test/ess_roles.json +++ b/x-pack/solutions/security/plugins/security_solution/common/test/ess_roles.json @@ -27,10 +27,12 @@ { "feature": { "ml": ["read"], - "siem": ["read", "read_alerts"], + "siemV2": ["read", "read_alerts"], "securitySolutionAssistant": ["none"], "securitySolutionAttackDiscovery": ["none"], "securitySolutionCasesV2": ["read"], + "securitySolutionTimeline": ["read"], + "securitySolutionNotes": ["read"], "actions": ["read"], "builtInAlerts": ["read"] }, @@ -76,10 +78,12 @@ { "feature": { "ml": ["read"], - "siem": ["all", "read_alerts", "crud_alerts"], + "siemV2": ["all", "read_alerts", "crud_alerts"], "securitySolutionAssistant": ["all"], "securitySolutionAttackDiscovery": ["all"], "securitySolutionCasesV2": ["all"], + "securitySolutionTimeline": ["read"], + "securitySolutionNotes": ["read"], "actions": ["read"], "builtInAlerts": ["all"] }, @@ -125,10 +129,12 @@ { "feature": { "ml": ["read"], - "siem": ["all", "read_alerts", "crud_alerts"], + "siemV2": ["all", "read_alerts", "crud_alerts"], "securitySolutionAssistant": ["all"], "securitySolutionAttackDiscovery": ["all"], "securitySolutionCasesV2": ["all"], + "securitySolutionTimeline": ["all"], + "securitySolutionNotes": ["all"], "builtInAlerts": ["all"] }, "spaces": ["*"], @@ -146,7 +152,115 @@ "kibana": [ { "feature": { - "siem": ["read"] + "siemV2": ["read"] + }, + "spaces": ["*"], + "base": [] + } + ] + }, + "timeline_none": { + "name": "timeline_none", + "elasticsearch": { + "cluster": [], + "indices": [ + { + "names": [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*", + ".lists*", + ".items*", + ".asset-criticality.asset-criticality-*" + ], + "privileges": ["read", "write"] + }, + { + "names": [ + ".alerts-security*", + ".preview.alerts-security*", + ".internal.preview.alerts-security*", + ".siem-signals-*" + ], + "privileges": ["read", "write", "manage"] + }, + { + "names": ["metrics-endpoint.metadata_current_*", ".fleet-agents*", ".fleet-actions*"], + "privileges": ["read"] + } + ], + "run_as": [] + }, + "kibana": [ + { + "feature": { + "ml": ["read"], + "siemV2": ["all", "read_alerts", "crud_alerts"], + "securitySolutionAssistant": ["all"], + "securitySolutionAttackDiscovery": ["all"], + "securitySolutionCasesV2": ["all"], + "securitySolutionNotes": ["all"], + "actions": ["all"], + "builtInAlerts": ["all"] + }, + "spaces": ["*"], + "base": [] + } + ] + }, + "notes_none": { + "name": "notes_none", + "elasticsearch": { + "cluster": [], + "indices": [ + { + "names": [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*", + ".lists*", + ".items*", + ".asset-criticality.asset-criticality-*" + ], + "privileges": ["read", "write"] + }, + { + "names": [ + ".alerts-security*", + ".preview.alerts-security*", + ".internal.preview.alerts-security*", + ".siem-signals-*" + ], + "privileges": ["read", "write", "manage"] + }, + { + "names": ["metrics-endpoint.metadata_current_*", ".fleet-agents*", ".fleet-actions*"], + "privileges": ["read"] + } + ], + "run_as": [] + }, + "kibana": [ + { + "feature": { + "ml": ["read"], + "siemV2": ["all", "read_alerts", "crud_alerts"], + "securitySolutionAssistant": ["all"], + "securitySolutionAttackDiscovery": ["all"], + "securitySolutionCasesV2": ["all"], + "securitySolutionTimeline": ["all"], + "actions": ["all"], + "builtInAlerts": ["all"] }, "spaces": ["*"], "base": [] diff --git a/x-pack/solutions/security/plugins/security_solution/common/test/index.ts b/x-pack/solutions/security/plugins/security_solution/common/test/index.ts index 277f54c78e6c5..b54e082bf2625 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/test/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/test/index.ts @@ -30,6 +30,8 @@ export enum ROLES { hunter = 'hunter', hunter_no_actions = 'hunter_no_actions', no_risk_engine_privileges = 'no_risk_engine_privileges', + timeline_none = 'timeline_none', + notes_none = 'notes_none', } /** diff --git a/x-pack/solutions/security/plugins/security_solution/common/types/header_actions/index.ts b/x-pack/solutions/security/plugins/security_solution/common/types/header_actions/index.ts index 29f0b608fe56b..14852edd8c864 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/types/header_actions/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/types/header_actions/index.ts @@ -114,6 +114,7 @@ export interface ActionProps { toggleShowNotes?: () => void; width?: number; disablePinAction?: boolean; + disableTimelineAction?: boolean; } interface AdditionalControlColumnProps { diff --git a/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/cell_action/add_to_timeline.test.ts b/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/cell_action/add_to_timeline.test.ts index 3d105c34515b4..f17257bb25b3c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/cell_action/add_to_timeline.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/cell_action/add_to_timeline.test.ts @@ -104,6 +104,54 @@ describe('createAddToTimelineCellAction', () => { }) ).toEqual(false); }); + + it('should return true if the user has read access to timeline', async () => { + const factory = createAddToTimelineCellActionFactory({ + store, + services: { + ...services, + application: { + ...services.application, + capabilities: { + ...services.application.capabilities, + securitySolutionTimeline: { + read: true, + }, + }, + }, + }, + }); + const addToTimelineActionIsCompatible = factory({ + id: 'testAddToTimeline', + order: 1, + }); + + expect(await addToTimelineActionIsCompatible.isCompatible(context)).toEqual(true); + }); + + it('should return false if the user does not have access to timeline', async () => { + const factory = createAddToTimelineCellActionFactory({ + store, + services: { + ...services, + application: { + ...services.application, + capabilities: { + ...services.application.capabilities, + securitySolutionTimeline: { + read: false, + }, + }, + }, + }, + }); + const addToTimelineActionIsCompatible = factory({ + id: 'testAddToTimeline', + order: 1, + }); + + expect(await addToTimelineActionIsCompatible.isCompatible(context)).toEqual(false); + }); }); describe('execute', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/cell_action/add_to_timeline.ts b/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/cell_action/add_to_timeline.ts index 695569e98ee97..fafd7cc8ed7fc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/cell_action/add_to_timeline.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/cell_action/add_to_timeline.ts @@ -17,6 +17,7 @@ import type { KBN_FIELD_TYPES } from '@kbn/field-types'; import { addProvider } from '../../../../timelines/store/actions'; import { TimelineId } from '../../../../../common/types'; import type { SecurityAppStore } from '../../../../common/store'; +import { extractTimelineCapabilities } from '../../../../common/utils/timeline_capabilities'; import { fieldHasCellActions } from '../../utils'; import { ADD_TO_TIMELINE, @@ -38,17 +39,22 @@ export const createAddToTimelineCellActionFactory = createCellActionFactory( store: SecurityAppStore; services: StartServices; }): CellActionTemplate => { - const { notifications: notificationsService } = services; - + const { + notifications: notificationsService, + application: { capabilities }, + } = services; + const timelineCapabilities = extractTimelineCapabilities(capabilities); return { type: SecurityCellActionType.ADD_TO_TIMELINE, getIconType: () => ADD_TO_TIMELINE_ICON, getDisplayName: () => ADD_TO_TIMELINE, getDisplayNameTooltip: () => ADD_TO_TIMELINE, - isCompatible: async ({ data }) => { + + isCompatible: async ({ data, metadata }) => { const field = data[0]?.field; return ( + timelineCapabilities.read && data.length === 1 && // TODO Add support for multiple values fieldHasCellActions(field.name) && isValidDataProviderField(field.name, field.type) && diff --git a/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/cell_action/investigate_in_new_timeline.test.ts b/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/cell_action/investigate_in_new_timeline.test.ts index d7750dd1cd4cd..74f0aff7de934 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/cell_action/investigate_in_new_timeline.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/cell_action/investigate_in_new_timeline.test.ts @@ -107,6 +107,52 @@ describe('createAddToNewTimelineCellAction', () => { }) ).toEqual(false); }); + + it('should return true if the the user has read access to timeline', async () => { + const factory = createInvestigateInNewTimelineCellActionFactory({ + store, + services: { + ...services, + application: { + ...services.application, + capabilities: { + ...services.application.capabilities, + securitySolutionTimeline: { + read: true, + }, + }, + }, + }, + }); + const addToTimelineActionIsCompatible = factory({ + id: 'testAddToTimeline', + order: 1, + }); + expect(await addToTimelineActionIsCompatible.isCompatible(context)).toEqual(true); + }); + + it('should return flase if the user does not have access to timeline', async () => { + const factory = createInvestigateInNewTimelineCellActionFactory({ + store, + services: { + ...services, + application: { + ...services.application, + capabilities: { + ...services.application.capabilities, + securitySolutionTimeline: { + read: false, + }, + }, + }, + }, + }); + const addToTimelineActionIsCompatible = factory({ + id: 'testAddToTimeline', + order: 1, + }); + expect(await addToTimelineActionIsCompatible.isCompatible(context)).toEqual(false); + }); }); describe('execute', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/cell_action/investigate_in_new_timeline.ts b/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/cell_action/investigate_in_new_timeline.ts index ac0be8ea9eb15..f032843701ccf 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/cell_action/investigate_in_new_timeline.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/cell_action/investigate_in_new_timeline.ts @@ -19,6 +19,7 @@ import { addProvider, showTimeline } from '../../../../timelines/store/actions'; import { TimelineId } from '../../../../../common/types'; import type { SecurityAppStore } from '../../../../common/store'; import { fieldHasCellActions } from '../../utils'; +import { extractTimelineCapabilities } from '../../../../common/utils/timeline_capabilities'; import { ADD_TO_TIMELINE_FAILED_TEXT, ADD_TO_TIMELINE_FAILED_TITLE, @@ -39,7 +40,11 @@ export const createInvestigateInNewTimelineCellActionFactory = createCellActionF store: SecurityAppStore; services: StartServices; }): CellActionTemplate => { - const { notifications: notificationsService } = services; + const { + notifications: notificationsService, + application: { capabilities }, + } = services; + const timelineCapabilities = extractTimelineCapabilities(capabilities); return { type: SecurityCellActionType.INVESTIGATE_IN_NEW_TIMELINE, @@ -50,6 +55,7 @@ export const createInvestigateInNewTimelineCellActionFactory = createCellActionF const field = data[0]?.field; return ( + timelineCapabilities.read && data.length === 1 && // TODO Add support for multiple values fieldHasCellActions(field.name) && isValidDataProviderField(field.name, field.type) && diff --git a/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/discover/add_to_timeline.test.ts b/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/discover/add_to_timeline.test.ts index b79017aa322bb..240ef5a191a2a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/discover/add_to_timeline.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/discover/add_to_timeline.test.ts @@ -100,6 +100,54 @@ describe('createAddToTimelineDiscoverCellActionFactory', () => { }) ).toEqual(false); }); + + it('should return true if the user has read access to timeline', async () => { + const factory = createAddToTimelineDiscoverCellActionFactory({ + store, + services: { + ...services, + application: { + ...services.application, + capabilities: { + ...services.application.capabilities, + securitySolutionTimeline: { + read: true, + }, + }, + }, + }, + }); + const addToTimelineActionIsCompatible = factory({ + id: 'testAddToTimeline', + order: 1, + }); + + expect(await addToTimelineActionIsCompatible.isCompatible(context)).toEqual(true); + }); + + it('should return false if the user does not have access to timeline', async () => { + const factory = createAddToTimelineDiscoverCellActionFactory({ + store, + services: { + ...services, + application: { + ...services.application, + capabilities: { + ...services.application.capabilities, + securitySolutionTimeline: { + read: false, + }, + }, + }, + }, + }); + const addToTimelineActionIsCompatible = factory({ + id: 'testAddToTimeline', + order: 1, + }); + + expect(await addToTimelineActionIsCompatible.isCompatible(context)).toEqual(false); + }); }); describe('execute', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/discover/add_to_timeline.ts b/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/discover/add_to_timeline.ts index 27b309b2acee4..0690454151c30 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/discover/add_to_timeline.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/discover/add_to_timeline.ts @@ -8,6 +8,7 @@ import type { CellAction, CellActionFactory } from '@kbn/cell-actions'; import { isInSecurityApp } from '../../../../common/hooks/is_in_security_app'; import type { SecurityAppStore } from '../../../../common/store'; +import { extractTimelineCapabilities } from '../../../../common/utils/timeline_capabilities'; import type { StartServices } from '../../../../types'; import { createAddToTimelineCellActionFactory } from '../cell_action/add_to_timeline'; @@ -19,6 +20,7 @@ export const createAddToTimelineDiscoverCellActionFactory = ({ services: StartServices; }): CellActionFactory => { const { application } = services; + const timelineCapabilities = extractTimelineCapabilities(application.capabilities); let currentAppId: string | undefined; application.currentAppId$.subscribe((appId) => { @@ -31,6 +33,6 @@ export const createAddToTimelineDiscoverCellActionFactory = ({ }); return securityAddToTimelineActionFactory.combine({ - isCompatible: async () => isInSecurityApp(currentAppId), + isCompatible: async () => timelineCapabilities.read && isInSecurityApp(currentAppId), }); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/lens/add_to_timeline.test.ts b/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/lens/add_to_timeline.test.ts index 362fb5f9ee885..06437f3851762 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/lens/add_to_timeline.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/lens/add_to_timeline.test.ts @@ -163,8 +163,26 @@ describe('createAddToTimelineLensAction', () => { expect(await addToTimelineAction.isCompatible(context)).toEqual(false); }); - it('should return true if everything is okay', async () => { - expect(await addToTimelineAction.isCompatible(context)).toEqual(true); + it('should return false when the user does not have access to timeline', async () => { + ( + KibanaServices.get().application.capabilities.securitySolutionTimeline as { + crud: boolean; + read: boolean; + } + ).read = false; + const _action = createAddToTimelineLensAction({ store, order: 1 }); + expect(await _action.isCompatible(context)).toEqual(false); + }); + + it('should return true when the user has read access to timeline', async () => { + ( + KibanaServices.get().application.capabilities.securitySolutionTimeline as { + crud: boolean; + read: boolean; + } + ).read = true; + const _action = createAddToTimelineLensAction({ store, order: 1 }); + expect(await _action.isCompatible(context)).toEqual(false); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/lens/add_to_timeline.ts b/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/lens/add_to_timeline.ts index 2a2b11a9beb15..4849bf5f2d799 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/lens/add_to_timeline.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/app/actions/add_to_timeline/lens/add_to_timeline.ts @@ -14,6 +14,7 @@ import { KibanaServices } from '../../../../common/lib/kibana'; import type { SecurityAppStore } from '../../../../common/store/types'; import { addProvider } from '../../../../timelines/store/actions'; import type { DataProvider } from '../../../../../common/types'; +import { extractTimelineCapabilities } from '../../../../common/utils/timeline_capabilities'; import { EXISTS_OPERATOR, TimelineId } from '../../../../../common/types'; import { fieldHasCellActions } from '../../utils'; import { @@ -76,6 +77,7 @@ export const createAddToTimelineLensAction = ({ applicationService.currentAppId$.subscribe((appId) => { currentAppId = appId; }); + const timelineCapabilities = extractTimelineCapabilities(applicationService.capabilities); return createAction({ id: ACTION_ID, @@ -84,6 +86,7 @@ export const createAddToTimelineLensAction = ({ getIconType: () => ADD_TO_TIMELINE_ICON, getDisplayName: () => ADD_TO_TIMELINE, isCompatible: async ({ embeddable, data }) => + timelineCapabilities.read && !hasBlockingError(embeddable) && isLensApi(embeddable) && apiPublishesUnifiedSearch(embeddable) && diff --git a/x-pack/solutions/security/plugins/security_solution/public/app/app_routes.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/app/app_routes.test.tsx index 585323a7b8da9..20fecf03e6863 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/app/app_routes.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/app/app_routes.test.tsx @@ -14,7 +14,7 @@ import { noCasesCapabilities, readCasesCapabilities, } from '../cases_test_utils'; -import { CASES_FEATURE_ID, SERVER_APP_ID } from '../../common/constants'; +import { CASES_FEATURE_ID, SECURITY_FEATURE_ID } from '../../common/constants'; const mockNotFoundPage = jest.fn(() => null); jest.mock('./404', () => ({ @@ -33,7 +33,7 @@ describe('RedirectRoute', () => { it('RedirectRoute should redirect to overview page when siem and case privileges are all', () => { const mockCapabilities = { - [SERVER_APP_ID]: { show: true, crud: true }, + [SECURITY_FEATURE_ID]: { show: true, crud: true }, [CASES_FEATURE_ID]: allCasesCapabilities(), } as unknown as Capabilities; render(); @@ -42,7 +42,7 @@ describe('RedirectRoute', () => { it('RedirectRoute should redirect to overview page when siem and case privileges are read', () => { const mockCapabilities = { - [SERVER_APP_ID]: { show: true, crud: false }, + [SECURITY_FEATURE_ID]: { show: true, crud: false }, [CASES_FEATURE_ID]: readCasesCapabilities(), } as unknown as Capabilities; render(); @@ -51,7 +51,7 @@ describe('RedirectRoute', () => { it('RedirectRoute should redirect to not_found page when siem and case privileges are off', () => { const mockCapabilities = { - [SERVER_APP_ID]: { show: false, crud: false }, + [SECURITY_FEATURE_ID]: { show: false, crud: false }, [CASES_FEATURE_ID]: noCasesCapabilities(), } as unknown as Capabilities; render(); @@ -61,7 +61,7 @@ describe('RedirectRoute', () => { it('RedirectRoute should redirect to overview page when siem privilege is read and case privilege is all', () => { const mockCapabilities = { - [SERVER_APP_ID]: { show: true, crud: false }, + [SECURITY_FEATURE_ID]: { show: true, crud: false }, [CASES_FEATURE_ID]: allCasesCapabilities(), } as unknown as Capabilities; render(); @@ -70,7 +70,7 @@ describe('RedirectRoute', () => { it('RedirectRoute should redirect to overview page when siem privilege is read and case privilege is read', () => { const mockCapabilities = { - [SERVER_APP_ID]: { show: true, crud: false }, + [SECURITY_FEATURE_ID]: { show: true, crud: false }, [CASES_FEATURE_ID]: allCasesCapabilities(), } as unknown as Capabilities; render(); @@ -79,7 +79,7 @@ describe('RedirectRoute', () => { it('RedirectRoute should redirect to cases page when siem privilege is none and case privilege is read', () => { const mockCapabilities = { - [SERVER_APP_ID]: { show: false, crud: false }, + [SECURITY_FEATURE_ID]: { show: false, crud: false }, [CASES_FEATURE_ID]: readCasesCapabilities(), } as unknown as Capabilities; render(); @@ -88,7 +88,7 @@ describe('RedirectRoute', () => { it('RedirectRoute should redirect to cases page when siem privilege is none and case privilege is all', () => { const mockCapabilities = { - [SERVER_APP_ID]: { show: false, crud: false }, + [SECURITY_FEATURE_ID]: { show: false, crud: false }, [CASES_FEATURE_ID]: allCasesCapabilities(), } as unknown as Capabilities; render(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/app/app_routes.tsx b/x-pack/solutions/security/plugins/security_solution/public/app/app_routes.tsx index 4b3913bfb4d3c..86423642b4091 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/app/app_routes.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/app/app_routes.tsx @@ -10,14 +10,10 @@ import type { RouteProps } from 'react-router-dom'; import { Redirect } from 'react-router-dom'; import { Routes, Route } from '@kbn/shared-ux-router'; import type { Capabilities } from '@kbn/core/public'; -import { - CASES_FEATURE_ID, - CASES_PATH, - ONBOARDING_PATH, - SERVER_APP_ID, -} from '../../common/constants'; +import { CASES_FEATURE_ID, CASES_PATH, ONBOARDING_PATH } from '../../common/constants'; import { NotFoundPage } from './404'; import type { StartServices } from '../types'; +import { hasAccessToSecuritySolution } from '../helpers_access'; export interface AppRoutesProps { services: StartServices; @@ -37,7 +33,7 @@ export const AppRoutes: React.FC = React.memo(({ services, subPl AppRoutes.displayName = 'AppRoutes'; export const RedirectRoute = React.memo<{ capabilities: Capabilities }>(({ capabilities }) => { - if (capabilities[SERVER_APP_ID].show === true) { + if (hasAccessToSecuritySolution(capabilities)) { return ; } if (capabilities[CASES_FEATURE_ID].read_cases === true) { diff --git a/x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/assets_links.ts b/x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/assets_links.ts index c77e0fe7a03e7..b6bb88d7dd321 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/assets_links.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/assets_links.ts @@ -7,7 +7,7 @@ import { SecurityPageName, ExternalPageName } from '@kbn/security-solution-navigation'; import { ASSETS_PATH, CLOUD_DEFEND_PATH } from '../../../../../common/constants'; -import { SERVER_APP_ID } from '../../../../../common'; +import { SECURITY_FEATURE_ID } from '../../../../../common'; import type { LinkItem } from '../../../../common/links/types'; import type { SolutionNavLink } from '../../../../common/links'; import { IconEcctlLazy, IconFleetLazy } from './lazy_icons'; @@ -18,7 +18,7 @@ const assetsAppLink: LinkItem = { id: SecurityPageName.assets, title: i18n.ASSETS_TITLE, path: ASSETS_PATH, - capabilities: [`${SERVER_APP_ID}.show`], + capabilities: [`${SECURITY_FEATURE_ID}.show`], hideTimeline: true, skipUrlState: true, links: [], // endpoints and cloudDefend links are added in createAssetsLinkFromManage @@ -30,7 +30,7 @@ const assetsCloudDefendAppLink: LinkItem = { title: i18n.CLOUD_DEFEND_TITLE, description: i18n.CLOUD_DEFEND_DESCRIPTION, path: CLOUD_DEFEND_PATH, - capabilities: [`${SERVER_APP_ID}.show`], + capabilities: [`${SECURITY_FEATURE_ID}.show`], landingIcon: IconEcctlLazy, isBeta: true, hideTimeline: true, diff --git a/x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/investigations_links.ts b/x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/investigations_links.ts index ddcd88667d967..de41c8f74d6fa 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/investigations_links.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/investigations_links.ts @@ -7,7 +7,7 @@ import { ExternalPageName, SecurityPageName } from '@kbn/security-solution-navigation'; import { INVESTIGATIONS_PATH } from '../../../../../common/constants'; -import { SERVER_APP_ID } from '../../../../../common'; +import { SECURITY_FEATURE_ID } from '../../../../../common'; import type { LinkItem } from '../../../../common/links/types'; import type { SolutionNavLink } from '../../../../common/links'; import { IconOsqueryLazy, IconTimelineLazy } from './lazy_icons'; @@ -18,7 +18,7 @@ const investigationsAppLink: LinkItem = { id: SecurityPageName.investigations, title: i18n.INVESTIGATIONS_TITLE, path: INVESTIGATIONS_PATH, - capabilities: [`${SERVER_APP_ID}.show`], + capabilities: [`${SECURITY_FEATURE_ID}.show`], hideTimeline: true, skipUrlState: true, links: [], // timeline and note links are added via the methods below diff --git a/x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/ml_links.ts b/x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/ml_links.ts index f9ef049c73d30..e1e44d0e9195d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/ml_links.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/ml_links.ts @@ -12,7 +12,7 @@ import { } from '@kbn/security-solution-navigation'; import { MACHINE_LEARNING_PATH } from '../../../../../common/constants'; import type { LinkItem } from '../../../../common/links/types'; -import { SERVER_APP_ID } from '../../../../../common'; +import { SECURITY_FEATURE_ID } from '../../../../../common'; import type { SolutionLinkCategory, SolutionNavLink } from '../../../../common/links'; import { IconLensLazy, @@ -39,7 +39,7 @@ export const mlAppLink: LinkItem = { id: SecurityPageName.mlLanding, title: i18n.ML_TITLE, path: MACHINE_LEARNING_PATH, - capabilities: [[`${SERVER_APP_ID}.show`, `ml.canGetJobs`]], + capabilities: [[`${SECURITY_FEATURE_ID}.show`, `ml.canGetJobs`]], globalSearchKeywords: [i18n.ML_KEYWORD], hideTimeline: true, skipUrlState: true, diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/send_to_timeline/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/assistant/send_to_timeline/index.tsx index 86d9cf727be77..470e773003a67 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/assistant/send_to_timeline/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/assistant/send_to_timeline/index.tsx @@ -12,6 +12,7 @@ import type { Filter } from '@kbn/es-query'; import { useDispatch, useSelector } from 'react-redux'; import { useAssistantContext } from '@kbn/elastic-assistant'; +import { extractTimelineCapabilities } from '../../common/utils/timeline_capabilities'; import { sourcererSelectors } from '../../common/store'; import { sourcererActions } from '../../common/store/actions'; import { inputsActions } from '../../common/store/inputs'; @@ -36,6 +37,7 @@ import { useDiscoverInTimelineContext } from '../../common/components/discover_i import { useShowTimeline } from '../../common/utils/timeline/use_show_timeline'; import { useSourcererDataView } from '../../sourcerer/containers'; import { useDiscoverState } from '../../timelines/components/timeline/tabs/esql/use_discover_state'; +import { useKibana } from '../../common/lib/kibana'; export interface SendToTimelineButtonProps { asEmptyButton: boolean; @@ -61,7 +63,10 @@ export const SendToTimelineButton: FC { it('for serverless, it specifies capabilities as an AND condition, via a nested array', () => { expect(links.capabilities).toEqual([ - [`${SERVER_APP_ID}.show`, `${ATTACK_DISCOVERY_FEATURE_ID}.attack-discovery`], + [`${SECURITY_FEATURE_ID}.show`, `${ATTACK_DISCOVERY_FEATURE_ID}.attack-discovery`], ]); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/links.ts b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/links.ts index 531c0a1c48610..bab8fec37b6de 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/links.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/links.ts @@ -12,12 +12,14 @@ import { ATTACK_DISCOVERY_FEATURE_ID, ATTACK_DISCOVERY_PATH, SecurityPageName, - SERVER_APP_ID, + SECURITY_FEATURE_ID, } from '../../common/constants'; import type { LinkItem } from '../common/links/types'; export const links: LinkItem = { - capabilities: [[`${SERVER_APP_ID}.show`, `${ATTACK_DISCOVERY_FEATURE_ID}.attack-discovery`]], // This is an AND condition via the nested array + capabilities: [ + [`${SECURITY_FEATURE_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', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/index.test.tsx index fe59d4cda04cd..1445a5d1ba3cd 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/index.test.tsx @@ -66,7 +66,7 @@ jest.mock( jest.mock('../../common/links', () => ({ useLinkInfo: jest.fn().mockReturnValue({ - capabilities: ['siem.show'], + capabilities: ['siemV2.show'], globalNavPosition: 4, globalSearchKeywords: ['Attack discovery'], id: 'attack_discovery', @@ -117,7 +117,7 @@ jest.mock('../../common/lib/kibana', () => { services: { application: { capabilities: { - siem: { crud_alerts: true, read_alerts: true }, + siemV2: { crud_alerts: true, read_alerts: true }, }, navigateToUrl: jest.fn(), }, @@ -149,7 +149,7 @@ jest.mock('../../common/lib/kibana', () => { dataViews: mockDataViewsService, docLinks: { links: { - siem: { + siemV2: { privileges: 'link', }, }, diff --git a/x-pack/solutions/security/plugins/security_solution/public/cases/pages/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/cases/pages/index.tsx index 787dfc973c5d2..bc90dfcd0dc02 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/cases/pages/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/cases/pages/index.tsx @@ -21,6 +21,7 @@ import { SecuritySolutionPageWrapper } from '../../common/components/page_wrappe import { getEndpointDetailsPath } from '../../management/common/routing'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { useInsertTimeline } from '../components/use_insert_timeline'; +import { useUserPrivileges } from '../../common/components/user_privileges'; import * as timelineMarkdownPlugin from '../../common/components/markdown_editor/plugins/timeline'; import { useFetchAlertData } from './use_fetch_alert_data'; import { useUpsellingMessage } from '../../common/hooks/use_upselling'; @@ -33,6 +34,9 @@ const CaseContainerComponent: React.FC = () => { const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); const dispatch = useDispatch(); const { openFlyout } = useExpandableFlyoutApi(); + const { + timelinePrivileges: { read: canSeeTimeline }, + } = useUserPrivileges(); const interactionsUpsellingMessage = useUpsellingMessage('investigation_guide_interactions'); @@ -129,7 +133,10 @@ const CaseContainerComponent: React.FC = () => { editor_plugins: { parsingPlugin: timelineMarkdownPlugin.parser, processingPluginRenderer: timelineMarkdownPlugin.renderer, - uiPlugin: timelineMarkdownPlugin.plugin({ interactionsUpsellingMessage }), + uiPlugin: timelineMarkdownPlugin.plugin({ + interactionsUpsellingMessage, + canSeeTimeline, + }), }, hooks: { useInsertTimeline, diff --git a/x-pack/solutions/security/plugins/security_solution/public/cloud_defend/links.ts b/x-pack/solutions/security/plugins/security_solution/public/cloud_defend/links.ts index c1748664a966d..cbc0a710a7ba1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/cloud_defend/links.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/cloud_defend/links.ts @@ -7,13 +7,13 @@ import { getSecuritySolutionLink } from '@kbn/cloud-defend-plugin/public'; import { i18n } from '@kbn/i18n'; import type { SecurityPageName } from '../../common/constants'; -import { SERVER_APP_ID } from '../../common/constants'; +import { SECURITY_FEATURE_ID } from '../../common/constants'; import type { LinkItem } from '../common/links/types'; import { IconCloudDefend } from '../common/icons/cloud_defend'; const commonLinkProperties: Partial = { hideTimeline: true, - capabilities: [`${SERVER_APP_ID}.show`], + capabilities: [`${SECURITY_FEATURE_ID}.show`], }; export const cloudDefendLink: LinkItem = { diff --git a/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/links.ts b/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/links.ts index e0a66b54f91c0..d22284258680d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/links.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture/links.ts @@ -7,7 +7,7 @@ import { getSecuritySolutionLink } from '@kbn/cloud-security-posture-plugin/public'; import { i18n } from '@kbn/i18n'; import type { SecurityPageName } from '../../common/constants'; -import { SERVER_APP_ID } from '../../common/constants'; +import { SECURITY_FEATURE_ID } from '../../common/constants'; import cloudSecurityPostureDashboardImage from '../common/images/cloud_security_posture_dashboard_page.png'; import cloudNativeVulnerabilityManagementDashboardImage from '../common/images/cloud_native_vulnerability_management_dashboard_page.png'; import type { LinkItem } from '../common/links/types'; @@ -15,7 +15,7 @@ import { IconEndpoints } from '../common/icons/endpoints'; const commonLinkProperties: Partial = { hideTimeline: true, - capabilities: [`${SERVER_APP_ID}.show`], + capabilities: [`${SECURITY_FEATURE_ID}.show`], }; export const findingsLinks: LinkItem = { diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/control_columns/row_action/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/control_columns/row_action/index.test.tsx index 17039cd443888..37c24472db351 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/control_columns/row_action/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/control_columns/row_action/index.test.tsx @@ -19,6 +19,8 @@ import { DocumentDetailsRightPanelKey } from '../../../../flyout/document_detail import type { ExpandableFlyoutState } from '@kbn/expandable-flyout'; import { useExpandableFlyoutApi, useExpandableFlyoutState } from '@kbn/expandable-flyout'; import { createExpandableFlyoutApiMock } from '../../../mock/expandable_flyout'; +import { useUserPrivileges } from '../../user_privileges'; +import { initialUserPrivilegesState } from '../../user_privileges/user_privileges_context'; const mockDispatch = jest.fn(); jest.mock('react-redux', () => { @@ -57,6 +59,8 @@ jest.mock('@kbn/kibana-react-plugin/public', () => { }); jest.mock('../../guided_onboarding_tour/tour_step'); +jest.mock('../../user_privileges'); + const mockRouteSpy: RouteSpyState = { pageName: SecurityPageName.overview, detailName: undefined, @@ -140,4 +144,40 @@ describe('RowAction', () => { }, }); }); + + describe('privileges', () => { + test('should show notes and timeline buttons when the user has the required privileges', () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + ...initialUserPrivilegesState(), + notesPrivileges: { read: true }, + timelinePrivileges: { read: true }, + }); + + const wrapper = render( + + + + ); + + expect(wrapper.queryByTestId('timeline-notes-button-small')).toBeInTheDocument(); + expect(wrapper.queryByTestId('send-alert-to-timeline-button')).toBeInTheDocument(); + }); + + test('should not show notes and timeline buttons when the user does not have the required privileges', () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + ...initialUserPrivilegesState(), + notesPrivileges: { read: false }, + timelinePrivileges: { read: false }, + }); + + const wrapper = render( + + + + ); + + expect(wrapper.queryByTestId('timeline-notes-button-small')).not.toBeInTheDocument(); + expect(wrapper.queryByTestId('send-alert-to-timeline-button')).not.toBeInTheDocument(); + }); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx index a56010182f138..35d7aadc17118 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx @@ -26,6 +26,7 @@ import { useTourContext } from '../../guided_onboarding_tour'; import { AlertsCasesTourSteps, SecurityStepId } from '../../guided_onboarding_tour/tour_config'; import { NotesEventTypes, DocumentEventTypes } from '../../../lib/telemetry'; import { getMappedNonEcsValue } from '../../../utils/get_mapped_non_ecs_value'; +import { useUserPrivileges } from '../../user_privileges'; export type RowActionProps = EuiDataGridCellValueElementProps & { columnHeaders: ColumnHeaderOptions[]; @@ -97,6 +98,11 @@ const RowActionComponent = ({ const securitySolutionNotesDisabled = useIsExperimentalFeatureEnabled( 'securitySolutionNotesDisabled' ); + const { + notesPrivileges: { read: canReadNotes }, + timelinePrivileges: { read: canReadTimelines }, + } = useUserPrivileges(); + const showNotes = canReadNotes && !securitySolutionNotesDisabled; const handleOnEventDetailPanelOpened = useCallback(() => { openFlyout({ @@ -181,7 +187,8 @@ const RowActionComponent = ({ setEventsLoading={setEventsLoading} setEventsDeleted={setEventsDeleted} refetch={refetch} - showNotes={!securitySolutionNotesDisabled} + showNotes={showNotes} + disableTimelineAction={!canReadTimelines} /> )} diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/event_details/investigate_in_timeline_button.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/event_details/investigate_in_timeline_button.test.tsx index c9b2312c28f5e..6235dc77da438 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/event_details/investigate_in_timeline_button.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/event_details/investigate_in_timeline_button.test.tsx @@ -11,14 +11,19 @@ import React from 'react'; import { InvestigateInTimelineButton } from './investigate_in_timeline_button'; import { TestProviders } from '../../mock'; import { getDataProvider } from './use_action_cell_data_provider'; +import { useUserPrivileges } from '../user_privileges'; import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../detections/components/alerts_table/translations'; jest.mock('../../lib/kibana'); +jest.mock('../user_privileges'); describe('InvestigateInTimelineButton', () => { describe('When all props are provided', () => { test('it should display the add to timeline button', () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + timelinePrivileges: { read: true }, + }); const dataProviders = ['127.0.0.1', '::1', '10.1.2.3', '2001:0DB8:AC10:FE01::'].map( (ipValue) => getDataProvider('host.ip', '', ipValue) ); @@ -28,6 +33,22 @@ describe('InvestigateInTimelineButton', () => { ); expect(screen.queryByLabelText(ACTION_INVESTIGATE_IN_TIMELINE)).toBeInTheDocument(); + expect(screen.queryByLabelText(ACTION_INVESTIGATE_IN_TIMELINE)).not.toBeDisabled(); + }); + + it('should be disabled when the user has insufficient privileges', () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + timelinePrivileges: { read: false }, + }); + const dataProviders = ['127.0.0.1', '::1', '10.1.2.3', '2001:0DB8:AC10:FE01::'].map( + (ipValue) => getDataProvider('host.ip', '', ipValue) + ); + render( + + + + ); + expect(screen.queryByLabelText(ACTION_INVESTIGATE_IN_TIMELINE)).toBeDisabled(); }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/event_details/investigate_in_timeline_button.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/event_details/investigate_in_timeline_button.tsx index 506b6c768494f..7b5549822793b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/event_details/investigate_in_timeline_button.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/event_details/investigate_in_timeline_button.tsx @@ -13,6 +13,7 @@ import type { Filter } from '@kbn/es-query'; import type { TimeRange } from '../../store/inputs/model'; import type { DataProvider } from '../../../../common/types'; import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../detections/components/alerts_table/translations'; +import { useUserPrivileges } from '../user_privileges'; import { useInvestigateInTimeline } from '../../hooks/timeline/use_investigate_in_timeline'; export interface InvestigateInTimelineButtonProps { @@ -37,6 +38,10 @@ export interface InvestigateInTimelineButtonProps { iconType?: IconType; children?: React.ReactNode; flush?: EuiButtonEmptyProps['flush']; + /** + * Data test subject string for testing + */ + ['data-test-subj']?: string; } /** @@ -54,6 +59,8 @@ export const InvestigateInTimelineButton: FC< keepDataView, iconType, flush, + isDisabled, + 'data-test-subj': dataTestSubj, ...rest }) => { const { investigateInTimeline } = useInvestigateInTimeline(); @@ -65,6 +72,11 @@ export const InvestigateInTimelineButton: FC< keepDataView, }); }, [dataProviders, filters, timeRange, keepDataView, investigateInTimeline]); + const { + timelinePrivileges: { read: canUseTimeline }, + } = useUserPrivileges(); + + const disabled = !canUseTimeline || isDisabled; return asEmptyButton ? ( {children} ) : ( - + {children} ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx index 3d93dd19cf691..037f9f2c8a52a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx @@ -17,8 +17,10 @@ import { licenseService } from '../../hooks/use_license'; import { mockHistory } from '../../mock/router'; import { DEFAULT_EVENTS_STACK_BY_VALUE } from './histogram_configurations'; import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; +import { useUserPrivileges } from '../user_privileges'; jest.mock('../../hooks/use_experimental_features'); +jest.mock('../user_privileges'); const mockGetDefaultControlColumn = jest.fn(); jest.mock('../../../timelines/components/timeline/body/control_columns', () => ({ @@ -103,6 +105,9 @@ describe('EventsQueryTabBody', () => { beforeEach(() => { (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false); + (useUserPrivileges as jest.Mock).mockReturnValue({ + notesPrivileges: { read: true }, + }); jest.clearAllMocks(); }); @@ -216,7 +221,19 @@ describe('EventsQueryTabBody', () => { expect(mockGetDefaultControlColumn).toHaveBeenCalledWith(4); }); - it('should 6 columns on Action bar for Enterprise user', () => { + it('should have 4 columns on Action bar for non-Enterprise user and if user does not have Notes privileges', () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ notesPrivileges: { read: false } }); + + render( + + + + ); + + expect(mockGetDefaultControlColumn).toHaveBeenCalledWith(4); + }); + + it('should have 6 columns on Action bar for Enterprise user', () => { const licenseServiceMock = licenseService as jest.Mocked; licenseServiceMock.isEnterprise.mockReturnValue(true); @@ -229,7 +246,7 @@ describe('EventsQueryTabBody', () => { expect(mockGetDefaultControlColumn).toHaveBeenCalledWith(6); }); - it('should 6 columns on Action bar for Enterprise user and securitySolutionNotesDisabled is true', () => { + it('should have 5 columns on Action bar for Enterprise user and securitySolutionNotesDisabled is true', () => { const licenseServiceMock = licenseService as jest.Mocked; licenseServiceMock.isEnterprise.mockReturnValue(true); (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); @@ -242,4 +259,18 @@ describe('EventsQueryTabBody', () => { expect(mockGetDefaultControlColumn).toHaveBeenCalledWith(5); }); + + it('should have 5 columns on Action bar for Enterprise user and if user does not have Notes privileges', () => { + const licenseServiceMock = licenseService as jest.Mocked; + licenseServiceMock.isEnterprise.mockReturnValue(true); + (useUserPrivileges as jest.Mock).mockReturnValue({ notesPrivileges: { read: false } }); + + render( + + + + ); + + expect(mockGetDefaultControlColumn).toHaveBeenCalledWith(5); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx index 2bb51df635380..48b65e8ec1d79 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx @@ -46,6 +46,7 @@ import { } from '../../utils/global_query_string/helpers'; import type { BulkActionsProp } from '../toolbar/bulk_actions/types'; import { SecurityCellActionsTrigger } from '../cell_actions'; +import { useUserPrivileges } from '../user_privileges'; export const ALERTS_EVENTS_HISTOGRAM_ID = 'alertsOrEventsHistogramQuery'; @@ -61,6 +62,15 @@ export type EventsQueryTabBodyComponentProps = QueryTabBodyProps & { const EXTERNAL_ALERTS_URL_PARAM = 'onlyExternalAlerts'; +// we show a maximum of 6 action buttons +// - open flyout +// - investigate in timeline +// - 3-dot menu for more actions +// - add new note +// - session view +// - analyzer graph +const MAX_ACTION_BUTTON_COUNT = 6; + const EventsQueryTabBodyComponent: React.FC = ({ additionalFilters, deleteQuery, @@ -70,17 +80,27 @@ const EventsQueryTabBodyComponent: React.FC = startDate, tableId, }) => { + let ACTION_BUTTON_COUNT = MAX_ACTION_BUTTON_COUNT; + const dispatch = useDispatch(); const { globalFullScreen } = useGlobalFullScreen(); const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + const isEnterprisePlus = useLicense().isEnterprise(); - let ACTION_BUTTON_COUNT = isEnterprisePlus ? 6 : 5; + if (!isEnterprisePlus) { + ACTION_BUTTON_COUNT--; + } + + const { + notesPrivileges: { read: canReadNotes }, + } = useUserPrivileges(); const securitySolutionNotesDisabled = useIsExperimentalFeatureEnabled( 'securitySolutionNotesDisabled' ); - if (securitySolutionNotesDisabled) { + if (!canReadNotes || securitySolutionNotesDisabled) { ACTION_BUTTON_COUNT--; } + const leadingControlColumns = useMemo( () => getDefaultControlColumn(ACTION_BUTTON_COUNT), [ACTION_BUTTON_COUNT] diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/header_actions/actions.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/header_actions/actions.test.tsx index 818b56556e768..f4d264f9e3e3d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/header_actions/actions.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/header_actions/actions.test.tsx @@ -72,7 +72,7 @@ jest.mock('../../lib/kibana', () => { navigateToApp: jest.fn(), getUrlForApp: jest.fn(), capabilities: { - siem: { crud_alerts: true, read_alerts: true }, + siemV2: { crud_alerts: true, read_alerts: true }, }, }, cases: mockCasesContract(), @@ -600,4 +600,28 @@ describe('Actions', () => { expect(wrapper.find('[data-test-subj="pin-event"]').exists()).toBeTruthy(); }); }); + + describe('Timeline action', () => { + test('should show timeline action by default', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="send-alert-to-timeline-button"]').exists() + ).toBeTruthy(); + }); + + test('should hide timeline action when disableTimelineAction = true', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="send-alert-to-timeline-button"]').exists()).toBeFalsy(); + }); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/header_actions/actions.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/header_actions/actions.tsx index 4775b8f889028..dd2107efd1d88 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/header_actions/actions.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/header_actions/actions.tsx @@ -73,6 +73,7 @@ const ActionsComponent: React.FC = ({ refetch, toggleShowNotes, disablePinAction = true, + disableTimelineAction = false, }) => { const dispatch = useDispatch(); @@ -339,7 +340,7 @@ const ActionsComponent: React.FC = ({ )} <> - {timelineId !== TimelineId.active && ( + {!disableTimelineAction && timelineId !== TimelineId.active && ( { jest.clearAllMocks(); useUserPrivilegesMock.mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, + notesPrivileges: { crud: true, read: true }, endpointPrivileges: getEndpointPrivilegesInitialStateMock(), }); @@ -135,13 +135,13 @@ describe('AddEventNoteAction', () => { }); describe('button state', () => { - test('should disable the add note button when the user does NOT have crud privileges', () => { + test('should disable the add note button when the user does NOT have crud privileges and no notes have been created', () => { useUserPrivilegesMock.mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: false, read: true }, + notesPrivileges: { crud: false, read: true }, endpointPrivileges: getEndpointPrivilegesInitialStateMock(), }); - renderTestComponent(); + renderTestComponent({ notesCount: 0 }); expect(screen.getByTestId('timeline-notes-button-small-mock')).toHaveProperty( 'disabled', @@ -151,7 +151,7 @@ describe('AddEventNoteAction', () => { test('should enable the add note button when the user has crud privileges', () => { useUserPrivilegesMock.mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, + notesPrivileges: { crud: true, read: true }, endpointPrivileges: getEndpointPrivilegesInitialStateMock(), }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/header_actions/add_note_icon_item.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/header_actions/add_note_icon_item.tsx index f931042863a62..f95ebca1ad52a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/header_actions/add_note_icon_item.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/header_actions/add_note_icon_item.tsx @@ -24,7 +24,13 @@ const NOTES_ADD_TOOLTIP = i18n.translate( defaultMessage: 'Add note', } ); -const NOTES_COUNT_TOOLTIP = ({ notesCount }: { notesCount: number }) => +const NO_NOTES_TOOLTIP = i18n.translate( + 'xpack.securitySolution.timeline.body.notes.addNoteTooltip', + { + defaultMessage: 'No notes available', + } +); +const ADD_NOTES_COUNT_TOOLTIP = ({ notesCount }: { notesCount: number }) => i18n.translate( 'xpack.securitySolution.timeline.body.notes.addNote.multipleNotesAvailableTooltip', { @@ -34,6 +40,16 @@ const NOTES_COUNT_TOOLTIP = ({ notesCount }: { notesCount: number }) => } ); +const VIEW_NOTES_COUNT_TOOLTIP = ({ notesCount }: { notesCount: number }) => + i18n.translate( + 'xpack.securitySolution.timeline.body.notes.addNote.multipleNotesAvailableTooltip', + { + values: { notesCount }, + defaultMessage: + '{notesCount} {notesCount, plural, one {note} other {notes} } available. Click to view {notesCount, plural, one {it} other {them}}.', + } + ); + interface AddEventNoteActionProps { ariaLabel?: string; timelineType: TimelineType; @@ -52,22 +68,36 @@ const AddEventNoteActionComponent: React.FC = ({ eventId, notesCount, }) => { - const { kibanaSecuritySolutionsPrivileges } = useUserPrivileges(); + const { + notesPrivileges: { crud: canAddNotes, read: canViewNotes }, + } = useUserPrivileges(); - const NOTES_TOOLTIP = useMemo( - () => (notesCount > 0 ? NOTES_COUNT_TOOLTIP({ notesCount }) : NOTES_ADD_TOOLTIP), - [notesCount] - ); + const tooltip = useMemo(() => { + if (timelineType === TimelineTypeEnum.template) { + return NOTES_DISABLE_TOOLTIP; + } + if (canAddNotes) { + return notesCount > 0 ? ADD_NOTES_COUNT_TOOLTIP({ notesCount }) : NOTES_ADD_TOOLTIP; + } + if (canViewNotes) { + return notesCount > 0 ? VIEW_NOTES_COUNT_TOOLTIP({ notesCount }) : NO_NOTES_TOOLTIP; + } + + // we can return an empty string for tooltip because the icon is actually no shown at all + return ''; + }, [canAddNotes, canViewNotes, notesCount, timelineType]); + + const disabled = useMemo(() => !canAddNotes && notesCount === 0, [canAddNotes, notesCount]); return ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/header_actions/pin_event_action.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/header_actions/pin_event_action.test.tsx index 33358264d6417..b830b4dc1d6e0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/header_actions/pin_event_action.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/header_actions/pin_event_action.test.tsx @@ -25,7 +25,7 @@ describe('PinEventAction', () => { describe('isDisabled', () => { test('it disables the pin event button when the user does NOT have crud privileges', () => { useUserPrivilegesMock.mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: false, read: true }, + timelinePrivileges: { crud: false, read: true }, endpointPrivileges: getEndpointPrivilegesInitialStateMock(), }); @@ -46,7 +46,7 @@ describe('PinEventAction', () => { test('it enables the pin event button when the user has crud privileges', () => { useUserPrivilegesMock.mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, + timelinePrivileges: { crud: true, read: true }, endpointPrivileges: getEndpointPrivilegesInitialStateMock(), }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/header_actions/pin_event_action.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/header_actions/pin_event_action.tsx index b2569ac58eaf9..2322c2c985094 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/header_actions/pin_event_action.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/header_actions/pin_event_action.tsx @@ -31,7 +31,7 @@ const PinEventActionComponent: React.FC = ({ eventIsPinned, timelineType, }) => { - const { kibanaSecuritySolutionsPrivileges } = useUserPrivileges(); + const { timelinePrivileges } = useUserPrivileges(); const tooltipContent = useMemo( () => getPinTooltip({ @@ -51,7 +51,7 @@ const PinEventActionComponent: React.FC = ({ ariaLabel={ariaLabel} allowUnpinning={!eventHasNotes(noteIds)} data-test-subj="pin-event" - isDisabled={kibanaSecuritySolutionsPrivileges.crud === false} + isDisabled={timelinePrivileges.crud === false} isAlert={isAlert} onClick={onPinClicked} pinned={eventIsPinned} diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/markdown_editor/editor.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/markdown_editor/editor.tsx index 402c27efab56d..c1dcb839f9c0d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/markdown_editor/editor.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/markdown_editor/editor.tsx @@ -22,6 +22,7 @@ import type { ContextShape } from '@elastic/eui/src/components/markdown_editor/m import { uiPlugins, parsingPlugins, processingPlugins } from './plugins'; import { useUpsellingMessage } from '../../hooks/use_upselling'; +import { useUserPrivileges } from '../user_privileges'; interface MarkdownEditorProps { onChange: (content: string) => void; @@ -76,14 +77,18 @@ const MarkdownEditorComponent = forwardRef { return includePlugins ? uiPlugins({ insightsUpsellingMessage, interactionsUpsellingMessage, + canSeeTimeline, }) : undefined; - }, [includePlugins, insightsUpsellingMessage, interactionsUpsellingMessage]); + }, [includePlugins, canSeeTimeline, insightsUpsellingMessage, interactionsUpsellingMessage]); // @ts-expect-error update types useImperativeHandle(ref, () => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts b/x-pack/solutions/security/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts index b6067ac21eb1d..c76eb39a20c09 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts @@ -23,9 +23,11 @@ export const platinumOnlyPluginTokens = [insightMarkdownPlugin.insightPrefix]; export const uiPlugins = ({ insightsUpsellingMessage, interactionsUpsellingMessage, + canSeeTimeline, }: { insightsUpsellingMessage?: string; interactionsUpsellingMessage?: string; + canSeeTimeline: boolean; }) => { const currentPlugins = nonStatefulUiPlugins.map((plugin) => plugin.name); const insightPluginWithLicense = insightMarkdownPlugin.plugin({ @@ -33,6 +35,7 @@ export const uiPlugins = ({ }); const timelinePluginWithLicense = timelineMarkdownPlugin.plugin({ interactionsUpsellingMessage, + canSeeTimeline, }); const osqueryPluginWithLicense = osqueryMarkdownPlugin.plugin({ interactionsUpsellingMessage, diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/plugin.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/plugin.tsx index 4d5b2e14e0d95..1b1f368ffcad0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/plugin.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/plugin.tsx @@ -77,15 +77,17 @@ const TimelineEditor = memo(TimelineEditorComponent); export const plugin = ({ interactionsUpsellingMessage, + canSeeTimeline, }: { interactionsUpsellingMessage?: string; + canSeeTimeline: boolean; }): EuiMarkdownEditorUiPlugin => { return { name: ID, button: { label: interactionsUpsellingMessage ?? i18n.INSERT_TIMELINE, iconType: 'timeline', - isDisabled: !!interactionsUpsellingMessage, + isDisabled: !canSeeTimeline || !!interactionsUpsellingMessage, }, helpText: ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/processor.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/processor.tsx index c2460cf7f5d19..4d0c1c7ae2268 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/processor.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/processor.tsx @@ -13,6 +13,7 @@ import { useTimelineClick } from '../../../../utils/timeline/use_timeline_click' import type { TimelineProps } from './types'; import * as i18n from './translations'; import { useAppToasts } from '../../../../hooks/use_app_toasts'; +import { useUserPrivileges } from '../../../user_privileges'; export const TimelineMarkDownRendererComponent: React.FC = ({ id, @@ -22,6 +23,10 @@ export const TimelineMarkDownRendererComponent: React.FC = ({ const { addError } = useAppToasts(); const interactionsUpsellingMessage = useUpsellingMessage('investigation_guide_interactions'); + const { + timelinePrivileges: { read: canReadTimelines }, + } = useUserPrivileges(); + const isDisabled = !!interactionsUpsellingMessage || !canReadTimelines; const handleTimelineClick = useTimelineClick(); @@ -43,7 +48,7 @@ export const TimelineMarkDownRendererComponent: React.FC = ({ {title} diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts b/x-pack/solutions/security/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts index 2cfe665697b93..1711e4f224411 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts @@ -53,7 +53,7 @@ describe('When using useEndpointPrivileges hook', () => { catalogue: {}, management: {}, navLinks: {}, - siem: { + siemV2: { crud: true, show: true, }, diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/user_privileges/user_privileges_context.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/user_privileges/user_privileges_context.tsx index b17a043170e80..471adae7b2ee2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/user_privileges/user_privileges_context.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/user_privileges/user_privileges_context.tsx @@ -7,17 +7,21 @@ import React, { createContext, useEffect, useState } from 'react'; import type { Capabilities } from '@kbn/core/types'; -import { SERVER_APP_ID } from '../../../../common/constants'; +import { SECURITY_FEATURE_ID } from '../../../../common/constants'; import { useFetchListPrivileges } from '../../../detections/components/user_privileges/use_fetch_list_privileges'; import { useFetchDetectionEnginePrivileges } from '../../../detections/components/user_privileges/use_fetch_detection_engine_privileges'; import { getEndpointPrivilegesInitialState, useEndpointPrivileges } from './endpoint'; import type { EndpointPrivileges } from '../../../../common/endpoint/types'; +import { extractTimelineCapabilities } from '../../utils/timeline_capabilities'; +import { extractNotesCapabilities } from '../../utils/notes_capabilities'; export interface UserPrivilegesState { listPrivileges: ReturnType; detectionEnginePrivileges: ReturnType; endpointPrivileges: EndpointPrivileges; kibanaSecuritySolutionsPrivileges: { crud: boolean; read: boolean }; + timelinePrivileges: { crud: boolean; read: boolean }; + notesPrivileges: { crud: boolean; read: boolean }; } export const initialUserPrivilegesState = (): UserPrivilegesState => ({ @@ -25,6 +29,8 @@ export const initialUserPrivilegesState = (): UserPrivilegesState => ({ detectionEnginePrivileges: { loading: false, error: undefined, result: undefined }, endpointPrivileges: getEndpointPrivilegesInitialState(), kibanaSecuritySolutionsPrivileges: { crud: false, read: false }, + timelinePrivileges: { crud: false, read: false }, + notesPrivileges: { crud: false, read: false }, }); export const UserPrivilegesContext = createContext( initialUserPrivilegesState() @@ -39,8 +45,8 @@ export const UserPrivilegesProvider = ({ kibanaCapabilities, children, }: UserPrivilegesProviderProps) => { - const crud: boolean = kibanaCapabilities[SERVER_APP_ID].crud === true; - const read: boolean = kibanaCapabilities[SERVER_APP_ID].show === true; + const crud: boolean = kibanaCapabilities[SECURITY_FEATURE_ID].crud === true; + const read: boolean = kibanaCapabilities[SECURITY_FEATURE_ID].show === true; const [kibanaSecuritySolutionsPrivileges, setKibanaSecuritySolutionsPrivileges] = useState({ crud, read, @@ -50,6 +56,34 @@ export const UserPrivilegesProvider = ({ const detectionEnginePrivileges = useFetchDetectionEnginePrivileges(read); const endpointPrivileges = useEndpointPrivileges(); + const [timelinePrivileges, setTimelinePrivileges] = useState( + extractTimelineCapabilities(kibanaCapabilities) + ); + const [notesPrivileges, setNotesPrivileges] = useState( + extractNotesCapabilities(kibanaCapabilities) + ); + + useEffect(() => { + setNotesPrivileges((currPrivileges) => { + const { read: notesRead, crud: notesCrud } = extractNotesCapabilities(kibanaCapabilities); + if (currPrivileges.read !== notesRead || currPrivileges.crud !== notesCrud) { + return { read: notesRead, crud: notesCrud }; + } + return currPrivileges; + }); + }, [kibanaCapabilities]); + + useEffect(() => { + setTimelinePrivileges((currPrivileges) => { + const { read: timelineRead, crud: timelineCrud } = + extractTimelineCapabilities(kibanaCapabilities); + if (currPrivileges.read !== timelineRead || currPrivileges.crud !== timelineCrud) { + return { read: timelineRead, crud: timelineCrud }; + } + return currPrivileges; + }); + }, [kibanaCapabilities]); + useEffect(() => { setKibanaSecuritySolutionsPrivileges((currPrivileges) => { if (currPrivileges.read !== read || currPrivileges.crud !== crud) { @@ -66,6 +100,8 @@ export const UserPrivilegesProvider = ({ detectionEnginePrivileges, endpointPrivileges, kibanaSecuritySolutionsPrivileges, + timelinePrivileges, + notesPrivileges, }} > {children} diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/solutions/security/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts index 07a9220b3dfa4..c24142f63a843 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts @@ -32,7 +32,7 @@ import { DEFAULT_RULES_TABLE_REFRESH_SETTING, DEFAULT_RULE_REFRESH_INTERVAL_ON, DEFAULT_RULE_REFRESH_INTERVAL_VALUE, - SERVER_APP_ID, + SECURITY_FEATURE_ID, } from '../../../../common/constants'; import type { StartServices } from '../../../types'; import { createSecuritySolutionStorageMock } from '../../mock/mock_local_storage'; @@ -201,7 +201,11 @@ export const createStartServicesMock = ( ...core.application, capabilities: { ...core.application.capabilities, - [SERVER_APP_ID]: { + [SECURITY_FEATURE_ID]: { + crud: true, + read: true, + }, + securitySolutionTimeline: { crud: true, read: true, }, diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/links/links.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/links/links.test.tsx index 7ecd8541e909f..14c14246e44d3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/links/links.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/links/links.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { CASES_FEATURE_ID, SecurityPageName, SERVER_APP_ID } from '../../../common/constants'; +import { CASES_FEATURE_ID, SecurityPageName, SECURITY_FEATURE_ID } from '../../../common/constants'; import type { Capabilities } from '@kbn/core/types'; import { mockGlobalState, TestProviders } from '../mock'; import type { ILicense, LicenseType } from '@kbn/licensing-plugin/common/types'; @@ -53,7 +53,7 @@ const mockExperimentalDefaults = mockGlobalState.app.enableExperimental; const mockCapabilities = { [CASES_FEATURE_ID]: { read_cases: true, crud_cases: true }, - [SERVER_APP_ID]: { show: true }, + [SECURITY_FEATURE_ID]: { show: true }, } as unknown as Capabilities; const fakePageId = 'fakePage'; @@ -115,7 +115,7 @@ describe('Security links', () => { id: SecurityPageName.network, title: 'Network', path: '/network', - capabilities: [`${CASES_FEATURE_ID}.read_cases`, `${SERVER_APP_ID}.show`], + capabilities: [`${CASES_FEATURE_ID}.read_cases`, `${SECURITY_FEATURE_ID}.show`], experimentalKey: 'flagEnabled' as unknown as keyof typeof mockExperimentalDefaults, hideWhenExperimentalKey: 'flagDisabled' as unknown as keyof typeof mockExperimentalDefaults, licenseType: 'basic' as const, @@ -432,7 +432,7 @@ describe('Security links', () => { }); describe('hasCapabilities', () => { - const siemShow = 'siem.show'; + const siemShow = 'siemV2.show'; const createCases = 'securitySolutionCasesV2.create_cases'; const readCases = 'securitySolutionCasesV2.read_cases'; const pushCases = 'securitySolutionCasesV2.push_cases'; @@ -442,18 +442,20 @@ describe('Security links', () => { }); it('returns true when the capability requested is specified as a single value', () => { - expect(hasCapabilities(createCapabilities({ siem: { show: true } }), siemShow)).toBeTruthy(); + expect( + hasCapabilities(createCapabilities({ siemV2: { show: true } }), siemShow) + ).toBeTruthy(); }); it('returns true when the capability requested is a single entry in an array', () => { expect( - hasCapabilities(createCapabilities({ siem: { show: true } }), [siemShow]) + hasCapabilities(createCapabilities({ siemV2: { show: true } }), [siemShow]) ).toBeTruthy(); }); it("returns true when the capability requested is a single entry in an AND'd array format", () => { expect( - hasCapabilities(createCapabilities({ siem: { show: true } }), [[siemShow]]) + hasCapabilities(createCapabilities({ siemV2: { show: true } }), [[siemShow]]) ).toBeTruthy(); }); @@ -461,7 +463,7 @@ describe('Security links', () => { expect( hasCapabilities( createCapabilities({ - siem: { show: true }, + siemV2: { show: true }, securitySolutionCasesV2: { create_cases: false }, }), [siemShow, createCases] @@ -473,7 +475,7 @@ describe('Security links', () => { expect( hasCapabilities( createCapabilities({ - siem: { show: false }, + siemV2: { show: false }, securitySolutionCasesV2: { create_cases: true }, }), [siemShow, createCases] @@ -485,7 +487,7 @@ describe('Security links', () => { expect( hasCapabilities( createCapabilities({ - siem: { show: true }, + siemV2: { show: true }, securitySolutionCasesV2: { create_cases: false }, }), [readCases, createCases] @@ -497,7 +499,7 @@ describe('Security links', () => { expect( hasCapabilities( createCapabilities({ - siem: { show: true }, + siemV2: { show: true }, securitySolutionCasesV2: { read_cases: true, create_cases: true }, }), [[readCases, createCases]] @@ -509,7 +511,7 @@ describe('Security links', () => { expect( hasCapabilities( createCapabilities({ - siem: { show: false }, + siemV2: { show: false }, securitySolutionCasesV2: { read_cases: false, create_cases: true }, }), [siemShow, [readCases, createCases]] @@ -521,7 +523,7 @@ describe('Security links', () => { expect( hasCapabilities( createCapabilities({ - siem: { show: true }, + siemV2: { show: true }, securitySolutionCasesV2: { read_cases: false, create_cases: true }, }), [siemShow, [readCases, createCases]] @@ -533,7 +535,7 @@ describe('Security links', () => { expect( hasCapabilities( createCapabilities({ - siem: { show: true }, + siemV2: { show: true }, securitySolutionCasesV2: { read_cases: false, create_cases: true, push_cases: false }, }), [ diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/mock/create_store.ts b/x-pack/solutions/security/plugins/security_solution/public/common/mock/create_store.ts index 91eac259e61f2..09cb3f842f8ed 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/mock/create_store.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/mock/create_store.ts @@ -12,12 +12,11 @@ import { createStore } from '../store'; import { mockGlobalState } from './global_state'; import type { AppAction } from '../store/actions'; import type { Immutable } from '../../../common/endpoint/types'; -import type { StartServices } from '../../types'; import { createSecuritySolutionStorageMock } from './mock_local_storage'; +import { createStartServicesMock } from '../lib/kibana/kibana_react.mock'; const { storage: storageMock } = createSecuritySolutionStorageMock(); - -const kibanaMock = {} as unknown as StartServices; +const kibanaMock = createStartServicesMock(); export const createMockStore = ( state: State = mockGlobalState, diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/mock/test_providers.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/mock/test_providers.tsx index d5f1830857823..73490ece96624 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/mock/test_providers.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/mock/test_providers.tsx @@ -140,7 +140,7 @@ const TestProvidersWithPrivilegesComponent: React.FC = ({ | null = null; @@ -71,7 +71,7 @@ export const createStoreFactory = async ( index_mapping_outdated: null, }; try { - if (coreStart.application.capabilities[SERVER_APP_ID].show === true) { + if (hasAccessToSecuritySolution(coreStart.application.capabilities)) { signal = await coreStart.http.fetch(DETECTION_ENGINE_INDEX_URL, { version: '2023-10-31', method: 'GET', diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/utils/notes_capabilities.ts b/x-pack/solutions/security/plugins/security_solution/public/common/utils/notes_capabilities.ts new file mode 100644 index 0000000000000..895a560d61b5e --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/utils/notes_capabilities.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { Capabilities } from '@kbn/core/types'; + +export function extractNotesCapabilities(capabilities: Capabilities) { + const notesCrud = capabilities.securitySolutionNotes?.crud === true; + const notesRead = capabilities.securitySolutionNotes?.read === true; + return { read: notesRead, crud: notesCrud }; +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx index 1d3a758b70199..9bc192880ea89 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx @@ -10,9 +10,12 @@ import { allowedExperimentalValues } from '../../../../common/experimental_featu import { UpsellingService } from '@kbn/security-solution-upselling/service'; import { updateAppLinks } from '../../links'; import { appLinks } from '../../../app_links'; +import { useUserPrivileges } from '../../components/user_privileges'; import { useShowTimeline } from './use_show_timeline'; import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks'; +jest.mock('../../components/user_privileges'); + const mockUseLocation = jest.fn().mockReturnValue({ pathname: '/overview' }); jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -43,7 +46,7 @@ jest.mock('../../lib/kibana', () => { ...original.useKibana().services, application: { capabilities: { - siem: { + siemV2: { show: mockSiemUserCanRead(), }, }, @@ -58,6 +61,10 @@ const mockUiSettingsClient = uiSettingsServiceMock.createStartContract(); describe('use show timeline', () => { beforeAll(() => { + (useUserPrivileges as unknown as jest.Mock).mockReturnValue({ + timelinePrivileges: { read: true }, + }); + // initialize all App links before running test updateAppLinks(appLinks, { experimentalFeatures: allowedExperimentalValues, @@ -66,7 +73,7 @@ describe('use show timeline', () => { management: {}, catalogue: {}, actions: { show: true, crud: true }, - siem: { + siemV2: { show: true, crud: true, }, @@ -98,6 +105,24 @@ describe('use show timeline', () => { const { result } = renderHook(() => useShowTimeline()); await waitFor(() => expect(result.current).toEqual([false])); }); + it('hides timeline for users without timeline access', async () => { + (useUserPrivileges as unknown as jest.Mock).mockReturnValue({ + timelinePrivileges: { read: false }, + }); + + const { result } = renderHook(() => useShowTimeline()); + const showTimeline = result.current; + expect(showTimeline).toEqual([false]); + }); +}); +it('shows timeline for users with timeline read access', async () => { + (useUserPrivileges as unknown as jest.Mock).mockReturnValue({ + timelinePrivileges: { read: true }, + }); + + const { result } = renderHook(() => useShowTimeline()); + const showTimeline = result.current; + expect(showTimeline).toEqual([true]); }); describe('sourcererDataView', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx index c6554b80c2626..239ead8b4e481 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx @@ -8,14 +8,18 @@ import { useMemo } from 'react'; import { useLocation } from 'react-router-dom'; import { useShowTimelineForGivenPath } from './use_show_timeline_for_path'; +import { useUserPrivileges } from '../../components/user_privileges'; export const useShowTimeline = () => { const { pathname } = useLocation(); const getIsTimelineVisible = useShowTimelineForGivenPath(); + const { + timelinePrivileges: { read: canSeeTimeline }, + } = useUserPrivileges(); const showTimeline = useMemo( - () => getIsTimelineVisible(pathname), - [pathname, getIsTimelineVisible] + () => canSeeTimeline && getIsTimelineVisible(pathname), + [pathname, canSeeTimeline, getIsTimelineVisible] ); return [showTimeline]; }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts index 0e7b208c2dd1d..30f5b078770b5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline/use_show_timeline_for_path.ts @@ -12,6 +12,7 @@ import { getLinksWithHiddenTimeline } from '../../links'; import { SourcererScopeName } from '../../../sourcerer/store/model'; import { useSourcererDataView } from '../../../sourcerer/containers'; import { useKibana } from '../../lib/kibana'; +import { hasAccessToSecuritySolution } from '../../../helpers_access'; const isTimelinePathVisible = (currentPath: string): boolean => { const groupLinksWithHiddenTimelinePaths = getLinksWithHiddenTimeline().map((l) => l.path); @@ -21,7 +22,12 @@ const isTimelinePathVisible = (currentPath: string): boolean => { export const useShowTimelineForGivenPath = () => { const { indicesExist, dataViewId } = useSourcererDataView(SourcererScopeName.timeline); - const userHasSecuritySolutionVisible = useKibana().services.application.capabilities.siem.show; + const { + services: { + application: { capabilities }, + }, + } = useKibana(); + const userHasSecuritySolutionVisible = hasAccessToSecuritySolution(capabilities); const isTimelineAllowed = useMemo( () => userHasSecuritySolutionVisible && (indicesExist || dataViewId === null), diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline_capabilities.ts b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline_capabilities.ts new file mode 100644 index 0000000000000..b5340852ecbc0 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/utils/timeline_capabilities.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { Capabilities } from '@kbn/core/types'; + +export function extractTimelineCapabilities(capabilities: Capabilities) { + const timelineCrud = capabilities.securitySolutionTimeline?.crud === true; + const timelineRead = capabilities.securitySolutionTimeline?.read === true; + return { read: timelineRead, crud: timelineCrud }; +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/dashboards/links.ts b/x-pack/solutions/security/plugins/security_solution/public/dashboards/links.ts index 0f4f8800578f6..a6914e76734a8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/dashboards/links.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/dashboards/links.ts @@ -5,7 +5,7 @@ * 2.0. */ import { i18n } from '@kbn/i18n'; -import { DASHBOARDS_PATH, SecurityPageName, SERVER_APP_ID } from '../../common/constants'; +import { DASHBOARDS_PATH, SecurityPageName, SECURITY_FEATURE_ID } from '../../common/constants'; import { DASHBOARDS } from '../app/translations'; import type { LinkItem } from '../common/links/types'; import { links as kubernetesLinks } from '../kubernetes/links'; @@ -33,7 +33,7 @@ export const dashboardsLinks: LinkItem = { title: DASHBOARDS, path: DASHBOARDS_PATH, globalNavPosition: 1, - capabilities: [`${SERVER_APP_ID}.show`], + capabilities: [`${SECURITY_FEATURE_ID}.show`], globalSearchKeywords: [ i18n.translate('xpack.securitySolution.appLinks.dashboards', { defaultMessage: 'Dashboards', diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/step_rule_actions/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/step_rule_actions/index.test.tsx index 583769069157d..d0c1a729d1906 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/step_rule_actions/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/step_rule_actions/index.test.tsx @@ -28,7 +28,7 @@ jest.mock('../../../../common/lib/kibana', () => ({ application: { getUrlForApp: jest.fn(), capabilities: { - siem: { + siemV2: { crud: true, }, actions: { diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx index 6a31f9bee8fd7..61ca99a70405a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx @@ -45,6 +45,7 @@ import { } from '../../../rule_creation/components/related_integrations/test_helpers'; import { useEsqlAvailability } from '../../../../common/hooks/esql/use_esql_availability'; import { useMLRuleConfig } from '../../../../common/components/ml/hooks/use_ml_rule_config'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; // Set the extended default timeout for all define rule step form test jest.setTimeout(10 * 1000); @@ -207,6 +208,7 @@ jest.mock('../../../../detections/containers/detection_engine/rules/use_rule_fro jest.mock('../../../../common/hooks/esql/use_esql_availability'); jest.mock('../../../../common/components/ml/hooks/use_ml_rule_config'); +jest.mock('../../../../common/components/user_privileges'); const mockUseRuleFromTimeline = useRuleFromTimeline as jest.Mock; const onOpenTimeline = jest.fn(); @@ -226,6 +228,9 @@ describe('StepDefineRule', () => { loading: false, mlSuppressionFields: [], }); + (useUserPrivileges as jest.Mock).mockReturnValue({ + timelinePrivileges: { read: true }, + }); }); it('renders correctly', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx index 88423bb64cf05..c5c3f1754b048 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx @@ -93,6 +93,7 @@ import { usePersistentNewTermsState } from './use_persistent_new_terms_state'; import { usePersistentAlertSuppressionState } from './use_persistent_alert_suppression_state'; import { usePersistentThresholdState } from './use_persistent_threshold_state'; import { usePersistentQuery } from './use_persistent_query'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; import { usePersistentMachineLearningState } from './use_persistent_machine_learning_state'; import { usePersistentThreatMatchState } from './use_persistent_threat_match_state'; @@ -191,6 +192,9 @@ const StepDefineRuleComponent: FC = ({ const isThresholdRule = getIsThresholdRule(ruleType); const alertSuppressionUpsellingMessage = useUpsellingMessage('alert_suppression_rule_form'); const { getFields, reset, setFieldValue } = form; + const { + timelinePrivileges: { read: canAttachTimelineTemplates }, + } = useUserPrivileges(); // Callback for when user toggles between Data Views and Index Patterns const onChangeDataSource = useCallback( @@ -700,7 +704,7 @@ const StepDefineRuleComponent: FC = ({ component={PickTimeline} componentProps={{ idAria: 'detectionEngineStepDefineRuleTimeline', - isDisabled: isLoading, + isDisabled: isLoading || !canAttachTimelineTemplates, dataTestSubj: 'detectionEngineStepDefineRuleTimeline', }} /> diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx index 0326f5d3f1e5d..2de25b7d30a25 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx @@ -14,6 +14,7 @@ import React, { useCallback } from 'react'; import { MAX_MANUAL_RULE_RUN_BULK_SIZE } from '../../../../../../common/constants'; import type { TimeRange } from '../../../../rule_gaps/types'; import { useKibana } from '../../../../../common/lib/kibana'; +import { useUserPrivileges } from '../../../../../common/components/user_privileges'; import { convertRulesFilterToKQL } from '../../../../../../common/detection_engine/rule_management/rule_filtering'; import { DuplicateOptions } from '../../../../../../common/detection_engine/rule_management/constants'; import type { @@ -82,6 +83,9 @@ export const useBulkActions = ({ const { executeBulkAction } = useExecuteBulkAction(); const { bulkExport } = useBulkExport(); const downloadExportedRules = useDownloadExportedRules(); + const { + timelinePrivileges: { crud: canCreateTimelines }, + } = useUserPrivileges(); const { state: { isAllSelected, rules, loadingRuleIds, selectedRuleIds }, @@ -430,7 +434,7 @@ export const useBulkActions = ({ key: i18n.BULK_ACTION_APPLY_TIMELINE_TEMPLATE, name: i18n.BULK_ACTION_APPLY_TIMELINE_TEMPLATE, 'data-test-subj': 'applyTimelineTemplateBulk', - disabled: isEditDisabled, + disabled: !canCreateTimelines || isEditDisabled, onClick: handleBulkEdit(BulkActionEditTypeEnum.set_timeline), toolTipContent: missingActionPrivileges ? i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES @@ -592,6 +596,7 @@ export const useBulkActions = ({ filterOptions, completeBulkEditForm, startServices, + canCreateTimelines, ] ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx index fa14fc317a78a..e51dcb6f57cde 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx @@ -76,7 +76,7 @@ jest.mock('../../../../common/lib/kibana', () => { services: { timelines: { ...mockTimelines }, application: { - capabilities: { siem: { crud_alerts: true, read_alerts: true } }, + capabilities: { siemV2: { crud_alerts: true, read_alerts: true } }, }, cases: { ...mockCasesContract(), diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.test.tsx index 7173670f80a68..ab728622c65e9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.test.tsx @@ -13,6 +13,7 @@ import * as actions from '../actions'; import { coreMock } from '@kbn/core/public/mocks'; import { InvestigateInTimelineAction } from './investigate_in_timeline_action'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; const ecsRowData: Ecs = { _id: '1', @@ -28,6 +29,7 @@ const ecsRowData: Ecs = { }, }; +jest.mock('../../../../common/components/user_privileges'); jest.mock('../../../../common/lib/kibana'); jest.mock('../../../../common/lib/apm/use_start_transaction'); jest.mock('../../../../common/hooks/use_app_toasts'); @@ -48,6 +50,12 @@ const mockSendAlertToTimeline = jest.spyOn(actions, 'sendAlertToTimelineAction') (useAppToasts as jest.Mock).mockReturnValue({ addError: jest.fn(), }); +(useUserPrivileges as jest.Mock).mockReturnValue({ + timelinePrivileges: { + crud: true, + read: true, + }, +}); const props = { ecsRowData, @@ -79,4 +87,18 @@ describe('use investigate in timeline hook', () => { }); expect(mockSendAlertToTimeline).toHaveBeenCalledTimes(1); }); + test('it disables the button when the user does not have access to timeline', () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + timelinePrivileges: { + read: false, + }, + }); + + const wrapper = render( + + + + ); + expect(wrapper.getByTestId('send-alert-to-timeline-button')).toBeDisabled(); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx index 0b1168e91a7f1..768a80a9df6f1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx @@ -8,6 +8,7 @@ import React from 'react'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; import { ActionIconItem } from '../../../../common/components/header_actions/action_icon_item'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; import { ACTION_INVESTIGATE_IN_TIMELINE, @@ -32,6 +33,10 @@ const InvestigateInTimelineActionComponent: React.FC ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.test.tsx index 8de172d54a07f..868b390a3ceea 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.test.tsx @@ -19,11 +19,13 @@ import { EuiPopover, EuiContextMenu } from '@elastic/eui'; import * as timelineActions from '../../../../timelines/store/actions'; import { getTimelineTemplate } from '../../../../timelines/containers/api'; import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; jest.mock('../../../../common/lib/kibana'); jest.mock('../../../../timelines/containers/api'); jest.mock('../../../../common/lib/apm/use_start_transaction'); jest.mock('../../../../common/hooks/use_app_toasts'); +jest.mock('../../../../common/components/user_privileges'); const ecsRowData: Ecs = { _id: '1', @@ -269,6 +271,9 @@ describe('useInvestigateInTimeline', () => { }, }, }); + (useUserPrivileges as jest.Mock).mockReturnValue({ + timelinePrivileges: { read: true }, + }); }); afterEach(() => { jest.clearAllMocks(); @@ -392,4 +397,17 @@ describe('useInvestigateInTimeline', () => { }); }); }); + + describe('privileges', () => { + test('should not return a timeline action when the user does not have sufficient privileges', () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + timelinePrivileges: { read: false }, + }); + + const { result } = renderHook(() => useInvestigateInTimeline(props), { + wrapper: TestProviders, + }); + expect(result.current.investigateInTimelineActionItems).toHaveLength(0); + }); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx index 3b36452f9315d..7a0d02456ad3a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx @@ -32,6 +32,7 @@ import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction'; import { ALERTS_ACTIONS } from '../../../../common/lib/apm/user_actions'; import { defaultUdtHeaders } from '../../../../timelines/components/timeline/body/column_headers/default_headers'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; interface UseInvestigateInTimelineActionProps { ecsRowData?: Ecs | Ecs[] | null; @@ -189,17 +190,24 @@ export const useInvestigateInTimeline = ({ getExceptionFilter, ]); + const { + timelinePrivileges: { read: canInvestigateInTimeline }, + } = useUserPrivileges(); + const investigateInTimelineActionItems = useMemo( - () => [ - { - key: 'investigate-in-timeline-action-item', - 'data-test-subj': 'investigate-in-timeline-action-item', - disabled: ecsRowData == null, - onClick: investigateInTimelineAlertClick, - name: ACTION_INVESTIGATE_IN_TIMELINE, - }, - ], - [ecsRowData, investigateInTimelineAlertClick] + () => + canInvestigateInTimeline + ? [ + { + key: 'investigate-in-timeline-action-item', + 'data-test-subj': 'investigate-in-timeline-action-item', + disabled: ecsRowData == null, + onClick: investigateInTimelineAlertClick, + name: ACTION_INVESTIGATE_IN_TIMELINE, + }, + ] + : [], + [ecsRowData, investigateInTimelineAlertClick, canInvestigateInTimeline] ); return { diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/user_info/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/user_info/index.test.tsx index 8e84d1a5e4f0b..2db975340ca4e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/user_info/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/user_info/index.test.tsx @@ -26,7 +26,7 @@ describe('useUserInfo', () => { services: { application: { capabilities: { - siem: { + siemV2: { crud: true, }, }, @@ -68,7 +68,7 @@ describe('useUserInfo', () => { const wrapper = ({ children }: React.PropsWithChildren) => ( {children} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx index e87ebd14b7c88..667585c5f4e56 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx @@ -76,6 +76,8 @@ const userPrivilegesInitial: ReturnType = { canAccessFleet: false, }), kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, + timelinePrivileges: { crud: true, read: true }, + notesPrivileges: { crud: true, read: true }, }; describe('useAlertsPrivileges', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_actions_column.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_actions_column.tsx index 286d377d86f56..d9aa0a730da5a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_actions_column.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_actions_column.tsx @@ -24,6 +24,7 @@ import type { TimelineItem } from '../../../../common/search_strategy'; import { getAlertsDefaultModel } from '../../components/alerts_table/default_config'; import type { State } from '../../../common/store'; import { RowAction } from '../../../common/components/control_columns/row_action'; +import { useUserPrivileges } from '../../../common/components/user_privileges'; // we show a maximum of 6 action buttons // - open flyout @@ -46,11 +47,21 @@ export const getUseActionColumnHook = ACTION_BUTTON_COUNT--; } - // we only want to show the note icon if the new notes system feature flag is enabled + const { + timelinePrivileges: { read: canReadTimelines }, + notesPrivileges: { read: canReadNotes }, + } = useUserPrivileges(); + + // remove space if investigate timeline icon shouldn't be displayed + if (!canReadTimelines) { + ACTION_BUTTON_COUNT--; + } + + // remove space if add notes icon shouldn't be displayed const securitySolutionNotesDisabled = useIsExperimentalFeatureEnabled( 'securitySolutionNotesDisabled' ); - if (securitySolutionNotesDisabled) { + if (!canReadNotes || securitySolutionNotesDisabled) { ACTION_BUTTON_COUNT--; } diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/links.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/links.ts index 1228b21d3363c..20b4e031e5478 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/links.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/links.ts @@ -5,7 +5,7 @@ * 2.0. */ import { i18n } from '@kbn/i18n'; -import { ALERTS_PATH, SecurityPageName, SERVER_APP_ID } from '../../common/constants'; +import { ALERTS_PATH, SecurityPageName, SECURITY_FEATURE_ID } from '../../common/constants'; import { ALERTS } from '../app/translations'; import type { LinkItem } from '../common/links/types'; @@ -13,7 +13,7 @@ export const links: LinkItem = { id: SecurityPageName.alerts, title: ALERTS, path: ALERTS_PATH, - capabilities: [`${SERVER_APP_ID}.show`], + capabilities: [`${SECURITY_FEATURE_ID}.show`], globalNavPosition: 3, globalSearchKeywords: [ i18n.translate('xpack.securitySolution.appLinks.alerts', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx index 4f8b8a391ea87..b307ea26f8a0b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx @@ -104,7 +104,7 @@ jest.mock('../../../common/lib/kibana', () => { application: { navigateToUrl: jest.fn(), capabilities: { - siem: { crud_alerts: true, read_alerts: true }, + siemV2: { crud_alerts: true, read_alerts: true }, }, }, dataViews: mockDataViewsService, @@ -119,7 +119,7 @@ jest.mock('../../../common/lib/kibana', () => { }, docLinks: { links: { - siem: { + siemV2: { privileges: 'link', }, }, diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/hooks/use_risk_input_actions_panels.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/hooks/use_risk_input_actions_panels.test.tsx index 17e5dc616e536..4c2bd22fd525d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/hooks/use_risk_input_actions_panels.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/hooks/use_risk_input_actions_panels.test.tsx @@ -13,6 +13,7 @@ import React from 'react'; import { TestProviders } from '../../../../common/mock'; import { alertInputDataMock } from '../mocks'; import { useRiskInputActionsPanels } from './use_risk_input_actions_panels'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; const casesServiceMock = casesPluginMock.createStartContract(); const mockCanUseCases = jest.fn().mockReturnValue({ @@ -42,6 +43,13 @@ jest.mock('@kbn/kibana-react-plugin/public', () => { }; }); +jest.mock('../../../../common/components/user_privileges'); +(useUserPrivileges as jest.Mock).mockReturnValue({ + timelinePrivileges: { + read: false, + }, +}); + const TestMenu = ({ panels }: { panels: EuiContextMenuPanelDescriptor[] }) => ( ); @@ -89,4 +97,22 @@ describe('useRiskInputActionsPanels', () => { expect(container).not.toHaveTextContent('Add to existing case'); expect(container).not.toHaveTextContent('Add to new case'); }); + + it('displays the timeline action when user has sufficient privileges', () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + timelinePrivileges: { read: true }, + }); + const { container } = customRender(); + + expect(container).toHaveTextContent('Add to new timeline'); + }); + + it('does NOT display the timeline action when user has NO insufficient privileges', () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + timelinePrivileges: { read: false }, + }); + const { container } = customRender(); + + expect(container).not.toHaveTextContent('Add to new timeline'); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/hooks/use_risk_input_actions_panels.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/hooks/use_risk_input_actions_panels.tsx index 03b25bc85d8db..232b1f9f9d43d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/hooks/use_risk_input_actions_panels.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/hooks/use_risk_input_actions_panels.tsx @@ -16,6 +16,7 @@ import { get } from 'lodash/fp'; import { ALERT_RULE_NAME } from '@kbn/rule-data-utils'; import { useRiskInputActions } from './use_risk_input_actions'; import type { InputAlert } from '../../../hooks/use_risk_contributing_alerts'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; export const useRiskInputActionsPanels = (inputs: InputAlert[], closePopover: () => void) => { const { cases: casesService } = useKibana<{ cases?: CasesService }>().services; @@ -25,6 +26,9 @@ export const useRiskInputActionsPanels = (inputs: InputAlert[], closePopover: () ); const userCasesPermissions = casesService?.helpers.canUseCases([SECURITY_SOLUTION_OWNER]); const hasCasesPermissions = userCasesPermissions?.create && userCasesPermissions?.read; + const { + timelinePrivileges: { read: canAddToTimeline }, + } = useUserPrivileges(); return useMemo(() => { const timelinePanel = { @@ -68,33 +72,42 @@ export const useRiskInputActionsPanels = (inputs: InputAlert[], closePopover: () /> ), id: 0, - items: hasCasesPermissions - ? [ - timelinePanel, - { - name: ( - - ), + items: [ + ...(canAddToTimeline ? [timelinePanel] : []), + ...(hasCasesPermissions + ? [ + { + name: ( + + ), - onClick: addToNewCaseClick, - }, + onClick: addToNewCaseClick, + }, - { - name: ( - - ), + { + name: ( + + ), - onClick: addToExistingCase, - }, - ] - : [timelinePanel], + onClick: addToExistingCase, + }, + ] + : []), + ], }, ]; - }, [addToExistingCase, addToNewCaseClick, addToNewTimeline, inputs, hasCasesPermissions]); + }, [ + addToExistingCase, + addToNewCaseClick, + addToNewTimeline, + inputs, + hasCasesPermissions, + canAddToTimeline, + ]); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/explore/links.ts b/x-pack/solutions/security/plugins/security_solution/public/explore/links.ts index a65eccc95c372..18f176494fffe 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/explore/links.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/explore/links.ts @@ -11,7 +11,7 @@ import { NETWORK_PATH, USERS_PATH, EXPLORE_PATH, - SERVER_APP_ID, + SECURITY_FEATURE_ID, SecurityPageName, } from '../../common/constants'; import { EXPLORE, HOSTS, NETWORK, USERS } from '../app/translations'; @@ -34,7 +34,7 @@ const networkLinks: LinkItem = { defaultMessage: 'Network', }), ], - capabilities: [`${SERVER_APP_ID}.show`], + capabilities: [`${SECURITY_FEATURE_ID}.show`], links: [ { id: SecurityPageName.networkFlows, @@ -97,7 +97,7 @@ const usersLinks: LinkItem = { defaultMessage: 'Users', }), ], - capabilities: [`${SERVER_APP_ID}.show`], + capabilities: [`${SECURITY_FEATURE_ID}.show`], links: [ { id: SecurityPageName.usersAll, @@ -152,7 +152,7 @@ const hostsLinks: LinkItem = { defaultMessage: 'Hosts', }), ], - capabilities: [`${SERVER_APP_ID}.show`], + capabilities: [`${SECURITY_FEATURE_ID}.show`], links: [ { id: SecurityPageName.hostsAll, @@ -209,7 +209,7 @@ export const exploreLinks: LinkItem = { title: EXPLORE, path: EXPLORE_PATH, globalNavPosition: 9, - capabilities: [`${SERVER_APP_ID}.show`], + capabilities: [`${SECURITY_FEATURE_ID}.show`], globalSearchKeywords: [ i18n.translate('xpack.securitySolution.appLinks.explore', { defaultMessage: 'Explore', diff --git a/x-pack/solutions/security/plugins/security_solution/public/explore/network/pages/network.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/explore/network/pages/network.test.tsx index 4f44045cbf6f3..ab4627f911165 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/explore/network/pages/network.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/explore/network/pages/network.test.tsx @@ -82,7 +82,7 @@ jest.mock('../../../common/lib/kibana', () => { application: { ...original.useKibana().services.application, capabilities: { - siem: { crud_alerts: true, read_alerts: true }, + siemV2: { crud_alerts: true, read_alerts: true }, maps: mockMapVisibility(), }, navigateToApp: mockNavigateToApp, diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/attach_to_active_timeline.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/attach_to_active_timeline.test.tsx index ad9f2444882e7..4566676fe04b8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/attach_to_active_timeline.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/attach_to_active_timeline.test.tsx @@ -24,7 +24,7 @@ const mockSetAttachToTimeline = jest.fn(); describe('AttachToActiveTimeline', () => { it('should render the component for an unsaved timeline', () => { (useUserPrivileges as jest.Mock).mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: true }, + timelinePrivileges: { crud: true }, }); const mockStore = createMockStore({ ...mockGlobalState, diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/notes_details.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/notes_details.test.tsx index c6f8b51817de7..feb644efa8ce4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/notes_details.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/notes_details.test.tsx @@ -84,7 +84,8 @@ describe('NotesDetails', () => { beforeEach(() => { jest.clearAllMocks(); useUserPrivilegesMock.mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: true }, + notesPrivileges: { crud: true }, + timelinePrivileges: { crud: true }, }); (useWhichFlyout as jest.Mock).mockReturnValue(Flyouts.timeline); (useBasicDataFromDetailsData as jest.Mock).mockReturnValue({ isAlert: true }); @@ -196,7 +197,8 @@ describe('NotesDetails', () => { it('should not render the add note section for users without crud privileges', () => { useUserPrivilegesMock.mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: false }, + notesPrivileges: { crud: false }, + timelinePrivileges: { crud: false }, }); const { queryByTestId } = renderNotesDetails(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/notes_details.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/notes_details.tsx index 8877e1759c475..e9c667d748be7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/notes_details.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/notes_details.tsx @@ -56,12 +56,12 @@ export const NotesDetails = memo(() => { const { addError: addErrorToast } = useAppToasts(); const dispatch = useDispatch(); const { eventId, dataFormattedForFieldBrowser } = useDocumentDetailsContext(); - const { kibanaSecuritySolutionsPrivileges } = useUserPrivileges(); + const { notesPrivileges } = useUserPrivileges(); const { basicAlertData: basicData } = useInvestigationGuide({ dataFormattedForFieldBrowser, }); - const canCreateNotes = kibanaSecuritySolutionsPrivileges.crud; + const canCreateNotes = notesPrivileges.crud; // will drive the value we send to the AddNote component // if true (timeline is saved and the user kept the checkbox checked) we'll send the timelineId to the AddNote component diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/prevalence_details.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/prevalence_details.test.tsx index e0bef746b213b..e768719a00be4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/prevalence_details.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/prevalence_details.test.tsx @@ -20,6 +20,7 @@ import { PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID, PREVALENCE_DETAILS_TABLE_PREVIEW_LINK_CELL_TEST_ID, PREVALENCE_DETAILS_TABLE_UPSELL_CELL_TEST_ID, + PREVALENCE_DETAILS_TABLE_INVESTIGATE_IN_TIMELINE_BUTTON_TEST_ID, } from './test_ids'; import { usePrevalence } from '../../shared/hooks/use_prevalence'; import { TestProviders } from '../../../../common/mock'; @@ -31,8 +32,10 @@ import { HOST_PREVIEW_BANNER } from '../../right/components/host_entity_overview import { UserPreviewPanelKey } from '../../../entity_details/user_right'; import { USER_PREVIEW_BANNER } from '../../right/components/user_entity_overview'; import { createTelemetryServiceMock } from '../../../../common/lib/telemetry/telemetry_service.mock'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; jest.mock('@kbn/expandable-flyout'); +jest.mock('../../../../common/components/user_privileges'); const mockedTelemetry = createTelemetryServiceMock(); jest.mock('../../../../common/lib/kibana', () => { @@ -134,6 +137,7 @@ describe('PrevalenceDetails', () => { jest.clearAllMocks(); licenseServiceMock.isPlatinumPlus.mockReturnValue(true); jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi); + (useUserPrivileges as jest.Mock).mockReturnValue({ timelinePrivileges: { read: true } }); }); it('should render the table with all data if license is platinum', () => { @@ -219,7 +223,7 @@ describe('PrevalenceDetails', () => { ).not.toHaveTextContent('10%'); }); - it('should render formatted numbers for the alert and document count columns', () => { + it('should render formatted numbers for the alert and document count columns and be clickable buttons', () => { (usePrevalence as jest.Mock).mockReturnValue({ loading: false, error: false, @@ -235,7 +239,7 @@ describe('PrevalenceDetails', () => { ], }); - const { getByTestId } = render( + const { getByTestId, getAllByTestId } = render( @@ -254,6 +258,52 @@ describe('PrevalenceDetails', () => { expect(getByTestId(PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID)).toHaveTextContent( '10%' ); + + expect( + getAllByTestId(PREVALENCE_DETAILS_TABLE_INVESTIGATE_IN_TIMELINE_BUTTON_TEST_ID).length + ).toBeGreaterThan(1); + }); + + it('should render formatted numbers as text if user lacks timeline read privileges', () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ timelinePrivileges: { read: false } }); + (usePrevalence as jest.Mock).mockReturnValue({ + loading: false, + error: false, + data: [ + { + field: 'field1', + values: ['value1'], + alertCount: 1000, + docCount: 2000000, + hostPrevalence: 0.05, + userPrevalence: 0.1, + }, + ], + }); + + const { getByTestId, queryAllByTestId } = render( + + + + + + ); + + expect(getByTestId(PREVALENCE_DETAILS_TABLE_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID)).toHaveTextContent('field1'); + expect(getByTestId(PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID)).toHaveTextContent('value1'); + expect(getByTestId(PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID)).toHaveTextContent('1k'); + expect(getByTestId(PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID)).toHaveTextContent('2M'); + expect(getByTestId(PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID)).toHaveTextContent( + '5%' + ); + expect(getByTestId(PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID)).toHaveTextContent( + '10%' + ); + + expect( + queryAllByTestId(PREVALENCE_DETAILS_TABLE_INVESTIGATE_IN_TIMELINE_BUTTON_TEST_ID).length + ).not.toBeGreaterThan(1); }); it('should render multiple values in value column', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/prevalence_details.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/prevalence_details.tsx index 77ab6eb66df31..d2c90f43e1a5d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/prevalence_details.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/prevalence_details.tsx @@ -39,6 +39,8 @@ import { PREVALENCE_DETAILS_TABLE_TEST_ID, PREVALENCE_DETAILS_UPSELL_TEST_ID, PREVALENCE_DETAILS_TABLE_UPSELL_CELL_TEST_ID, + PREVALENCE_DETAILS_TABLE_INVESTIGATE_IN_TIMELINE_BUTTON_TEST_ID, + PREVALENCE_DETAILS_TABLE_COUNT_TEXT_BUTTON_TEST_ID, } from './test_ids'; import { useDocumentDetailsContext } from '../../shared/context'; import { @@ -49,6 +51,7 @@ import { getEmptyTagValue } from '../../../../common/components/empty_value'; import { IS_OPERATOR } from '../../../../../common/types'; import { hasPreview, PreviewLink } from '../../../shared/components/preview_link'; import { CellActions } from '../../shared/components/cell_actions'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; export const PREVALENCE_TAB_ID = 'prevalence'; const DEFAULT_FROM = 'now-30d'; @@ -84,6 +87,10 @@ interface PrevalenceDetailsRow extends PrevalenceData { * Scope id to pass to the preview link */ scopeId: string; + /** + * True if user have the correct timeline read privilege + */ + canUseTimeline: boolean; } const columns: Array> = [ @@ -169,17 +176,29 @@ const columns: Array> = [ const dataProviders = data.values.map((value) => getDataProvider(data.field, `timeline-indicator-${data.field}-${value}`, value) ); - return data.alertCount > 0 ? ( + + if (data.alertCount === 0) { + return getEmptyTagValue(); + } + + const alertCount = ; + if (!data.canUseTimeline) { + return ( + + {alertCount} + + ); + } + return ( - + {alertCount} - ) : ( - getEmptyTagValue() ); }, width: '10%', @@ -224,18 +243,30 @@ const columns: Array> = [ ), ], })); - return data.docCount > 0 ? ( + + if (data.docCount === 0) { + return getEmptyTagValue(); + } + + const docCount = ; + if (!data.canUseTimeline) { + return ( + + {docCount} + + ); + } + return ( - + {docCount} - ) : ( - getEmptyTagValue() ); }, width: '10%', @@ -325,6 +356,10 @@ export const PrevalenceDetails: React.FC = () => { const { dataFormattedForFieldBrowser, investigationFields, scopeId } = useDocumentDetailsContext(); + const { + timelinePrivileges: { read: canUseTimeline }, + } = useUserPrivileges(); + const isPlatinumPlus = useLicense().isPlatinumPlus(); // these two are used by the usePrevalence hook to fetch the data @@ -377,8 +412,9 @@ export const PrevalenceDetails: React.FC = () => { to: absoluteEnd, isPlatinumPlus, scopeId, + canUseTimeline, })), - [data, absoluteStart, absoluteEnd, isPlatinumPlus, scopeId] + [data, absoluteStart, absoluteEnd, canUseTimeline, isPlatinumPlus, scopeId] ); const upsell = ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts index 6979fa9cfa053..6765ed751b359 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/components/test_ids.ts @@ -32,6 +32,10 @@ export const PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID = `${PREVALENCE_DETAILS_TABLE_TEST_ID}AlertCountCell` as const; export const PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID = `${PREVALENCE_DETAILS_TABLE_TEST_ID}DocCountCell` as const; +export const PREVALENCE_DETAILS_TABLE_INVESTIGATE_IN_TIMELINE_BUTTON_TEST_ID = + `${PREVALENCE_DETAILS_TABLE_TEST_ID}InvestigateInTimelineButton` as const; +export const PREVALENCE_DETAILS_TABLE_COUNT_TEXT_BUTTON_TEST_ID = + `${PREVALENCE_DETAILS_TABLE_TEST_ID}CountTextButton` as const; export const PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID = `${PREVALENCE_DETAILS_TABLE_TEST_ID}HostPrevalenceCell` as const; export const PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID = diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/index.tsx index 6dcf9da06d2b6..63aaeca39685b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/left/index.tsx @@ -11,6 +11,7 @@ import React, { memo, useMemo } from 'react'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { useUiSetting$ } from '@kbn/kibana-react-plugin/public'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; +import { useUserPrivileges } from '../../../common/components/user_privileges'; import { ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING } from '../../../../common/constants'; import { DocumentDetailsLeftPanelKey } from '../shared/constants/panel_keys'; import { useKibana } from '../../../common/lib/kibana'; @@ -39,6 +40,9 @@ export const LeftPanel: FC> = memo(({ path }) => { const securitySolutionNotesDisabled = useIsExperimentalFeatureEnabled( 'securitySolutionNotesDisabled' ); + const { + notesPrivileges: { read: canSeeNotes }, + } = useUserPrivileges(); const [visualizationInFlyoutEnabled] = useUiSetting$( ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING @@ -49,14 +53,20 @@ export const LeftPanel: FC> = memo(({ path }) => { eventKind === EventKind.signal ? [tabs.insightsTab, tabs.investigationTab, tabs.responseTab] : [tabs.insightsTab]; - if (!securitySolutionNotesDisabled && !isPreview) { + if (canSeeNotes && !securitySolutionNotesDisabled && !isPreview) { tabList.push(tabs.notesTab); } if (visualizationInFlyoutEnabled && !isPreview) { return [tabs.visualizeTab, ...tabList]; } return tabList; - }, [eventKind, isPreview, securitySolutionNotesDisabled, visualizationInFlyoutEnabled]); + }, [ + eventKind, + isPreview, + canSeeNotes, + securitySolutionNotesDisabled, + visualizationInFlyoutEnabled, + ]); const selectedTabId = useMemo(() => { const defaultTab = tabsDisplayed[0].id; diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/notes.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/notes.test.tsx index 3e07d9df1f79d..5ad5d878f6946 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/notes.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/notes.test.tsx @@ -50,7 +50,7 @@ describe('', () => { beforeEach(() => { jest.clearAllMocks(); (useUserPrivileges as jest.Mock).mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: true }, + notesPrivileges: { crud: true, read: true }, }); (useNavigateToLeftPanel as jest.Mock).mockReturnValue({ navigateToLeftPanel: mockNavigateToLeftPanel, @@ -280,7 +280,7 @@ describe('', () => { it('should show View note button if user does not have the correct privileges but notes have already been created', () => { (useUserPrivileges as jest.Mock).mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: false }, + notesPrivileges: { crud: false, read: true }, }); const contextValue = { @@ -313,7 +313,7 @@ describe('', () => { it('should show a - if user does not have the correct privileges and no notes have been created', () => { (useUserPrivileges as jest.Mock).mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: false }, + notesPrivileges: { read: true, crud: false }, }); const { getByText, queryByTestId } = render( diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/notes.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/notes.tsx index 46377ee6db628..6538854b93fe6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/notes.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/notes.tsx @@ -71,7 +71,7 @@ export const Notes = memo(() => { const dispatch = useDispatch(); const { eventId, isPreview } = useDocumentDetailsContext(); const { addError: addErrorToast } = useAppToasts(); - const { kibanaSecuritySolutionsPrivileges } = useUserPrivileges(); + const { notesPrivileges } = useUserPrivileges(); const { navigateToLeftPanel: openExpandedFlyoutNotesTab, isEnabled: isLinkEnabled } = useNavigateToLeftPanel({ @@ -79,13 +79,15 @@ export const Notes = memo(() => { }); const isNotesDisabled = !isLinkEnabled || isPreview; + const cannotAddNotes = isNotesDisabled || !notesPrivileges.crud; + const cannotReadNotes = isNotesDisabled || !notesPrivileges.read; useEffect(() => { - // only fetch notes if we are not in a preview panel, or not in a rule preview workflow - if (!isNotesDisabled) { + // fetch notes only if we are not in a preview panel, or not in a rule preview workflow, and if the user has the correct privileges + if (!cannotReadNotes) { dispatch(fetchNotesByDocumentIds({ documentIds: [eventId] })); } - }, [dispatch, eventId, isNotesDisabled]); + }, [dispatch, eventId, cannotReadNotes]); const fetchStatus = useSelector((state: State) => selectFetchNotesByDocumentIdsStatus(state)); const fetchError = useSelector((state: State) => selectFetchNotesByDocumentIdsError(state)); @@ -106,7 +108,7 @@ export const Notes = memo(() => { @@ -117,7 +119,7 @@ export const Notes = memo(() => { /> ), - [isNotesDisabled, notes.length, openExpandedFlyoutNotesTab] + [cannotReadNotes, notes.length, openExpandedFlyoutNotesTab] ); const addNoteButton = useMemo( () => ( @@ -125,21 +127,21 @@ export const Notes = memo(() => { iconType="plusInCircle" onClick={openExpandedFlyoutNotesTab} size="s" - disabled={isNotesDisabled} + disabled={cannotAddNotes} aria-label={ADD_NOTE_BUTTON} data-test-subj={NOTES_ADD_NOTE_BUTTON_TEST_ID} > {ADD_NOTE_BUTTON} ), - [isNotesDisabled, openExpandedFlyoutNotesTab] + [cannotAddNotes, openExpandedFlyoutNotesTab] ); const addNoteButtonIcon = useMemo( () => ( { data-test-subj={NOTES_ADD_NOTE_ICON_BUTTON_TEST_ID} /> ), - [ - euiTheme.size.xs, - isNotesDisabled, - kibanaSecuritySolutionsPrivileges.crud, - openExpandedFlyoutNotesTab, - ] + [euiTheme.size.xs, cannotAddNotes, openExpandedFlyoutNotesTab] ); return ( @@ -174,14 +171,14 @@ export const Notes = memo(() => { ) : ( <> {notes.length === 0 ? ( - <>{kibanaSecuritySolutionsPrivileges.crud ? addNoteButton : getEmptyTagValue()} + <>{notesPrivileges.crud ? addNoteButton : getEmptyTagValue()} ) : ( - {kibanaSecuritySolutionsPrivileges.crud ? addNoteButtonIcon : viewNotesButton} + {notesPrivileges.crud ? addNoteButtonIcon : viewNotesButton} )} diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.test.tsx index d9ae4673b1749..711d441c8ba39 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.test.tsx @@ -12,8 +12,16 @@ import { AlertCountInsight, getFormattedAlertStats } from './alert_count_insight import { useAlertsByStatus } from '../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status'; import type { ParsedAlertsData } from '../../../../overview/components/detection_response/alerts_by_status/types'; import { SEVERITY_COLOR } from '../../../../overview/components/detection_response/utils'; +import { + INSIGHTS_ALERTS_COUNT_INVESTIGATE_IN_TIMELINE_BUTTON_TEST_ID, + INSIGHTS_ALERTS_COUNT_TEXT_TEST_ID, +} from './test_ids'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; +import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; jest.mock('../../../../common/lib/kibana'); +jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_index'); +jest.mock('../../../../common/components/user_privileges'); jest.mock('react-router-dom', () => { const actual = jest.requireActual('react-router-dom'); @@ -66,15 +74,42 @@ const renderAlertCountInsight = () => { }; describe('AlertCountInsight', () => { + beforeEach(() => { + (useSignalIndex as jest.Mock).mockReturnValue({ signalIndexName: '' }); + (useUserPrivileges as jest.Mock).mockReturnValue({ timelinePrivileges: { read: true } }); + }); + it('renders', () => { (useAlertsByStatus as jest.Mock).mockReturnValue({ isLoading: false, items: mockAlertData, }); - const { getByTestId } = renderAlertCountInsight(); + + const { getByTestId, queryByTestId } = renderAlertCountInsight(); + expect(getByTestId(testId)).toBeInTheDocument(); expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument(); expect(getByTestId(`${testId}-count`)).toHaveTextContent('8'); + expect( + getByTestId(INSIGHTS_ALERTS_COUNT_INVESTIGATE_IN_TIMELINE_BUTTON_TEST_ID) + ).toBeInTheDocument(); + expect(queryByTestId(INSIGHTS_ALERTS_COUNT_TEXT_TEST_ID)).not.toBeInTheDocument(); + }); + + it('renders the count as text instead of button', () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ timelinePrivileges: { read: false } }); + (useAlertsByStatus as jest.Mock).mockReturnValue({ + isLoading: false, + items: mockAlertData, + }); + + const { getByTestId, queryByTestId } = renderAlertCountInsight(); + + expect(getByTestId(`${testId}-count`)).toHaveTextContent('8'); + expect(getByTestId(INSIGHTS_ALERTS_COUNT_TEXT_TEST_ID)).toBeInTheDocument(); + expect( + queryByTestId(INSIGHTS_ALERTS_COUNT_INVESTIGATE_IN_TIMELINE_BUTTON_TEST_ID) + ).not.toBeInTheDocument(); }); it('renders loading spinner if data is being fetched', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.tsx index 9b5b056311354..77a06132e463c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.tsx @@ -7,7 +7,7 @@ import React, { useMemo } from 'react'; import { capitalize } from 'lodash'; -import { EuiLoadingSpinner, EuiFlexItem, type EuiFlexGroupProps } from '@elastic/eui'; +import { EuiLoadingSpinner, EuiFlexItem, EuiText, type EuiFlexGroupProps } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { InsightDistributionBar } from './insight_distribution_bar'; import { getSeverityColor } from '../../../../detections/components/alerts_kpis/severity_level_panel/helpers'; @@ -26,6 +26,11 @@ import type { AlertsByStatus, ParsedAlertsData, } from '../../../../overview/components/detection_response/alerts_by_status/types'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; +import { + INSIGHTS_ALERTS_COUNT_INVESTIGATE_IN_TIMELINE_BUTTON_TEST_ID, + INSIGHTS_ALERTS_COUNT_TEXT_TEST_ID, +} from './test_ids'; interface AlertCountInsightProps { /** @@ -82,6 +87,10 @@ export const AlertCountInsight: React.FC = ({ direction, 'data-test-subj': dataTestSubj, }) => { + const { + timelinePrivileges: { read: canUseTimeline }, + } = useUserPrivileges(); + const entityFilter = useMemo(() => ({ field: fieldName, value: name }), [fieldName, name]); const { to, from } = useGlobalTime(); const { signalIndexName } = useSignalIndex(); @@ -119,6 +128,29 @@ export const AlertCountInsight: React.FC = ({ [fieldName, name] ); + // renders either a button to open timeline or just plain text depending on the user's timeline privileges + const alertCount = useMemo(() => { + const formattedAlertCount = ; + + if (!canUseTimeline) { + return ( + + {formattedAlertCount} + + ); + } + return ( + + {formattedAlertCount} + + ); + }, [canUseTimeline, dataProviders, totalAlertCount]); + if (!isLoading && totalAlertCount === 0) return null; return ( @@ -134,17 +166,7 @@ export const AlertCountInsight: React.FC = ({ /> } stats={alertStats} - count={ -
- - - -
- } + count={
{alertCount}
} direction={direction} data-test-subj={`${dataTestSubj}-distribution-bar`} /> diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/take_action_dropdown.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/take_action_dropdown.test.tsx index ffed3e064d7f5..d862c6c09e62b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/take_action_dropdown.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/take_action_dropdown.test.tsx @@ -108,7 +108,7 @@ describe('take action dropdown', () => { isOsqueryAvailable: jest.fn().mockReturnValue(true), }, application: { - capabilities: { siem: { crud_alerts: true, read_alerts: true }, osquery: true }, + capabilities: { siemV2: { crud_alerts: true, read_alerts: true }, osquery: true }, }, }, }; @@ -147,6 +147,10 @@ describe('take action dropdown', () => { let wrapper: ReactWrapper; beforeAll(() => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + ...getUserPrivilegesMockDefaultValue(), + timelinePrivileges: { read: true }, + }); wrapper = mount( @@ -246,6 +250,29 @@ describe('take action dropdown', () => { }); }); + describe('privileges', () => { + test('should not render "Investigate in timeline" when the user does not have timeline privileges', async () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + ...getUserPrivilegesMockDefaultValue(), + timelinePrivileges: { read: false }, + }); + const wrapper = mount( + + + + ); + wrapper + .find(`button[data-test-subj="${FLYOUT_FOOTER_DROPDOWN_BUTTON_TEST_ID}"]`) + .simulate('click'); + + await waitFor(() => { + expect( + wrapper.exists('[data-test-subj="investigate-in-timeline-action-item"]') + ).toBeFalsy(); + }); + }); + }); + describe('for Endpoint related actions', () => { /** Removes the detail data that is used to determine if data is for an Alert */ const setAlertDetailsDataMockToEvent = () => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/test_ids.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/test_ids.ts index 5895386d43dc1..0cc556ca4d01c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/test_ids.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/components/test_ids.ts @@ -12,7 +12,9 @@ export const FLYOUT_PREVIEW_LINK_TEST_ID = `${PREFIX}PreviewLink` as const; export const SESSION_VIEW_UPSELL_TEST_ID = `${PREFIX}SessionViewUpsell` as const; export const SESSION_VIEW_NO_DATA_TEST_ID = `${PREFIX}SessionViewNoData` as const; -export const MISCONFIGURATIONS_TEST_ID = `${PREFIX}Misconfigurations` as const; -export const VULNERABILITIES_TEST_ID = `${PREFIX}Vulnerabilities` as const; +const INSIGHTS_TEST_ID = `${PREFIX}Insights` as const; +export const INSIGHTS_ALERTS_COUNT_TEXT_TEST_ID = `${INSIGHTS_TEST_ID}AlertsCount` as const; +export const INSIGHTS_ALERTS_COUNT_INVESTIGATE_IN_TIMELINE_BUTTON_TEST_ID = + `${INSIGHTS_TEST_ID}AlertsCountInvestigateInTimelineButton` as const; export const FLYOUT_FOOTER_DROPDOWN_BUTTON_TEST_ID = `${PREFIX}FooterDropdownButton` as const; diff --git a/x-pack/solutions/security/plugins/security_solution/public/helper_hooks.tsx b/x-pack/solutions/security/plugins/security_solution/public/helper_hooks.tsx index 90d15643aec8b..c22513a989a06 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/helper_hooks.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/helper_hooks.tsx @@ -27,5 +27,5 @@ export const useOnOpenCloseHandler = (): [boolean, () => void, () => void] => { */ export const useHasSecurityCapability = (capability: string): boolean => { const { capabilities } = useKibana().services.application; - return !!capabilities.siem[capability]; + return !!capabilities.siemV2[capability]; }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/helpers.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/helpers.test.tsx index 04eaad34f14ce..ab21a78c8deff 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/helpers.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/helpers.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import type { Capabilities } from '@kbn/core/public'; -import { CASES_FEATURE_ID, SERVER_APP_ID } from '../common/constants'; +import { CASES_FEATURE_ID, SECURITY_FEATURE_ID } from '../common/constants'; import { mockEcsDataWithAlert } from './common/mock'; import { ALERT_RULE_UUID, ALERT_RULE_NAME, ALERT_RULE_PARAMETERS } from '@kbn/rule-data-utils'; import { @@ -87,7 +87,7 @@ describe('#getSubPluginRoutesByCapabilities', () => { const routes = getSubPluginRoutesByCapabilities( mockSubPlugins, set(mockServices, 'application.capabilities', { - [SERVER_APP_ID]: { show: true, crud: false }, + [SECURITY_FEATURE_ID]: { show: true, crud: false }, [CASES_FEATURE_ID]: noCasesCapabilities(), }) ); @@ -107,7 +107,7 @@ describe('#getSubPluginRoutesByCapabilities', () => { const routes = getSubPluginRoutesByCapabilities( mockSubPlugins, set(mockServices, 'application.capabilities', { - [SERVER_APP_ID]: { show: false, crud: false }, + [SECURITY_FEATURE_ID]: { show: false, crud: false }, [CASES_FEATURE_ID]: readCasesCapabilities(), }) ); @@ -126,7 +126,7 @@ describe('#getSubPluginRoutesByCapabilities', () => { const routes = getSubPluginRoutesByCapabilities( mockSubPlugins, set(mockServices, 'application.capabilities', { - [SERVER_APP_ID]: { show: false, crud: false }, + [SECURITY_FEATURE_ID]: { show: false, crud: false }, [CASES_FEATURE_ID]: noCasesCapabilities(), }) ); @@ -157,7 +157,7 @@ describe('#isSubPluginAvailable', () => { it('plugin outsides of cases should be available if siem privilege is all and independently of cases privileges', () => { expect( isSubPluginAvailable('pluginKey', { - [SERVER_APP_ID]: { show: true, crud: true }, + [SECURITY_FEATURE_ID]: { show: true, crud: true }, [CASES_FEATURE_ID]: noCasesCapabilities(), } as unknown as Capabilities) ).toBeTruthy(); @@ -166,7 +166,7 @@ describe('#isSubPluginAvailable', () => { it('plugin outsides of cases should be available if siem privilege is read and independently of cases privileges', () => { expect( isSubPluginAvailable('pluginKey', { - [SERVER_APP_ID]: { show: true, crud: false }, + [SECURITY_FEATURE_ID]: { show: true, crud: false }, [CASES_FEATURE_ID]: noCasesCapabilities(), } as unknown as Capabilities) ).toBeTruthy(); @@ -175,7 +175,7 @@ describe('#isSubPluginAvailable', () => { it('plugin outsides of cases should NOT be available if siem privilege is none and independently of cases privileges', () => { expect( isSubPluginAvailable('pluginKey', { - [SERVER_APP_ID]: { show: false, crud: false }, + [SECURITY_FEATURE_ID]: { show: false, crud: false }, [CASES_FEATURE_ID]: noCasesCapabilities(), } as unknown as Capabilities) ).toBeFalsy(); @@ -184,7 +184,7 @@ describe('#isSubPluginAvailable', () => { it('cases plugin should be available if cases privilege is all and independently of siem privileges', () => { expect( isSubPluginAvailable('cases', { - [SERVER_APP_ID]: { show: false, crud: false }, + [SECURITY_FEATURE_ID]: { show: false, crud: false }, [CASES_FEATURE_ID]: allCasesCapabilities(), } as unknown as Capabilities) ).toBeTruthy(); @@ -193,7 +193,7 @@ describe('#isSubPluginAvailable', () => { it('cases plugin should be available if cases privilege is read and independently of siem privileges', () => { expect( isSubPluginAvailable('cases', { - [SERVER_APP_ID]: { show: false, crud: false }, + [SECURITY_FEATURE_ID]: { show: false, crud: false }, [CASES_FEATURE_ID]: readCasesCapabilities(), } as unknown as Capabilities) ).toBeTruthy(); @@ -202,7 +202,7 @@ describe('#isSubPluginAvailable', () => { it('cases plugin should NOT be available if cases privilege is none independently of siem privileges', () => { expect( isSubPluginAvailable('pluginKey', { - [SERVER_APP_ID]: { show: false, crud: false }, + [SECURITY_FEATURE_ID]: { show: false, crud: false }, [CASES_FEATURE_ID]: noCasesCapabilities(), } as unknown as Capabilities) ).toBeFalsy(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/helpers.tsx b/x-pack/solutions/security/plugins/security_solution/public/helpers.tsx index ffe7b2dd9668d..5390f8e45f868 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/helpers.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/helpers.tsx @@ -24,7 +24,6 @@ import { DASHBOARDS_PATH, EXCEPTIONS_PATH, RULES_PATH, - SERVER_APP_ID, THREAT_INTELLIGENCE_PATH, } from '../common/constants'; import type { @@ -39,6 +38,7 @@ import { CASES_SUB_PLUGIN_KEY } from './types'; import { timelineActions } from './timelines/store'; import { TimelineId } from '../common/types'; import { SourcererScopeName } from './sourcerer/store/model'; +import { hasAccessToSecuritySolution } from './helpers_access'; export const parseRoute = (location: Pick) => { if (!isEmpty(location.hash)) { @@ -234,7 +234,7 @@ export const isSubPluginAvailable = (pluginKey: string, capabilities: Capabiliti if (CASES_SUB_PLUGIN_KEY === pluginKey) { return capabilities[CASES_FEATURE_ID].read_cases === true; } - return capabilities[SERVER_APP_ID].show === true; + return hasAccessToSecuritySolution(capabilities); }; const siemSignalsFieldMappings: Record = { diff --git a/x-pack/solutions/security/plugins/security_solution/public/helpers_access.test.ts b/x-pack/solutions/security/plugins/security_solution/public/helpers_access.test.ts new file mode 100644 index 0000000000000..245d8d0686eb8 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/helpers_access.test.ts @@ -0,0 +1,46 @@ +/* + * 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 { Capabilities } from '@kbn/core/public'; +import { SECURITY_FEATURE_ID } from '../common/constants'; +import { hasAccessToSecuritySolution } from './helpers_access'; + +const baseCapabilities: Capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, +}; + +describe('access helpers', () => { + describe('hasAccessToSecuritySolution', () => { + it('should return true for users with correct capabilities', () => { + expect( + hasAccessToSecuritySolution({ + ...baseCapabilities, + [SECURITY_FEATURE_ID]: { show: true }, + }) + ).toBe(true); + }); + + it('should return false for users with incorrect capabilities', () => { + expect( + hasAccessToSecuritySolution({ + ...baseCapabilities, + [SECURITY_FEATURE_ID]: { show: false }, + }) + ).toBe(false); + + expect( + hasAccessToSecuritySolution({ + ...baseCapabilities, + [SECURITY_FEATURE_ID]: {}, + }) + ).toBe(false); + + expect(hasAccessToSecuritySolution(baseCapabilities)).toBe(false); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/helpers_access.ts b/x-pack/solutions/security/plugins/security_solution/public/helpers_access.ts new file mode 100644 index 0000000000000..bcf54354a459b --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/helpers_access.ts @@ -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 type { Capabilities } from '@kbn/core/public'; +import { SECURITY_FEATURE_ID } from '../common/constants'; + +export function hasAccessToSecuritySolution(capabilities: Capabilities) { + return ( + // Using `siemV2` + capabilities[SECURITY_FEATURE_ID]?.show === true + ); +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/artifacts/artifact_tabs_in_policy_details.cy.ts b/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/artifacts/artifact_tabs_in_policy_details.cy.ts index 4aa8085fbf571..b6232dc052e3b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/artifacts/artifact_tabs_in_policy_details.cy.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/artifacts/artifact_tabs_in_policy_details.cy.ts @@ -45,7 +45,7 @@ const getRoleWithoutArtifactPrivilege = (privilegePrefix: string) => { ...endpointSecurityPolicyManagerRole.kibana[0], feature: { ...endpointSecurityPolicyManagerRole.kibana[0].feature, - siem: endpointSecurityPolicyManagerRole.kibana[0].feature.siem.filter( + siemV2: endpointSecurityPolicyManagerRole.kibana[0].feature.siemV2.filter( (privilege) => privilege !== `${privilegePrefix}all` ), }, diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/endpoint_list/endpoints_rbac_mocked_data.cy.ts b/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/endpoint_list/endpoints_rbac_mocked_data.cy.ts index 62786effd8303..42874e420138c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/endpoint_list/endpoints_rbac_mocked_data.cy.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/endpoint_list/endpoints_rbac_mocked_data.cy.ts @@ -36,8 +36,8 @@ describe('Endpoints RBAC', { tags: ['@ess'] }, () => { ...base.kibana[0], feature: { ...base.kibana[0].feature, - siem: [ - ...base.kibana[0].feature.siem, + siemV2: [ + ...base.kibana[0].feature.siemV2, `endpoint_list_all`, `policy_management_${endpointPolicyManagementPrivilege}`, ], diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/rbac/endpoint_role_rbac.cy.ts b/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/rbac/endpoint_role_rbac.cy.ts index b97b37191a951..715cde443e6e3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/rbac/endpoint_role_rbac.cy.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/rbac/endpoint_role_rbac.cy.ts @@ -31,7 +31,7 @@ describe.skip( () => { const getAllSubFeatureRows = (): Cypress.Chainable> => { return cy - .get('#featurePrivilegeControls_siem') + .get('#featurePrivilegeControls_siemV2') .findByTestSubj('mutexSubFeaturePrivilegeControl') .closest('.euiFlexGroup'); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/rbac/endpoint_role_rbac_with_space_awareness.cy.ts b/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/rbac/endpoint_role_rbac_with_space_awareness.cy.ts index 41f6613be88be..93fbc7ef76f27 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/rbac/endpoint_role_rbac_with_space_awareness.cy.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/cypress/e2e/rbac/endpoint_role_rbac_with_space_awareness.cy.ts @@ -88,7 +88,7 @@ describe( .findByTestSubj(`space-avatar-${spaceId}`) .should('exist'); - cy.get('#row_siem_expansion') + cy.get('#row_siemV2_expansion') .findByTestSubj('subFeatureEntry') .then(($element) => { const features: string[] = []; @@ -119,7 +119,7 @@ describe( it('should not display the privilege tooltip', () => { ENDPOINT_SUB_FEATURE_PRIVILEGE_IDS.forEach((subFeaturePrivilegeId) => { - cy.getByTestSubj(`securitySolution_siem_${subFeaturePrivilegeId}_nameTooltip`).should( + cy.getByTestSubj(`securitySolution_siemV2_${subFeaturePrivilegeId}_nameTooltip`).should( 'not.exist' ); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/cypress/fixtures/role_with_artifact_read_privilege.ts b/x-pack/solutions/security/plugins/security_solution/public/management/cypress/fixtures/role_with_artifact_read_privilege.ts index 247b491f04632..25bc5d2da1f8b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/cypress/fixtures/role_with_artifact_read_privilege.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/cypress/fixtures/role_with_artifact_read_privilege.ts @@ -17,8 +17,8 @@ export const getRoleWithArtifactReadPrivilege = (privilegePrefix: string) => { ...endpointSecurityPolicyManagerRole.kibana[0], feature: { ...endpointSecurityPolicyManagerRole.kibana[0].feature, - siem: [ - ...endpointSecurityPolicyManagerRole.kibana[0].feature.siem.filter( + siemV2: [ + ...endpointSecurityPolicyManagerRole.kibana[0].feature.siemV2.filter( (privilege) => privilege !== `${privilegePrefix}all` ), `${privilegePrefix}read`, diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/cypress/screens/stack_management/role_page.ts b/x-pack/solutions/security/plugins/security_solution/public/management/cypress/screens/stack_management/role_page.ts index a3e7bbc7e4e89..1b6d7e6c92548 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/cypress/screens/stack_management/role_page.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/cypress/screens/stack_management/role_page.ts @@ -65,11 +65,13 @@ export const getSecuritySolutionCategoryKibanaPrivileges = (): Cypress.Chainable * kibana feature privileges grouping. This is the area where Endpoint related RBAC is managed */ export const expandEndpointSecurityFeaturePrivileges = (): Cypress.Chainable => { - return cy.getByTestSubj('featurePrivilegeControls_securitySolution_siem_accordionToggle').click(); + return cy + .getByTestSubj('featurePrivilegeControls_securitySolution_siemV2_accordionToggle') + .click(); }; export const getEndpointSecurityFeaturePrivileges = () => { - return cy.getByTestSubj('featureCategory_securitySolution_siem'); + return cy.getByTestSubj('featureCategory_securitySolution_siemV2'); }; /** @@ -101,7 +103,9 @@ export const setKibanaPrivilegeSpace = (spaceId: string) => { export const setSecuritySolutionEndpointGroupPrivilege = ( privilege: 'all' | 'read' | 'none' ): Cypress.Chainable> => { - return getSecuritySolutionCategoryKibanaPrivileges().findByTestSubj(`siem_${privilege}`).click(); + return getSecuritySolutionCategoryKibanaPrivileges() + .findByTestSubj(`siemV2_${privilege}`) + .click(); }; /** @@ -144,7 +148,7 @@ export const setEndpointSubFeaturePrivilege = ( privilege: 'all' | 'read' | 'none' ): Cypress.Chainable> => { return getEndpointSecurityFeaturePrivileges() - .findByTestSubj(`securitySolution_siem_${feature}_privilegeGroup`) + .findByTestSubj(`securitySolution_siemV2_${feature}_privilegeGroup`) .find(`button[title="${privilegeMapToTitle[privilege]}"]`) .click(); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/links.ts b/x-pack/solutions/security/plugins/security_solution/public/management/links.ts index 909395df8df4f..fc9f8967859ca 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/links.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/links.ts @@ -24,7 +24,7 @@ import { POLICIES_PATH, RESPONSE_ACTIONS_HISTORY_PATH, SecurityPageName, - SERVER_APP_ID, + SECURITY_FEATURE_ID, TRUSTED_APPS_PATH, } from '../../common/constants'; import { @@ -100,7 +100,7 @@ export const links: LinkItem = { skipUrlState: true, hideTimeline: true, globalNavPosition: 11, - capabilities: [`${SERVER_APP_ID}.show`], + capabilities: [`${SECURITY_FEATURE_ID}.show`], globalSearchKeywords: [ i18n.translate('xpack.securitySolution.appLinks.manage', { defaultMessage: 'Manage', @@ -189,7 +189,7 @@ export const links: LinkItem = { path: ENTITY_ANALYTICS_MANAGEMENT_PATH, skipUrlState: true, hideTimeline: true, - capabilities: [`${SERVER_APP_ID}.entity-analytics`], + capabilities: [`${SECURITY_FEATURE_ID}.entity-analytics`], experimentalKey: 'riskScoringRoutesEnabled', licenseType: 'platinum', }, @@ -203,7 +203,7 @@ export const links: LinkItem = { path: ENTITY_ANALYTICS_ENTITY_STORE_MANAGEMENT_PATH, skipUrlState: true, hideTimeline: true, - capabilities: [`${SERVER_APP_ID}.entity-analytics`], + capabilities: [`${SECURITY_FEATURE_ID}.entity-analytics`], }, { id: SecurityPageName.responseActionsHistory, diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/mocks.tsx b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/mocks.tsx index 945a841c132fd..3a480b47c9d08 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/mocks.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/mocks.tsx @@ -96,7 +96,7 @@ export const createFleetContextRendererMock = (): AppContextTestRender => { startServices.application.capabilities = deepFreeze({ ...startServices.application.capabilities, - siem: { show: true, crud: true }, + siemV2: { show: true, crud: true }, }); return ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/notes/components/notes_list.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/notes/components/notes_list.test.tsx index d32b508d03037..56ca7a8e833a5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/notes/components/notes_list.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/notes/components/notes_list.test.tsx @@ -44,7 +44,8 @@ describe('NotesList', () => { (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false); useUserPrivilegesMock.mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, + timelinePrivileges: { read: true }, + notesPrivileges: { crud: true, read: true }, }); }); @@ -103,7 +104,8 @@ describe('NotesList', () => { it('should not render a delete icon when the user does not have crud privileges', () => { useUserPrivilegesMock.mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: false, read: true }, + timelinePrivileges: { read: true }, + notesPrivileges: { crud: false, read: true }, }); const { queryByTestId } = render( diff --git a/x-pack/solutions/security/plugins/security_solution/public/notes/components/notes_list.tsx b/x-pack/solutions/security/plugins/security_solution/public/notes/components/notes_list.tsx index 344935413731e..56b3579797aef 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/notes/components/notes_list.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/notes/components/notes_list.tsx @@ -59,8 +59,8 @@ export interface NotesListProps { * When a note is being created, the component renders a loading spinner when the new note is about to be added. */ export const NotesList = memo(({ notes, options }: NotesListProps) => { - const { kibanaSecuritySolutionsPrivileges } = useUserPrivileges(); - const canDeleteNotes = kibanaSecuritySolutionsPrivileges.crud; + const { notesPrivileges } = useUserPrivileges(); + const canDeleteNotes = notesPrivileges.crud; const createStatus = useSelector((state: State) => selectCreateNoteStatus(state)); diff --git a/x-pack/solutions/security/plugins/security_solution/public/notes/components/open_timeline_button.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/notes/components/open_timeline_button.test.tsx index c6a2629494758..108ee6f6886a9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/notes/components/open_timeline_button.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/notes/components/open_timeline_button.test.tsx @@ -11,9 +11,11 @@ import { OpenTimelineButtonIcon } from './open_timeline_button'; import type { Note } from '../../../common/api/timeline'; import { OPEN_TIMELINE_BUTTON_TEST_ID } from './test_ids'; import { useQueryTimelineById } from '../../timelines/components/open_timeline/helpers'; +import { useUserPrivileges } from '../../common/components/user_privileges'; jest.mock('../../common/hooks/use_experimental_features'); jest.mock('../../timelines/components/open_timeline/helpers'); +jest.mock('../../common/components/user_privileges'); const note: Note = { eventId: '1', @@ -29,12 +31,25 @@ const note: Note = { const index = 0; describe('OpenTimelineButtonIcon', () => { + beforeEach(() => { + (useUserPrivileges as jest.Mock).mockReturnValue({ timelinePrivileges: { read: true } }); + }); + it('should render the timeline icon', () => { const { getByTestId } = render(); expect(getByTestId(`${OPEN_TIMELINE_BUTTON_TEST_ID}-${index}`)).toBeInTheDocument(); }); + it('should disable the button if the user does not have the correct privileges', () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ timelinePrivileges: { read: false } }); + (useQueryTimelineById as jest.Mock).mockReturnValue(jest.fn()); + + const { getByTestId } = render(); + + expect(getByTestId(`${OPEN_TIMELINE_BUTTON_TEST_ID}-${index}`)).toHaveAttribute('disabled'); + }); + it('should call openTimeline with the correct values', () => { const openTimeline = jest.fn(); (useQueryTimelineById as jest.Mock).mockReturnValue(openTimeline); diff --git a/x-pack/solutions/security/plugins/security_solution/public/notes/components/open_timeline_button.tsx b/x-pack/solutions/security/plugins/security_solution/public/notes/components/open_timeline_button.tsx index 91f3a722ebeeb..81a1cf2a05e7f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/notes/components/open_timeline_button.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/notes/components/open_timeline_button.tsx @@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n'; import { useQueryTimelineById } from '../../timelines/components/open_timeline/helpers'; import { OPEN_TIMELINE_BUTTON_TEST_ID } from './test_ids'; import type { Note } from '../../../common/api/timeline'; +import { useUserPrivileges } from '../../common/components/user_privileges'; const OPEN_TIMELINE = i18n.translate('xpack.securitySolution.notes.management.openTimelineButton', { defaultMessage: 'Open saved timeline', @@ -31,6 +32,10 @@ export interface OpenTimelineButtonIconProps { * Renders a button to open the timeline associated with a note */ export const OpenTimelineButtonIcon = memo(({ note, index }: OpenTimelineButtonIconProps) => { + const { + timelinePrivileges: { read: canReadTimelines }, + } = useUserPrivileges(); + const queryTimelineById = useQueryTimelineById(); const openTimeline = useCallback( ({ timelineId }: { timelineId: string }) => @@ -51,6 +56,7 @@ export const OpenTimelineButtonIcon = memo(({ note, index }: OpenTimelineButtonI color="text" iconType="timelineWithArrow" onClick={() => openTimeline(note)} + disabled={!canReadTimelines} /> ); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/notes/hooks/use_fetch_notes.test.ts b/x-pack/solutions/security/plugins/security_solution/public/notes/hooks/use_fetch_notes.test.ts index b7fa3ab3fc519..f95a262b506a3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/notes/hooks/use_fetch_notes.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/notes/hooks/use_fetch_notes.test.ts @@ -10,6 +10,7 @@ import { useDispatch } from 'react-redux'; import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; import { fetchNotesByDocumentIds } from '../store/notes.slice'; import { useFetchNotes } from './use_fetch_notes'; +import { useUserPrivileges } from '../../common/components/user_privileges'; jest.mock('react-redux', () => ({ useDispatch: jest.fn(), @@ -23,6 +24,8 @@ jest.mock('../store/notes.slice', () => ({ fetchNotesByDocumentIds: jest.fn(), })); +jest.mock('../../common/components/user_privileges'); + const mockedUseDispatch = useDispatch as jest.MockedFunction; const mockedUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.MockedFunction; @@ -33,6 +36,9 @@ describe('useFetchNotes', () => { beforeEach(() => { mockDispatch = jest.fn(); mockedUseDispatch.mockReturnValue(mockDispatch); + (useUserPrivileges as jest.Mock).mockReturnValue({ + notesPrivileges: { read: true }, + }); }); afterEach(() => { @@ -61,6 +67,19 @@ describe('useFetchNotes', () => { expect(mockDispatch).not.toHaveBeenCalled(); }); + it('should not dispatch action when user has insufficient privileges', () => { + mockedUseIsExperimentalFeatureEnabled.mockReturnValue(false); + (useUserPrivileges as jest.Mock).mockReturnValue({ + notesPrivileges: { read: false }, + }); + const { result } = renderHook(() => useFetchNotes()); + + const events = [{ _id: '1' }, { _id: '2' }, { _id: '3' }]; + result.current.onLoad(events); + + expect(mockDispatch).not.toHaveBeenCalled(); + }); + it('should dispatch fetchNotesByDocumentIds with correct ids when conditions are met', () => { mockedUseIsExperimentalFeatureEnabled.mockReturnValue(false); const { result } = renderHook(() => useFetchNotes()); diff --git a/x-pack/solutions/security/plugins/security_solution/public/notes/hooks/use_fetch_notes.ts b/x-pack/solutions/security/plugins/security_solution/public/notes/hooks/use_fetch_notes.ts index cdfa8b7600dfd..00f3740cb8b44 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/notes/hooks/use_fetch_notes.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/notes/hooks/use_fetch_notes.ts @@ -8,6 +8,7 @@ import { useCallback } from 'react'; import { useDispatch } from 'react-redux'; import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; +import { useUserPrivileges } from '../../common/components/user_privileges'; import { fetchNotesByDocumentIds } from '../store/notes.slice'; export interface UseFetchNotesResult { @@ -25,16 +26,19 @@ export const useFetchNotes = (): UseFetchNotesResult => { const securitySolutionNotesDisabled = useIsExperimentalFeatureEnabled( 'securitySolutionNotesDisabled' ); + const { + notesPrivileges: { read: canReadNotes }, + } = useUserPrivileges(); const onLoad = useCallback( (events: Array>) => { - if (securitySolutionNotesDisabled || events.length === 0) return; + if (!canReadNotes || securitySolutionNotesDisabled || events.length === 0) return; const eventIds: string[] = events .map((event) => event._id) .filter((id) => id != null) as string[]; dispatch(fetchNotesByDocumentIds({ documentIds: eventIds })); }, - [dispatch, securitySolutionNotesDisabled] + [dispatch, securitySolutionNotesDisabled, canReadNotes] ); return { onLoad }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/notes/links.ts b/x-pack/solutions/security/plugins/security_solution/public/notes/links.ts index 628904ae30c41..94c1233dae1f1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/notes/links.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/notes/links.ts @@ -6,7 +6,12 @@ */ import { i18n } from '@kbn/i18n'; -import { NOTES_PATH, SecurityPageName, SERVER_APP_ID } from '../../common/constants'; +import { + NOTES_PATH, + SECURITY_FEATURE_ID, + SecurityPageName, + NOTES_FEATURE_ID, +} from '../../common/constants'; import { NOTES } from '../app/translations'; import type { LinkItem } from '../common/links/types'; @@ -18,7 +23,8 @@ export const links: LinkItem = { defaultMessage: 'Oversee, revise, and revisit the notes attached to alerts, events and Timelines.', }), - capabilities: [`${SERVER_APP_ID}.show`], + // It only makes sense to show this link when the user is also granted access to security solution + capabilities: [[`${SECURITY_FEATURE_ID}.show`, `${NOTES_FEATURE_ID}.read`]], landingIcon: 'filebeatApp', skipUrlState: true, hideTimeline: true, diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/links.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/links.ts index ea9e603a36fac..05519b12b06ff 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/links.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/links.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ONBOARDING_PATH, SecurityPageName, SERVER_APP_ID } from '../../common/constants'; +import { ONBOARDING_PATH, SecurityPageName, SECURITY_FEATURE_ID } from '../../common/constants'; import { GETTING_STARTED } from '../app/translations'; import type { LinkItem } from '../common/links/types'; @@ -14,7 +14,7 @@ export const onboardingLinks: LinkItem = { id: SecurityPageName.landing, title: GETTING_STARTED, path: ONBOARDING_PATH, - capabilities: [`${SERVER_APP_ID}.show`], + capabilities: [`${SECURITY_FEATURE_ID}.show`], globalSearchKeywords: [ i18n.translate('xpack.securitySolution.appLinks.getStarted', { defaultMessage: 'Getting started', diff --git a/x-pack/solutions/security/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.test.tsx index 40a9fab612ad6..d3db4c743b53d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.test.tsx @@ -14,7 +14,9 @@ import { CASES_FEATURE_ID } from '../../../../../common/constants'; import { TestProviders } from '../../../../common/mock/test_providers'; import { useAlertsByStatus } from './use_alerts_by_status'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; +jest.mock('../../../../common/components/user_privileges'); jest.mock('../../../../common/lib/kibana/kibana_react'); jest.mock('../../../../common/hooks/use_experimental_features', () => { return { useIsExperimentalFeatureEnabled: jest.fn() }; @@ -63,6 +65,9 @@ describe('AlertsByStatus', () => { isLoading: true, }); (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false); + (useUserPrivileges as jest.Mock).mockReturnValue({ + timelinePrivileges: { read: true }, + }); }); test('render HoverVisibilityContainer', () => { @@ -108,6 +113,20 @@ describe('AlertsByStatus', () => { expect(getByTestId('view-details-button')).toHaveTextContent('Investigate in Timeline'); }); + test('shows correct names when entity filter IS provided AND user does not have timeline privileges', () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + timelinePrivileges: {}, + }); + const { getByText, getByTestId } = render( + + + + ); + + expect(getByText('Alerts by Severity')).toBeInTheDocument(); + expect(getByTestId('view-details-button')).toHaveTextContent('View alerts'); + }); + test('render HeaderSection', () => { const { container } = render( diff --git a/x-pack/solutions/security/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.tsx b/x-pack/solutions/security/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.tsx index 60dcceb894119..7f2dd03ad8079 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.tsx @@ -51,6 +51,7 @@ import { useQueryToggle } from '../../../../common/containers/query_toggle'; import { VIEW_ALERTS } from '../../../pages/translations'; import { SEVERITY_COLOR } from '../utils'; import { FormattedCount } from '../../../../common/components/formatted_number'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; import { ChartLabel } from './chart_label'; import { Legend } from '../../../../common/components/charts/legend'; import { emptyDonutColor } from '../../../../common/components/charts/donutchart_empty'; @@ -109,6 +110,9 @@ export const AlertsByStatus = ({ const { toggleStatus, setToggleStatus } = useQueryToggle(DETECTION_RESPONSE_ALERTS_BY_STATUS_ID); const { openTimelineWithFilters } = useNavigateToTimeline(); const navigateToAlerts = useNavigateToAlertsPageWithFilters(); + const { + timelinePrivileges: { read: canAccessTimelines }, + } = useUserPrivileges(); const { onClick: goToAlerts, href } = useGetSecuritySolutionLinkProps()({ deepLinkId: SecurityPageName.alerts, }); @@ -125,15 +129,16 @@ export const AlertsByStatus = ({ const detailsButtonOptions = useMemo( () => ({ - name: entityFilter ? INVESTIGATE_IN_TIMELINE : VIEW_ALERTS, - href: entityFilter ? undefined : href, - onClick: entityFilter - ? async () => { - await openTimelineWithFilters([[entityFilter, eventKindSignalFilter]]); - } - : goToAlerts, + name: canAccessTimelines && entityFilter ? INVESTIGATE_IN_TIMELINE : VIEW_ALERTS, + href: canAccessTimelines && entityFilter ? undefined : href, + onClick: + canAccessTimelines && entityFilter + ? async () => { + await openTimelineWithFilters([[entityFilter, eventKindSignalFilter]]); + } + : goToAlerts, }), - [entityFilter, href, goToAlerts, openTimelineWithFilters] + [entityFilter, href, goToAlerts, openTimelineWithFilters, canAccessTimelines] ); const { diff --git a/x-pack/solutions/security/plugins/security_solution/public/overview/components/sidebar/sidebar.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/overview/components/sidebar/sidebar.test.tsx index 8835fc0e48f27..dfc376e986ec5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/overview/components/sidebar/sidebar.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/overview/components/sidebar/sidebar.test.tsx @@ -6,16 +6,17 @@ */ import React from 'react'; -import { mount } from 'enzyme'; -import { waitFor } from '@testing-library/react'; +import { screen, render, waitFor } from '@testing-library/react'; import { TestProviders } from '../../../common/mock'; import { Sidebar } from './sidebar'; import { useKibana } from '../../../common/lib/kibana'; import type { CaseUiClientMock } from '@kbn/cases-plugin/public/mocks'; import { casesPluginMock } from '@kbn/cases-plugin/public/mocks'; import { noCasesPermissions, readCasesPermissions } from '../../../cases_test_utils'; +import { useUserPrivileges } from '../../../common/components/user_privileges'; jest.mock('../../../common/lib/kibana'); +jest.mock('../../../common/components/user_privileges'); const useKibanaMock = useKibana as jest.MockedFunction; @@ -35,33 +36,64 @@ describe('Sidebar', () => { }, }, } as unknown as ReturnType); + (useUserPrivileges as jest.Mock).mockReturnValue({ + timelinePrivileges: { read: true }, + }); }); it('does not render the recently created cases section when the user does not have read permissions', async () => { casesMock.helpers.canUseCases.mockReturnValue(noCasesPermissions()); - await waitFor(() => - mount( - - {}} /> - - ) + render( + + {}} /> + ); - expect(casesMock.ui.getRecentCases).not.toHaveBeenCalled(); + await waitFor(() => { + expect(casesMock.ui.getRecentCases).not.toHaveBeenCalled(); + }); }); it('does render the recently created cases section when the user has read permissions', async () => { casesMock.helpers.canUseCases.mockReturnValue(readCasesPermissions()); - await waitFor(() => - mount( - - {}} /> - - ) + render( + + {}} /> + ); - expect(casesMock.ui.getRecentCases).toHaveBeenCalled(); + await waitFor(() => { + expect(casesMock.ui.getRecentCases).toHaveBeenCalled(); + }); + }); + + it('does not render recent timelines for users with insufficient privileges', () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + timelinePrivileges: {}, + }); + + render( + + {}} /> + + ); + + expect(screen.queryByTestId('recent-timelines-container')).not.toBeInTheDocument(); + }); + + it('does render recent timelines for users with sufficient privileges', () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + timelinePrivileges: { read: true }, + }); + + render( + + {}} /> + + ); + + expect(screen.queryByTestId('recent-timelines-container')).toBeInTheDocument(); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/overview/components/sidebar/sidebar.tsx b/x-pack/solutions/security/plugins/security_solution/public/overview/components/sidebar/sidebar.tsx index 501e03a65cb02..10d3e40bb926e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/overview/components/sidebar/sidebar.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/overview/components/sidebar/sidebar.tsx @@ -17,6 +17,7 @@ import { import { Filters as RecentTimelinesFilters } from '../recent_timelines/filters'; import { StatefulRecentTimelines } from '../recent_timelines'; import { StatefulNewsFeed } from '../../../common/components/news_feed'; +import { useUserPrivileges } from '../../../common/components/user_privileges'; import type { FilterMode as RecentTimelinesFilterMode } from '../recent_timelines/types'; import { SidebarHeader } from '../../../common/components/sidebar_header'; @@ -35,6 +36,9 @@ export const Sidebar = React.memo<{ setRecentTimelinesFilterBy: (filterBy: RecentTimelinesFilterMode) => void; }>(({ recentTimelinesFilterBy, setRecentTimelinesFilterBy }) => { const { cases } = useKibana().services; + const { + timelinePrivileges: { read: canSeeTimelines }, + } = useUserPrivileges(); const recentTimelinesFilters = useMemo( () => ( )} - - {recentTimelinesFilters} - - + {canSeeTimelines && ( + + {recentTimelinesFilters} + + + )} { private config: SecuritySolutionUiConfigType; @@ -438,7 +439,10 @@ export class Plugin implements IPlugin ({ status: AppStatus.inaccessible, visibleIn: [], diff --git a/x-pack/solutions/security/plugins/security_solution/public/rules/links.ts b/x-pack/solutions/security/plugins/security_solution/public/rules/links.ts index ba4280739bbe6..28f44585d3037 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/rules/links.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/rules/links.ts @@ -13,7 +13,7 @@ import { RULES_CREATE_PATH, RULES_LANDING_PATH, RULES_PATH, - SERVER_APP_ID, + SECURITY_FEATURE_ID, } from '../../common/constants'; import { ADD_RULES, @@ -38,7 +38,7 @@ export const links: LinkItem = { hideTimeline: true, skipUrlState: true, globalNavPosition: 2, - capabilities: [`${SERVER_APP_ID}.show`], + capabilities: [`${SECURITY_FEATURE_ID}.show`], links: [ { id: SecurityPageName.rules, @@ -79,7 +79,7 @@ export const links: LinkItem = { }), landingIcon: IconConsoleCloud, path: EXCEPTIONS_PATH, - capabilities: [`${SERVER_APP_ID}.showEndpointExceptions`], + capabilities: [`${SECURITY_FEATURE_ID}.showEndpointExceptions`], skipUrlState: true, hideTimeline: true, globalSearchKeywords: [ @@ -100,7 +100,7 @@ export const links: LinkItem = { } ), path: COVERAGE_OVERVIEW_PATH, - capabilities: [`${SERVER_APP_ID}.show`], + capabilities: [`${SECURITY_FEATURE_ID}.show`], globalSearchKeywords: [ i18n.translate('xpack.securitySolution.appLinks.coverageOverviewDashboard', { defaultMessage: 'MITRE ATT&CK Coverage', diff --git a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/links.ts b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/links.ts index 4ea47d829d794..29d374713cf17 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/links.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/siem_migrations/links.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { SecurityPageName, - SERVER_APP_ID, + SECURITY_FEATURE_ID, SIEM_MIGRATIONS_RULES_PATH, } from '../../common/constants'; import { SIEM_MIGRATIONS_RULES } from '../app/translations'; @@ -23,7 +23,7 @@ export const siemMigrationsLinks: LinkItem = { }), landingIcon: SiemMigrationsIcon, path: SIEM_MIGRATIONS_RULES_PATH, - capabilities: [`${SERVER_APP_ID}.show`], + capabilities: [`${SECURITY_FEATURE_ID}.show`], skipUrlState: true, hideTimeline: true, globalSearchKeywords: [ diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/hooks.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/hooks.test.tsx index 83019347d309f..1cc1e49516364 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/hooks.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/hooks.test.tsx @@ -81,7 +81,7 @@ jest.mock('../../common/lib/kibana', () => ({ services: { application: { capabilities: { - siem: { + siemV2: { crud: true, }, }, diff --git a/x-pack/solutions/security/plugins/security_solution/public/threat_intelligence/links.ts b/x-pack/solutions/security/plugins/security_solution/public/threat_intelligence/links.ts index afcb386981330..b9bb2c32d480a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/threat_intelligence/links.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/threat_intelligence/links.ts @@ -7,7 +7,7 @@ import { getSecuritySolutionLink } from '@kbn/threat-intelligence-plugin/public'; import type { SecurityPageName } from '../../common/constants'; -import { SERVER_APP_ID } from '../../common/constants'; +import { SECURITY_FEATURE_ID } from '../../common/constants'; import type { LinkItem } from '../common/links'; /** @@ -18,5 +18,5 @@ import type { LinkItem } from '../common/links'; export const indicatorsLinks: LinkItem = { ...getSecuritySolutionLink('indicators'), globalNavPosition: 8, - capabilities: [`${SERVER_APP_ID}.threat-intelligence`], + capabilities: [`${SECURITY_FEATURE_ID}.threat-intelligence`], }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/threat_intelligence/routes.tsx b/x-pack/solutions/security/plugins/security_solution/public/threat_intelligence/routes.tsx index edb57a635222a..16c473bbd83d1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/threat_intelligence/routes.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/threat_intelligence/routes.tsx @@ -31,11 +31,18 @@ import { deleteOneQuery, setQuery } from '../common/store/inputs/actions'; import { InputsModelId } from '../common/store/inputs/constants'; import { ArtifactFlyout } from '../management/components/artifact_list_page/components/artifact_flyout'; import { SecurityRoutePageWrapper } from '../common/components/security_route_page_wrapper'; +import { extractTimelineCapabilities } from '../common/utils/timeline_capabilities'; const ThreatIntelligence = memo(() => { - const { threatIntelligence, http } = useKibana().services; + const { + threatIntelligence, + http, + application: { capabilities }, + } = useKibana().services; const ThreatIntelligencePlugin = threatIntelligence.getComponent(); + const { read: hasAccessToTimeline } = extractTimelineCapabilities(capabilities); + const sourcererDataView = useSourcererDataView(); const securitySolutionStore = getStore() as Store; @@ -50,6 +57,7 @@ const ThreatIntelligence = memo(() => { licenseService, sourcererDataView: sourcererDataView as unknown as SelectedDataView, getUseInvestigateInTimeline: useInvestigateInTimeline, + hasAccessToTimeline, blockList: { canWriteBlocklist, @@ -85,7 +93,7 @@ const ThreatIntelligence = memo(() => { SiemSearchBar, }), - [canWriteBlocklist, http, securitySolutionStore, sourcererDataView] + [canWriteBlocklist, http, securitySolutionStore, sourcererDataView, hasAccessToTimeline] ); return ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/threat_intelligence/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/threat_intelligence/translations.ts deleted file mode 100644 index 02553c2c5140a..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/threat_intelligence/translations.ts +++ /dev/null @@ -1,15 +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 ACTION_INVESTIGATE_IN_TIMELINE = i18n.translate( - 'xpack.securitySolution.threatIntelligence.investigateInTimelineTitle', - { - defaultMessage: 'Investigate in timeline', - } -); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/add_to_favorites/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/add_to_favorites/index.test.tsx index 8aeec715c9941..51bc006c5dea7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/add_to_favorites/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/add_to_favorites/index.test.tsx @@ -10,6 +10,9 @@ import { render } from '@testing-library/react'; import { mockTimelineModel, TestProviders } from '../../../common/mock'; import { AddToFavoritesButton } from '.'; import { TimelineStatusEnum } from '../../../../common/api/timeline'; +import { useUserPrivileges } from '../../../common/components/user_privileges'; + +jest.mock('../../../common/components/user_privileges'); const mockGetState = jest.fn(); jest.mock('react-redux', () => { @@ -38,6 +41,14 @@ const renderAddFavoritesButton = () => ); describe('AddToFavoritesButton', () => { + beforeEach(() => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + timelinePrivileges: { + crud: true, + }, + }); + }); + it('should render favorite button enabled and unchecked', () => { mockGetState.mockReturnValue({ ...mockTimelineModel, @@ -54,6 +65,21 @@ describe('AddToFavoritesButton', () => { expect(queryByTestId('timeline-favorite-filled-star')).not.toBeInTheDocument(); }); + it('should render favorite button disabled for users without write access to timeline', () => { + mockGetState.mockReturnValue({ + ...mockTimelineModel, + status: TimelineStatusEnum.active, + }); + (useUserPrivileges as jest.Mock).mockReturnValue({ + timelinePrivileges: { + crud: false, + }, + }); + + const { getByTestId } = renderAddFavoritesButton(); + expect(getByTestId('timeline-favorite-empty-star')).toHaveProperty('disabled'); + }); + it('should render favorite button disabled for a draft timeline', () => { mockGetState.mockReturnValue({ ...mockTimelineModel, diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/add_to_favorites/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/add_to_favorites/index.tsx index 3acbbce33ee98..a6c372023164d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/add_to_favorites/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/add_to_favorites/index.tsx @@ -13,6 +13,7 @@ import type { State } from '../../../common/store'; import { selectTimelineById } from '../../store/selectors'; import { timelineActions } from '../../store'; import { TimelineStatusEnum } from '../../../../common/api/timeline'; +import { useUserPrivileges } from '../../../common/components/user_privileges'; const ADD_TO_FAVORITES = i18n.translate( 'xpack.securitySolution.timeline.addToFavoriteButtonLabel', @@ -44,8 +45,12 @@ export const AddToFavoritesButton = React.memo(({ tim const { isFavorite, status } = useSelector((state: State) => selectTimelineById(state, timelineId) ); + const { + timelinePrivileges: { crud: canWriteTimeline }, + } = useUserPrivileges(); const isTimelineDraftOrImmutable = status !== TimelineStatusEnum.active; + const isDisabled = !canWriteTimeline || isTimelineDraftOrImmutable; const label = isFavorite ? REMOVE_FROM_FAVORITES : ADD_TO_FAVORITES; const handleClick = useCallback( @@ -57,7 +62,7 @@ export const AddToFavoritesButton = React.memo(({ tim describe('SaveTimelineButton', () => { it('should render components', async () => { (useUserPrivileges as jest.Mock).mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: true }, + timelinePrivileges: { crud: true }, }); mockGetState.mockReturnValue({ ...mockTimelineModel, @@ -65,7 +65,7 @@ describe('SaveTimelineButton', () => { it('should override the default text and color of the button', async () => { (useUserPrivileges as jest.Mock).mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: true }, + timelinePrivileges: { crud: true }, }); mockGetState.mockReturnValue({ ...mockTimelineModel, @@ -92,7 +92,7 @@ describe('SaveTimelineButton', () => { it('should open the timeline save modal', async () => { (useUserPrivileges as jest.Mock).mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: true }, + timelinePrivileges: { crud: true }, }); mockGetState.mockReturnValue({ ...mockTimelineModel, @@ -113,7 +113,7 @@ describe('SaveTimelineButton', () => { it('should disable the save timeline button when the user does not have write access', () => { (useUserPrivileges as jest.Mock).mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: false }, + timelinePrivileges: { crud: false }, }); mockGetState.mockReturnValue(mockTimelineModel); @@ -124,7 +124,7 @@ describe('SaveTimelineButton', () => { it('should disable the save timeline button when the timeline is immutable', () => { (useUserPrivileges as jest.Mock).mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: true }, + timelinePrivileges: { crud: true }, }); mockGetState.mockReturnValue({ ...mockTimelineModel, status: TimelineStatusEnum.immutable }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/actions/save_timeline_button.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/actions/save_timeline_button.tsx index de9d35b283517..9687bbcb82054 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/actions/save_timeline_button.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/modal/actions/save_timeline_button.tsx @@ -55,7 +55,7 @@ export const SaveTimelineButton = React.memo( // TODO: User may have Crud privileges but they may not have access to timeline index. // Do we need to check that? const { - kibanaSecuritySolutionsPrivileges: { crud: canEditTimelinePrivilege }, + timelinePrivileges: { crud: canEditTimelinePrivilege }, } = useUserPrivileges(); const { status, isSaving } = useSelector((state: State) => diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/notes/old_notes.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/notes/old_notes.tsx index 09c45d887b373..a1b77bc5672c1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/notes/old_notes.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/notes/old_notes.tsx @@ -115,7 +115,7 @@ interface NotesTabContentProps { */ export const OldNotes: React.FC = React.memo(({ timelineId }) => { const dispatch = useDispatch(); - const { kibanaSecuritySolutionsPrivileges } = useUserPrivileges(); + const { notesPrivileges } = useUserPrivileges(); const getScrollToTop = useMemo(() => getScrollToTopSelector(), []); const scrollToTop = useShallowEqualSelector((state) => getScrollToTop(state, timelineId)); @@ -182,7 +182,7 @@ export const OldNotes: React.FC = React.memo(({ timelineId - {!isImmutable && kibanaSecuritySolutionsPrivileges.crud === true && ( + {!isImmutable && notesPrivileges.crud === true && ( { it('should render the callout and save components', () => { (useUserPrivileges as jest.Mock).mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: true }, + timelinePrivileges: { crud: true }, }); const mockStore = createMockStore({ diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx index 5a3ba28a82767..4e39ee6ccd6fd 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx @@ -109,7 +109,8 @@ describe('StatefulOpenTimeline', () => { pageName: SecurityPageName.timelines, }); useUserPrivilegesMock.mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, + timelinePrivileges: { crud: true, read: true }, + notesPrivileges: { crud: true, read: true }, }); mockHistory = []; (useHistory as jest.Mock).mockReturnValue(mockHistory); @@ -804,4 +805,44 @@ describe('StatefulOpenTimeline', () => { expect(queryByTestId('create-rule-from-timeline')).not.toBeInTheDocument(); }); }); + + describe('privileges', () => { + test('installs prepackaged timelines when the user has sufficient privileges', async () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + timelinePrivileges: { crud: true }, + }); + mount( + + + + ); + + await waitFor(() => { + expect(mockInstallPrepackagedTimelines).toHaveBeenCalled(); + }); + }); + + test('does not install prepackaged timelines when the user has insufficient privileges', async () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + timelinePrivileges: { crud: false }, + }); + mount( + + + + ); + + await waitFor(() => { + expect(mockInstallPrepackagedTimelines).not.toHaveBeenCalled(); + }); + }); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.tsx index 8af06fe910f99..7cd7cbe18927f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/index.tsx @@ -16,6 +16,7 @@ import { import { useNavigation } from '../../../common/lib/kibana'; import { SecurityPageName } from '../../../../common/constants'; import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; +import { useUserPrivileges } from '../../../common/components/user_privileges'; import type { SortFieldTimeline } from '../../../../common/api/timeline'; import { TimelineId } from '../../../../common/types/timeline'; import type { TimelineModel } from '../../store/model'; @@ -368,13 +369,20 @@ export const StatefulOpenTimelineComponent = React.memo( focusInput(); }, []); + const { + timelinePrivileges: { crud: canWriteTimelines }, + } = useUserPrivileges(); useEffect(() => { const fetchData = async () => { - await installPrepackagedTimelines(); - refetch(); + if (canWriteTimelines) { + await installPrepackagedTimelines(); + refetch(); + } else { + refetch(); + } }; fetchData(); - }, [refetch, installPrepackagedTimelines]); + }, [refetch, installPrepackagedTimelines, canWriteTimelines]); return !isModal ? ( { jest.mock('./hooks/use_delete_note'); +jest.mock('../../../../common/components/user_privileges'); + const deleteMutateMock = jest.fn(); describe('NotePreviews', () => { @@ -51,6 +54,11 @@ describe('NotePreviews', () => { onError: jest.fn(), isLoading: false, }); + (useUserPrivileges as jest.Mock).mockReturnValue({ + notesPrivileges: { + crud: true, + }, + }); }); test('it renders a note preview for each note when isModal is false', () => { @@ -341,4 +349,37 @@ describe('NotePreviews', () => { expect(deleteMutateMock.mock.calls[0][0]).toBe('test-id-1'); }); }); + + describe('Insuffiecient privileges', () => { + it('should not show the delete note button', () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + notesPrivileges: { + crud: false, + }, + }); + + const timeline = mockTimelineResults[0]; + (useDeepEqualSelector as jest.Mock).mockReturnValue(timeline); + + const wrapper = mountWithI18nProvider( + , + { + wrappingComponent: createReactQueryWrapper(), + } + ); + + expect(wrapper.find('[data-test-subj="delete-note"]').exists()).toBe(false); + }); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx index 19f98aff651fa..4080e254303a2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx @@ -35,6 +35,7 @@ import { useSourcererDataView } from '../../../../sourcerer/containers'; import { useDeleteNote } from './hooks/use_delete_note'; import { getTimelineNoteSelector } from '../../timeline/tabs/notes/selectors'; import { DocumentEventTypes } from '../../../../common/lib/telemetry'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; export const NotePreviewsContainer = styled.section` padding-top: ${({ theme }) => `${theme.eui.euiSizeS}`}; @@ -190,21 +191,10 @@ const NoteActions = React.memo<{ savedObjectId, showToggleEventDetailsAction = true, }) => { - return eventId && timelineId ? ( - <> - {showToggleEventDetailsAction ? ( - - ) : null} - - - ) : ( + const { + notesPrivileges: { crud: canCrudNotes }, + } = useUserPrivileges(); + const DeleteButton = canCrudNotes ? ( + ) : null; + + return eventId && timelineId ? ( + <> + {showToggleEventDetailsAction ? ( + + ) : null} + {DeleteButton} + + ) : ( + <>{DeleteButton} ); } ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx index 713dff571847b..1593e52f931cb 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx @@ -106,7 +106,7 @@ describe('OpenTimeline', () => { test('it shows the delete action columns when onDeleteSelected and deleteTimelines are specified', () => { useUserPrivilegesMock.mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, + timelinePrivileges: { crud: true, read: true }, }); const defaultProps = getDefaultTestProps(mockResults); const wrapper = mountWithIntl( @@ -190,7 +190,7 @@ describe('OpenTimeline', () => { deleteTimelines: undefined, }; useUserPrivilegesMock.mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: false, read: true }, + timelinePrivileges: { crud: false, read: true }, }); const wrapper = mountWithIntl( @@ -354,7 +354,7 @@ describe('OpenTimeline', () => { test('it should disable delete timeline if no timeline is selected', async () => { useUserPrivilegesMock.mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, + timelinePrivileges: { crud: true, read: true }, }); const defaultProps = { ...getDefaultTestProps(mockResults), @@ -405,7 +405,7 @@ describe('OpenTimeline', () => { test('it should enable delete timeline if a timeline is selected', async () => { useUserPrivilegesMock.mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, + timelinePrivileges: { crud: true, read: true }, }); const defaultProps = { ...getDefaultTestProps(mockResults), @@ -432,7 +432,7 @@ describe('OpenTimeline', () => { test("it should render a selectable timeline table if timelineStatus is active (selecting custom templates' tab)", () => { useUserPrivilegesMock.mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, + timelinePrivileges: { crud: true, read: true }, }); const defaultProps = { ...getDefaultTestProps(mockResults), @@ -451,7 +451,7 @@ describe('OpenTimeline', () => { test('it should include createRule in timeline actions if onCreateRule is passed', () => { useUserPrivilegesMock.mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, + timelinePrivileges: { crud: true, read: true }, }); const defaultProps = { ...getDefaultTestProps(mockResults), @@ -474,7 +474,7 @@ describe('OpenTimeline', () => { timelineStatus: TimelineStatusEnum.active, }; useUserPrivilegesMock.mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: false, read: true }, + timelinePrivileges: { crud: false, read: true }, }); const wrapper = mountWithIntl( @@ -517,7 +517,7 @@ describe('OpenTimeline', () => { test("it should not render a selectable timeline table if timelineStatus is immutable (selecting Elastic templates' tab)", () => { useUserPrivilegesMock.mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, + timelinePrivileges: { crud: true, read: true }, }); const defaultProps = { ...getDefaultTestProps(mockResults), @@ -564,7 +564,7 @@ describe('OpenTimeline', () => { test("it should render a selectable timeline table if timelineStatus is null (no template timelines' tab selected)", () => { useUserPrivilegesMock.mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, + timelinePrivileges: { crud: true, read: true }, }); const defaultProps = { ...getDefaultTestProps(mockResults), @@ -587,7 +587,7 @@ describe('OpenTimeline', () => { timelineStatus: null, }; useUserPrivilegesMock.mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: false, read: true }, + timelinePrivileges: { crud: false, read: true }, }); const wrapper = mountWithIntl( diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx index 5a1a9155bb5c1..3c62ebbcf065d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx @@ -78,9 +78,9 @@ export const OpenTimeline = React.memo( onCompleteEditTimelineAction, } = useEditTimelineActions(); const tableRef = useRef | null>(null); - const { kibanaSecuritySolutionsPrivileges } = useUserPrivileges(); + const { timelinePrivileges } = useUserPrivileges(); const { getBatchItemsPopoverContent } = useEditTimelineBatchActions({ - deleteTimelines: kibanaSecuritySolutionsPrivileges.crud ? deleteTimelines : undefined, + deleteTimelines: timelinePrivileges.crud ? deleteTimelines : undefined, selectedItems, tableRef, timelineType, @@ -156,7 +156,7 @@ export const OpenTimeline = React.memo( }, [setImportDataModalToggle, refetch]); const actionTimelineToShow = useMemo(() => { - if (kibanaSecuritySolutionsPrivileges.crud) { + if (timelinePrivileges.crud) { const createRule: ActionTimelineToShow[] = ['createRule']; const createRuleFromEql: ActionTimelineToShow[] = ['createRuleFromEql']; const timelineActions: ActionTimelineToShow[] = [ @@ -192,7 +192,7 @@ export const OpenTimeline = React.memo( timelineStatus, onDeleteSelected, deleteTimelines, - kibanaSecuritySolutionsPrivileges, + timelinePrivileges, ]); const SearchRowContent = useMemo(() => <>{templateTimelineFilter}, [templateTimelineFilter]); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx index 00cec44b290be..1496d44a43b2e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx @@ -14,6 +14,7 @@ import { mockOpenTimelineQueryResults } from '../../../../common/mock/timeline_r import { useGetAllTimeline, getAllTimeline } from '../../../containers/all'; import { useTimelineStatus } from '../use_timeline_status'; import { OpenTimelineModal } from '.'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; jest.mock('../../../../common/lib/kibana', () => { const actual = jest.requireActual('../../../../common/lib/kibana'); @@ -53,6 +54,8 @@ jest.mock( children({ width: 100, height: 500 }) ); +jest.mock('../../../../common/components/user_privileges'); + describe('OpenTimelineModal', () => { const mockInstallPrepackagedTimelines = jest.fn(); beforeEach(() => { @@ -68,6 +71,9 @@ describe('OpenTimelineModal', () => { templateTimelineFilter:
, installPrepackagedTimelines: mockInstallPrepackagedTimelines, }); + (useUserPrivileges as jest.Mock).mockReturnValue({ + timelinePrivileges: { crud: true }, + }); }); afterEach(() => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx index 0df2170b0697e..c8fd42d68c435 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx @@ -180,7 +180,7 @@ export const TimelinesTable = React.memo( onSelectionChange, }; }, [onSelectionChange]); - const { kibanaSecuritySolutionsPrivileges } = useUserPrivileges(); + const { timelinePrivileges } = useUserPrivileges(); const columns = useMemo( () => getTimelinesTableColumns({ @@ -196,7 +196,7 @@ export const TimelinesTable = React.memo( onToggleShowNotes, showExtendedColumns, timelineType, - hasCrudAccess: kibanaSecuritySolutionsPrivileges.crud, + hasCrudAccess: timelinePrivileges.crud, }), [ actionTimelineToShow, @@ -211,7 +211,7 @@ export const TimelinesTable = React.memo( onToggleShowNotes, showExtendedColumns, timelineType, - kibanaSecuritySolutionsPrivileges, + timelinePrivileges, ] ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/properties/notes_flyout.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/properties/notes_flyout.tsx index 8ff78376c7683..8621faa650d48 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/properties/notes_flyout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/properties/notes_flyout.tsx @@ -19,6 +19,7 @@ import type { EuiTheme } from '@kbn/react-kibana-context-styled'; import type { NoteCardsProps } from '../../notes/note_cards'; import { NoteCards } from '../../notes/note_cards'; import * as i18n from './translations'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; export type NotesFlyoutProps = { show: boolean; @@ -46,6 +47,9 @@ const NotesFlyoutContainer = styled(EuiFlyoutResizable)` export const NotesFlyout = React.memo(function NotesFlyout(props: NotesFlyoutProps) { const { eventId, toggleShowAddNote, show, onClose, associateNote, notes, timelineId, onCancel } = props; + const { + notesPrivileges: { crud: showAddNote }, + } = useUserPrivileges(); const notesFlyoutTitleId = useGeneratedHtmlId({ prefix: 'notesFlyoutTitle', @@ -78,7 +82,7 @@ export const NotesFlyout = React.memo(function NotesFlyout(props: NotesFlyoutPro className="notes-in-flyout" data-test-subj="note-cards" notes={notes} - showAddNote={true} + showAddNote={showAddNote} toggleShowAddNote={toggleShowAddNote} eventId={eventId} timelineId={timelineId} diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.test.tsx index a80a34596b1de..b307e06331039 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.test.tsx @@ -29,6 +29,8 @@ import { DefaultCellRenderer } from '../../cell_rendering/default_cell_renderer' import { defaultRowRenderers } from '../../body/renderers'; import { useDispatch } from 'react-redux'; import { TimelineTabs } from '@kbn/securitysolution-data-table'; +import { useUserPrivileges } from '../../../../../common/components/user_privileges'; +import { initialUserPrivilegesState } from '../../../../../common/components/user_privileges/user_privileges_context'; const SPECIAL_TEST_TIMEOUT = 30000; @@ -56,6 +58,8 @@ mockUseResizeObserver.mockImplementation(() => ({})); jest.mock('../../../../../common/lib/kibana'); +jest.mock('../../../../../common/components/user_privileges'); + let useTimelineEventsMock = jest.fn(); const loadPageMock = jest.fn(); @@ -125,6 +129,11 @@ describe('EQL Tab', () => { } ); + (useUserPrivileges as jest.Mock).mockReturnValue({ + ...initialUserPrivilegesState(), + notesPrivileges: { read: true }, + }); + HTMLElement.prototype.getBoundingClientRect = jest.fn(() => { return { width: 1000, diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/esql/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/esql/index.tsx index 4f78509c054d1..9e7386c5a7560 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/esql/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/esql/index.tsx @@ -28,6 +28,7 @@ import { useSetDiscoverCustomizationCallbacks } from './customizations/use_set_d import { EmbeddedDiscoverContainer, TimelineESQLGlobalStyles } from './styles'; import { timelineSelectors } from '../../../../store'; import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; +import { useUserPrivileges } from '../../../../../common/components/user_privileges'; import { timelineDefaults } from '../../../../store/defaults'; import { savedSearchComparator } from './utils'; import { GET_TIMELINE_DISCOVER_SAVED_SEARCH_TITLE } from './translations'; @@ -52,6 +53,9 @@ export const DiscoverTabContent: FC = ({ timelineId }) savedSearch: savedSearchService, }, } = useKibana(); + const { + timelinePrivileges: { crud: canSaveTimeline }, + } = useUserPrivileges(); const dispatch = useDispatch(); @@ -119,7 +123,6 @@ export const DiscoverTabContent: FC = ({ timelineId }) ]); const combinedDiscoverSavedSearchStateRef = useRef(); - useEffect(() => { if (isFetching) return; if (savedSearchByIdStatus === 'error' && savedSearchId) { @@ -137,6 +140,7 @@ export const DiscoverTabContent: FC = ({ timelineId }) if (!index) return; if (!latestState || combinedDiscoverSavedSearchStateRef.current === latestState) return; if (isEqualWith(latestState, savedSearchById, savedSearchComparator)) return; + if (!canSaveTimeline) return; updateSavedSearch(latestState, timelineId, function onUpdate() { combinedDiscoverSavedSearchStateRef.current = latestState; }); @@ -153,6 +157,7 @@ export const DiscoverTabContent: FC = ({ timelineId }) dispatch, savedSearchId, savedSearchByIdStatus, + canSaveTimeline, ]); useEffect(() => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/index.test.tsx index cc6d70ef1d46b..16b6675536fcd 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/index.test.tsx @@ -16,8 +16,10 @@ import { TimelineTypeEnum } from '../../../../../common/api/timeline'; import { useEsqlAvailability } from '../../../../common/hooks/esql/use_esql_availability'; import { render, screen, waitFor } from '@testing-library/react'; import { useLicense } from '../../../../common/hooks/use_license'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; jest.mock('../../../../common/hooks/use_license'); +jest.mock('../../../../common/components/user_privileges'); const mockUseUiSetting = jest.fn().mockReturnValue([false]); jest.mock('@kbn/kibana-react-plugin/public', () => { @@ -137,4 +139,36 @@ describe('Timeline', () => { expect(screen.queryByTestId(sessionViewTabSubj)).not.toBeInTheDocument(); }); }); + + describe('privileges', () => { + it('should show notes and pinned tabs for users with the required privileges', () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + timelinePrivileges: { read: true }, + notesPrivileges: { read: true }, + }); + + render( + + + + ); + expect(screen.getByTestId('timelineTabs-notes')).not.toBeDisabled(); + expect(screen.getByTestId('timelineTabs-pinned')).not.toBeDisabled(); + }); + + it('should not show notes and pinned tabs for users with the insufficient privileges', () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + timelinePrivileges: { read: false }, + notesPrivileges: { read: false }, + }); + + render( + + + + ); + expect(screen.getByTestId('timelineTabs-notes')).toBeDisabled(); + expect(screen.getByTestId('timelineTabs-pinned')).toBeDisabled(); + }); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx index 86999ff529fa1..88b134cf00e30 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx @@ -44,6 +44,7 @@ import { initializeTimelineSettings } from '../../../store/actions'; import { selectTimelineById, selectTimelineESQLSavedSearchId } from '../../../store/selectors'; import { fetchNotesBySavedObjectIds, selectSortedNotesBySavedObjectId } from '../../../../notes'; import { ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING } from '../../../../../common/constants'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; const HideShowContainer = styled.div.attrs<{ $isVisible: boolean; isOverflowYScroll: boolean }>( ({ $isVisible = false, isOverflowYScroll = false }) => ({ @@ -297,6 +298,11 @@ const TabsContentComponent: React.FC = ({ [timelineSavedObjectId] ); + const { + notesPrivileges: { read: canSeeNotes }, + timelinePrivileges: { read: canSeePinnedTab }, + } = useUserPrivileges(); + // new note system const fetchNotes = useCallback( () => dispatch(fetchNotesBySavedObjectIds({ savedObjectIds: [timelineSavedObjectId] })), @@ -433,7 +439,7 @@ const TabsContentComponent: React.FC = ({ data-test-subj={`timelineTabs-${TimelineTabs.notes}`} onClick={setNotesAsActiveTab} isSelected={activeTab === TimelineTabs.notes} - disabled={timelineType === TimelineTypeEnum.template} + disabled={!canSeeNotes || timelineType === TimelineTypeEnum.template} key={TimelineTabs.notes} > {i18n.NOTES_TAB} @@ -444,7 +450,7 @@ const TabsContentComponent: React.FC = ({ diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/notes/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/notes/index.test.tsx index 452dc1620b946..d6aaa6ba6df9b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/notes/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/notes/index.test.tsx @@ -72,7 +72,8 @@ describe('NotesTabContentComponent', () => { jest.clearAllMocks(); (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false); (useUserPrivileges as jest.Mock).mockReturnValue({ - kibanaSecuritySolutionsPrivileges: { crud: true }, + notesPrivileges: { crud: true }, + timelinePrivileges: { crud: true }, }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/notes/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/notes/index.tsx index 58522d32dd55f..1ce8570ce1a34 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/notes/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/notes/index.tsx @@ -83,8 +83,8 @@ const NotesTabContentComponent: React.FC = React.memo(({ t const { addError: addErrorToast } = useAppToasts(); const dispatch = useDispatch(); - const { kibanaSecuritySolutionsPrivileges } = useUserPrivileges(); - const canCreateNotes = kibanaSecuritySolutionsPrivileges.crud; + const { notesPrivileges } = useUserPrivileges(); + const canCreateNotes = notesPrivileges.crud; const securitySolutionNotesDisabled = useIsExperimentalFeatureEnabled( 'securitySolutionNotesDisabled' diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx index caf893e4d0951..25472d7d4f277 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.test.tsx @@ -248,6 +248,8 @@ describe('query tab with unified timeline', () => { ); (useUserPrivileges as jest.Mock).mockReturnValue({ + notesPrivileges: { crud: true, read: true }, + timelinePrivileges: { crud: true, read: true }, kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, endpointPrivileges: getEndpointPrivilegesInitialStateMock(), detectionEnginePrivileges: { loading: false, error: undefined, result: undefined }, diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.test.tsx index 54155a493da64..4f346d68739a6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.test.tsx @@ -43,7 +43,7 @@ jest.mock('../../../../../common/lib/kibana', () => { navigateToApp: jest.fn(), getUrlForApp: jest.fn(), capabilities: { - siem: { crud_alerts: true, read_alerts: true }, + siemV2: { crud_alerts: true, read_alerts: true }, }, }, sessionView: { diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_control_columns.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_control_columns.test.tsx index 7dc3ddc88caae..1830619e77b66 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_control_columns.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_control_columns.test.tsx @@ -5,12 +5,17 @@ * 2.0. */ -import type { EuiDataGridControlColumn } from '@elastic/eui'; -import { TestProviders } from '../../../../../common/mock'; -import { renderHook } from '@testing-library/react'; +import React from 'react'; +import type { EuiDataGridControlColumn, EuiDataGridCellValueElementProps } from '@elastic/eui'; +import { render, renderHook, screen } from '@testing-library/react'; +import { TestProviders, mockTimelineData } from '../../../../../common/mock'; import { useLicense } from '../../../../../common/hooks/use_license'; import { useTimelineControlColumn } from './use_timeline_control_columns'; import { TimelineId } from '@kbn/timelines-plugin/public/store/timeline'; +import type { UnifiedTimelineDataGridCellContext } from '../../types'; +import { useUserPrivileges } from '../../../../../common/components/user_privileges'; +import { initialUserPrivilegesState } from '../../../../../common/components/user_privileges/user_privileges_context'; +import { useTimelineUnifiedDataTableContext } from '../../unified_components/data_table/use_timeline_unified_data_table_context'; jest.mock('../../../../../common/hooks/use_license', () => ({ useLicense: jest.fn().mockReturnValue({ @@ -18,6 +23,9 @@ jest.mock('../../../../../common/hooks/use_license', () => ({ }), })); +jest.mock('../../unified_components/data_table/use_timeline_unified_data_table_context'); +jest.mock('../../../../../common/components/user_privileges'); + const useLicenseMock = useLicense as jest.Mock; describe('useTimelineControlColumns', () => { @@ -84,4 +92,115 @@ describe('useTimelineControlColumns', () => { expect(controlColumn.width).toBe(152); }); }); + + describe('privileges', () => { + const defaultProps = { + ariaRowindex: 2, + checked: false, + columnId: '', + columnValues: 'abc def', + disableExpandAction: false, + data: mockTimelineData[0].data, + ecsData: mockTimelineData[0].ecs, + eventId: 'abc', + eventIdToNoteIds: {}, + index: 2, + isEventPinned: false, + loadingEventIds: [], + onEventDetailsPanelOpened: () => {}, + onRowSelected: () => {}, + refetch: () => {}, + rowIndex: 10, + setEventsDeleted: () => {}, + setEventsLoading: () => {}, + showCheckboxes: true, + timelineId: 'test', + toggleShowNotes: () => {}, + setCellProps: () => {}, + isExpandable: true, + isExpanded: false, + isDetails: true, + colIndex: 0, + }; + + type RowCellRendererComponent = ( + props: EuiDataGridCellValueElementProps & UnifiedTimelineDataGridCellContext + ) => React.JSX.Element; + + beforeEach(() => { + useLicenseMock.mockReturnValue({ + isEnterprise: () => true, + isPlatinumPlus: () => true, + }); + (useTimelineUnifiedDataTableContext as jest.Mock).mockReturnValue({ + expanded: { id: mockTimelineData[0]._id }, + }); + }); + + it('should render the notes and pin buttons when the user has the correct privileges', async () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + ...initialUserPrivilegesState(), + notesPrivileges: { crud: true, read: true }, + timelinePrivileges: { crud: true }, + }); + + const { result } = renderHook( + () => + useTimelineControlColumn({ + timelineId: TimelineId.test, + refetch: refetchMock, + events: mockTimelineData, + pinnedEventIds: {}, + eventIdToNoteIds: {}, + onToggleShowNotes: jest.fn(), + }), + { + wrapper: TestProviders, + } + ); + const ControlColumnActions = result.current[0].rowCellRender as RowCellRendererComponent; + + render( + + + + ); + + expect(await screen.findByTestId('timeline-notes-button-small')).toBeVisible(); + expect(await screen.findByTestId('pin')).toBeVisible(); + }); + + it('should not render the notes and pin buttons when the user does not have the correct privilege', async () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + ...initialUserPrivilegesState(), + notesPrivileges: { crud: false, read: false }, + timelinePrivileges: { crud: false }, + }); + + const { result } = renderHook( + () => + useTimelineControlColumn({ + timelineId: TimelineId.test, + refetch: refetchMock, + events: mockTimelineData, + pinnedEventIds: {}, + eventIdToNoteIds: {}, + onToggleShowNotes: jest.fn(), + }), + { + wrapper: TestProviders, + } + ); + const ControlColumnActions = result.current[0].rowCellRender as RowCellRendererComponent; + + render( + + + + ); + + expect(await screen.queryByTestId('timeline-notes-button-small')).not.toBeInTheDocument(); + expect(await screen.queryByTestId('pin')).not.toBeInTheDocument(); + }); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_control_columns.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_control_columns.tsx index 979282cd7b752..fa5492b926690 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_control_columns.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/shared/use_timeline_control_columns.tsx @@ -14,6 +14,7 @@ import { getDefaultControlColumn } from '../../body/control_columns'; import { TimelineControlColumnCellRender } from '../../unified_components/data_table/control_column_cell_render'; import type { UnifiedTimelineDataGridCellContext } from '../../types'; import { useTimelineUnifiedDataTableContext } from '../../unified_components/data_table/use_timeline_unified_data_table_context'; +import { useUserPrivileges } from '../../../../../common/components/user_privileges'; interface UseTimelineControlColumnArgs { timelineId: string; @@ -37,6 +38,10 @@ export const useTimelineControlColumn = ({ }: UseTimelineControlColumnArgs) => { const isEnterprisePlus = useLicense().isEnterprise(); const ACTION_BUTTON_COUNT = useMemo(() => (isEnterprisePlus ? 5 : 4), [isEnterprisePlus]); + const { + notesPrivileges: { read: canReadNotes }, + timelinePrivileges: { crud: canWriteTimelines }, + } = useUserPrivileges(); const RowCellRender = useMemo( () => @@ -84,10 +89,21 @@ export const useTimelineControlColumn = ({ pinnedEventIds={pinnedEventIds} eventIdToNoteIds={eventIdToNoteIds} toggleShowNotes={onToggleShowNotes} + showNotes={canReadNotes} + disablePinAction={!canWriteTimelines} /> ); }, - [events, timelineId, refetch, pinnedEventIds, eventIdToNoteIds, onToggleShowNotes] + [ + events, + timelineId, + refetch, + pinnedEventIds, + eventIdToNoteIds, + onToggleShowNotes, + canReadNotes, + canWriteTimelines, + ] ); return useMemo(() => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/control_column_cell_render.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/control_column_cell_render.tsx index ca9a1b0c06d5e..43c37ec518512 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/control_column_cell_render.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/control_column_cell_render.tsx @@ -45,14 +45,14 @@ export const TimelineControlColumnCellRender = memo(function TimelineControlColu onRowSelected={noOp} onRuleChange={noOp} showCheckboxes={false} - showNotes={true} + showNotes={props.showNotes} timelineId={props.timelineId} ariaRowindex={rowIndex} checked={false} loadingEventIds={props.loadingEventIds} toggleShowNotes={props.toggleShowNotes} disableExpandAction - disablePinAction={false} + disablePinAction={props.disablePinAction} /> ); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/index.test.tsx index c41e62dc2d1cd..37da5e0f3b01d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/containers/index.test.tsx @@ -44,7 +44,17 @@ jest.mock('../../common/lib/kibana', () => ({ addWarning: jest.fn(), remove: jest.fn(), }), - useKibana: jest.fn(), + useKibana: jest.fn().mockReturnValue({ + services: { + application: { + capabilities: { + securitySolutionTimeline: { + crud: true, + }, + }, + }, + }, + }), })); const mockUseRouteSpy: jest.Mock = useRouteSpy as jest.Mock; diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/links.ts b/x-pack/solutions/security/plugins/security_solution/public/timelines/links.ts index 9315417d97646..1953b75829bed 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/links.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/links.ts @@ -6,7 +6,12 @@ */ import { i18n } from '@kbn/i18n'; -import { SecurityPageName, SERVER_APP_ID, TIMELINES_PATH } from '../../common/constants'; +import { + SECURITY_FEATURE_ID, + SecurityPageName, + TIMELINE_FEATURE_ID, + TIMELINES_PATH, +} from '../../common/constants'; import { TIMELINES } from '../app/translations'; import type { LinkItem } from '../common/links/types'; @@ -15,7 +20,8 @@ export const links: LinkItem = { title: TIMELINES, path: TIMELINES_PATH, globalNavPosition: 7, - capabilities: [`${SERVER_APP_ID}.show`], + // It only makes sense to show this link when the user is also granted access to security solution + capabilities: [[`${SECURITY_FEATURE_ID}.show`, `${TIMELINE_FEATURE_ID}.read`]], globalSearchKeywords: [ i18n.translate('xpack.securitySolution.appLinks.timelines', { defaultMessage: 'Timelines', @@ -29,6 +35,7 @@ export const links: LinkItem = { }), path: `${TIMELINES_PATH}/template`, sideNavDisabled: true, + capabilities: [[`${SECURITY_FEATURE_ID}.show`, `${TIMELINE_FEATURE_ID}.read`]], }, ], }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/index.tsx index 2151a2624aeb4..5e6339bd97655 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/index.tsx @@ -20,18 +20,20 @@ import { TIMELINES_PATH } from '../../../common/constants'; const timelinesPagePath = `${TIMELINES_PATH}/:tabName(${TimelineTypeEnum.default}|${TimelineTypeEnum.template})`; const timelinesDefaultPath = `${TIMELINES_PATH}/${TimelineTypeEnum.default}`; -export const Timelines = React.memo(() => ( - - - - - ( - - )} - /> - -)); +export const Timelines = React.memo(() => { + return ( + + + + + ( + + )} + /> + + ); +}); Timelines.displayName = 'Timelines'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx index ee7dfc19d59f5..ae82e2a381010 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx @@ -8,9 +8,9 @@ import type { ShallowWrapper } from 'enzyme'; import { shallow } from 'enzyme'; import React from 'react'; -import { useKibana } from '../../common/lib/kibana'; import { TimelinesPage } from './timelines_page'; import { useSourcererDataView } from '../../sourcerer/containers'; +import { useUserPrivileges } from '../../common/components/user_privileges'; jest.mock('react-router-dom', () => { const originalModule = jest.requireActual('react-router-dom'); @@ -24,14 +24,7 @@ jest.mock('react-router-dom', () => { }); jest.mock('../../overview/components/events_by_dataset'); jest.mock('../../sourcerer/containers'); -jest.mock('../../common/lib/kibana', () => { - const originalModule = jest.requireActual('../../common/lib/kibana'); - - return { - ...originalModule, - useKibana: jest.fn(), - }; -}); +jest.mock('../../common/components/user_privileges'); describe('TimelinesPage', () => { let wrapper: ShallowWrapper; @@ -41,7 +34,11 @@ describe('TimelinesPage', () => { indicesExist: false, sourcererDataView: {}, }); - (useKibana as unknown as jest.Mock).mockReturnValue({}); + (useUserPrivileges as jest.Mock).mockReturnValue({ + timelinePrivileges: { + crud: true, + }, + }); wrapper = shallow(); @@ -55,15 +52,9 @@ describe('TimelinesPage', () => { indicesExist: true, sourcererDataView: {}, }); - (useKibana as unknown as jest.Mock).mockReturnValue({ - services: { - application: { - capabilities: { - siem: { - crud: true, - }, - }, - }, + (useUserPrivileges as jest.Mock).mockReturnValue({ + timelinePrivileges: { + crud: true, }, }); @@ -74,23 +65,18 @@ describe('TimelinesPage', () => { expect(wrapper.exists('[data-test-subj="stateful-open-timeline"]')).toBeTruthy(); }); - it('should not show import button or modal if user does not have crud authorization', () => { - (useKibana as unknown as jest.Mock).mockReturnValue({ - services: { - application: { - capabilities: { - siem: { - crud: false, - }, - }, - }, + it('should not show import button or modal if user does not have crud privileges but it should show the new timeline button', () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + timelinePrivileges: { + crud: false, + read: true, }, }); wrapper = shallow(); expect(wrapper.exists('[data-test-subj="timelines-page-open-import-data"]')).toBeFalsy(); - expect(wrapper.exists('[data-test-subj="timelines-page-new"]')).toBeFalsy(); + expect(wrapper.exists('[data-test-subj="timelines-page-new"]')).toBeTruthy(); expect(wrapper.exists('[data-test-subj="stateful-open-timeline"]')).toBeTruthy(); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.tsx index 08cf46b41ad35..d213eeaf8f5f0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/pages/timelines_page.tsx @@ -12,21 +12,22 @@ import { NewTimelineButton } from '../components/new_timeline'; import { TimelineTypeEnum } from '../../../common/api/timeline'; import { HeaderPage } from '../../common/components/header_page'; import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; -import { useKibana } from '../../common/lib/kibana'; -import { SpyRoute } from '../../common/utils/route/spy_routes'; +import { useUserPrivileges } from '../../common/components/user_privileges'; import { StatefulOpenTimeline } from '../components/open_timeline'; import * as i18n from './translations'; import { SecurityPageName } from '../../app/types'; import { useSourcererDataView } from '../../sourcerer/containers'; import { EmptyPrompt } from '../../common/components/empty_prompt'; +import { SecurityRoutePageWrapper } from '../../common/components/security_route_page_wrapper'; export const DEFAULT_SEARCH_RESULTS_PER_PAGE = 10; export const TimelinesPage = React.memo(() => { const { tabName } = useParams<{ pageName: SecurityPageName; tabName: string }>(); const { indicesExist } = useSourcererDataView(); - const capabilitiesCanUserCRUD: boolean = - !!useKibana().services?.application?.capabilities?.siem?.crud; + const { + timelinePrivileges: { crud: canWriteTimeline }, + } = useUserPrivileges(); const [isImportDataModalOpen, setImportDataModal] = useState(false); const openImportModal = useCallback(() => { @@ -37,12 +38,12 @@ export const TimelinesPage = React.memo(() => { tabName === TimelineTypeEnum.default ? TimelineTypeEnum.default : TimelineTypeEnum.template; return ( - <> + {indicesExist ? ( - {capabilitiesCanUserCRUD && ( - + + {canWriteTimeline && ( { {i18n.ALL_TIMELINES_IMPORT_TIMELINE_TITLE} - - - - - )} + )} + + + + + { ) : ( )} - - - + ); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/store/middlewares/create_timeline_middlewares.ts b/x-pack/solutions/security/plugins/security_solution/public/timelines/store/middlewares/create_timeline_middlewares.ts index d473f509c688c..effb4b0819773 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/store/middlewares/create_timeline_middlewares.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/store/middlewares/create_timeline_middlewares.ts @@ -12,9 +12,11 @@ import { favoriteTimelineMiddleware } from './timeline_favorite'; import { addNoteToTimelineMiddleware } from './timeline_note'; import { addPinnedEventToTimelineMiddleware } from './timeline_pinned_event'; import { saveTimelineMiddleware } from './timeline_save'; +import { timelinePrivilegesMiddleware } from './timeline_privileges'; export function createTimelineMiddlewares(kibana: CoreStart) { return [ + timelinePrivilegesMiddleware(kibana), timelineChangedMiddleware, favoriteTimelineMiddleware(kibana), addNoteToTimelineMiddleware(kibana), diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/store/middlewares/timeline_changed.test.ts b/x-pack/solutions/security/plugins/security_solution/public/timelines/store/middlewares/timeline_changed.test.ts index ac8412f2fe8f5..d48715cc6be5b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/store/middlewares/timeline_changed.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/store/middlewares/timeline_changed.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { createMockStore } from '../../../common/mock'; +import { createMockStore, kibanaMock } from '../../../common/mock'; import { selectTimelineById } from '../selectors'; import { TimelineId } from '../../../../common/types/timeline'; @@ -79,10 +79,10 @@ const timelineChangedTypesCopy = [ const setChangedMock = setChanged as unknown as jest.Mock; describe('Timeline changed middleware', () => { - let store = createMockStore(); + let store = createMockStore(undefined, undefined, kibanaMock); beforeEach(() => { - store = createMockStore(); + store = createMockStore(undefined, undefined, kibanaMock); setChangedMock.mockClear(); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/store/middlewares/timeline_privileges.test.ts b/x-pack/solutions/security/plugins/security_solution/public/timelines/store/middlewares/timeline_privileges.test.ts new file mode 100644 index 0000000000000..96589c64104ed --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/store/middlewares/timeline_privileges.test.ts @@ -0,0 +1,63 @@ +/* + * 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 { createMockStore, kibanaMock } from '../../../common/mock'; +import { TimelineId } from '../../../../common/types/timeline'; + +import { showTimeline } from '../actions'; + +jest.mock('../actions', () => { + const actual = jest.requireActual('../actions'); + const showTL = jest.fn((...args) => actual.showTimeline(...args)); + (showTL as unknown as { match: Function }).match = () => false; + (showTL as unknown as { type: string }).type = actual.showTimeline.type; + return { + ...actual, + showTimeline: showTL, + }; +}); + +const showTimelineMock = showTimeline as unknown as jest.Mock; + +describe('Timeline privileges middleware', () => { + let store = createMockStore(undefined, undefined, kibanaMock); + + beforeEach(() => { + store = createMockStore(undefined, undefined, kibanaMock); + showTimelineMock.mockClear(); + }); + + it('should not show a toast when a timeline should be shown to a user with sufficient timeline privileges', async () => { + const addWarningMock = jest.spyOn(kibanaMock.notifications.toasts, 'addWarning'); + + await store.dispatch(showTimeline({ id: TimelineId.test, show: true })); + + expect(addWarningMock).not.toHaveBeenCalled(); + }); + + it('should show a toast when a timeline should be shown to a user with insufficient timeline privileges', async () => { + const addWarningMock = jest.spyOn(kibanaMock.notifications.toasts, 'addWarning'); + store = createMockStore(undefined, undefined, { + ...kibanaMock, + application: { + ...kibanaMock.application, + capabilities: { + ...kibanaMock.application.capabilities, + securitySolutionTimeline: { + read: false, + }, + }, + }, + }); + await store.dispatch(showTimeline({ id: TimelineId.test, show: true })); + + expect(addWarningMock).toHaveBeenCalled(); + expect(showTimelineMock).toHaveBeenCalledWith( + expect.objectContaining({ id: TimelineId.test, show: false }) + ); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/store/middlewares/timeline_privileges.ts b/x-pack/solutions/security/plugins/security_solution/public/timelines/store/middlewares/timeline_privileges.ts new file mode 100644 index 0000000000000..504f64a449f8c --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/store/middlewares/timeline_privileges.ts @@ -0,0 +1,44 @@ +/* + * 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 { Action, Middleware } from 'redux'; +import type { CoreStart } from '@kbn/core/public'; +import { i18n } from '@kbn/i18n'; + +import type { State } from '../../../common/store/types'; + +import { showTimeline } from '../actions'; +import { extractTimelineCapabilities } from '../../../common/utils/timeline_capabilities'; + +function isShowTimelineAction(action: Action): action is ReturnType { + return action.type === showTimeline.type; +} + +export const timelinePrivilegesMiddleware: (kibana: CoreStart) => Middleware<{}, State> = + (kibana: CoreStart) => (store) => (next) => async (action: Action) => { + const { read: hasAccessToTimeline } = extractTimelineCapabilities( + kibana.application.capabilities + ); + + if (isShowTimelineAction(action) && action.payload.show && !hasAccessToTimeline) { + kibana.notifications.toasts.addWarning({ + title: i18n.translate( + 'xpack.securitySolution.timeline.toast.insufficientPrivileges.title', + { + defaultMessage: 'Insufficient privileges (timeline)', + } + ), + text: i18n.translate('xpack.securitySolution.timeline.toast.insufficientPrivileges.text', { + defaultMessage: + 'You are trying to open a timeline but you do not have sufficient privileges. Please contact your administrator in order to get set up with the correct privileges for timeline.', + }), + }); + return next(showTimeline({ id: action.payload.id, show: false })); + } else { + return next(action); + } + }; diff --git a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/detections_engineer.ts b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/detections_engineer.ts index 51188f92a46cd..f2e0d5c72001d 100644 --- a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/detections_engineer.ts +++ b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/detections_engineer.ts @@ -17,7 +17,7 @@ export const getDetectionsEngineer: () => Omit = () => { ...noResponseActionsRole.kibana[0], feature: { ...noResponseActionsRole.kibana[0].feature, - siem: [ + siemV2: [ 'minimal_all', 'policy_management_read', @@ -29,6 +29,8 @@ export const getDetectionsEngineer: () => Omit = () => { 'actions_log_management_read', ], + securitySolutionTimeline: ['all'], + securitySolutionNotes: ['all'], }, }, ], diff --git a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/endpoint_operations_analyst.ts b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/endpoint_operations_analyst.ts index 168e88386f2d3..21bf6c7346c16 100644 --- a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/endpoint_operations_analyst.ts +++ b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/endpoint_operations_analyst.ts @@ -57,7 +57,7 @@ export const getEndpointOperationsAnalyst: () => Omit = () => { osquery: ['all'], securitySolutionCasesV2: ['all'], builtinAlerts: ['all'], - siem: [ + siemV2: [ 'all', 'read_alerts', 'policy_management_all', @@ -74,6 +74,8 @@ export const getEndpointOperationsAnalyst: () => Omit = () => { 'scan_operations_all', 'workflow_insights_all', ], + securitySolutionTimeline: ['all'], + securitySolutionNotes: ['all'], }, spaces: ['*'], }, diff --git a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/endpoint_security_policy_manager.ts b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/endpoint_security_policy_manager.ts index ed82f4026c1aa..6535f42154a20 100644 --- a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/endpoint_security_policy_manager.ts +++ b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/endpoint_security_policy_manager.ts @@ -17,7 +17,7 @@ export const getEndpointSecurityPolicyManager: () => Omit = () => ...noResponseActionsRole.kibana[0], feature: { ...noResponseActionsRole.kibana[0].feature, - siem: [ + siemV2: [ 'minimal_all', 'policy_management_all', @@ -29,6 +29,8 @@ export const getEndpointSecurityPolicyManager: () => Omit = () => 'workflow_insights_all', ], + securitySolutionTimeline: ['all'], + securitySolutionNotes: ['all'], }, }, ], @@ -44,7 +46,9 @@ export const getEndpointSecurityPolicyManagementReadRole: () => Omit Omit = () => { ...noResponseActionsRole.kibana[0], feature: { ...noResponseActionsRole.kibana[0].feature, - siem: [ + siemV2: [ 'minimal_all', 'policy_management_read', @@ -31,6 +31,8 @@ export const getHunter: () => Omit = () => { 'process_operations_all', 'actions_log_management_all', ], + securitySolutionTimeline: ['all'], + securitySolutionNotes: ['all'], }, }, ], diff --git a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/platform_engineer.ts b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/platform_engineer.ts index 397d1da1cf7b7..ff6c9aaa82933 100644 --- a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/platform_engineer.ts +++ b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/platform_engineer.ts @@ -17,7 +17,7 @@ export const getPlatformEngineer: () => Omit = () => { ...noResponseActionsRole.kibana[0], feature: { ...noResponseActionsRole.kibana[0].feature, - siem: [ + siemV2: [ 'minimal_all', 'policy_management_all', @@ -31,6 +31,8 @@ export const getPlatformEngineer: () => Omit = () => { 'workflow_insights_all', ], + securitySolutionTimeline: ['all'], + securitySolutionNotes: ['all'], }, }, ], diff --git a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/rule_author.ts b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/rule_author.ts index 63286dc4e925b..2c32c21a1d521 100644 --- a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/rule_author.ts +++ b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/rule_author.ts @@ -17,7 +17,7 @@ export const getRuleAuthor: () => Omit = () => { ...noResponseActionsRole.kibana[0], feature: { ...noResponseActionsRole.kibana[0].feature, - siem: [ + siemV2: [ 'all', 'read_alerts', 'crud_alerts', @@ -30,6 +30,8 @@ export const getRuleAuthor: () => Omit = () => { 'actions_log_management_read', 'workflow_insights_all', ], + securitySolutionTimeline: ['all'], + securitySolutionNotes: ['all'], }, }, ], diff --git a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/serverless/es_serverless_resources/roles.yml b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/serverless/es_serverless_resources/roles.yml index 3fe0d44073f3f..467a180d289a3 100644 --- a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/serverless/es_serverless_resources/roles.yml +++ b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/serverless/es_serverless_resources/roles.yml @@ -14,7 +14,7 @@ system_indices_superuser: #-------------------------------------------------------------------------------------------------- # # FILE SOURCE AT: -# https://github.com/elastic/project-controller/blob/main/internal/project/security/config/roles.yml +# https://github.com/elastic/elasticsearch-controller/blob/main/helm/values.yaml # # !!!! IMPORTANT !!!! DO NOT MAKE CHANGES TO THIS FILE, UNLESS THOSE CHANGES # HAVE ALSO BEEN MADE TO PROJECT CONTROLLER (path above) @@ -25,19 +25,19 @@ viewer: cluster: [] indices: - names: - - ".siem-signals*" - - ".lists-*" - - ".items-*" + - '.siem-signals*' + - '.lists-*' + - '.items-*' privileges: - - "read" - - "view_index_metadata" + - 'read' + - 'view_index_metadata' allow_restricted_indices: false - names: - - ".alerts*" - - ".preview.alerts*" + - '.alerts*' + - '.preview.alerts*' privileges: - - "read" - - "view_index_metadata" + - 'read' + - 'view_index_metadata' allow_restricted_indices: false - names: - apm-*-transaction* @@ -49,24 +49,26 @@ viewer: - packetbeat-* - winlogbeat-* - metrics-endpoint.metadata_current_* - - ".fleet-agents*" - - ".fleet-actions*" - - "risk-score.risk-score-*" - - ".asset-criticality.asset-criticality-*" - - ".entities.v1.latest.security_*" - - ".ml-anomalies-*" + - '.fleet-agents*' + - '.fleet-actions*' + - 'risk-score.risk-score-*' + - '.asset-criticality.asset-criticality-*' + - '.entities.v1.latest.security_*' + - '.ml-anomalies-*' privileges: - read applications: - - application: "kibana-.kibana" + - application: 'kibana-.kibana' privileges: - feature_ml.read - - feature_siem.read - - feature_siem.read_alerts - - feature_siem.endpoint_list_read + - feature_siemV2.read + - feature_siemV2.read_alerts + - feature_siemV2.endpoint_list_read - feature_securitySolutionCases.read - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.read + - feature_securitySolutionNotes.read - feature_actions.read - feature_builtInAlerts.read - feature_osquery.read @@ -76,7 +78,7 @@ viewer: - feature_graph.all - feature_maps.all - feature_visualize.all - resources: "*" + resources: '*' run_as: [] # modeled after t3_analyst @@ -84,14 +86,14 @@ editor: cluster: [] indices: - names: - - ".siem-signals*" - - ".lists-*" - - ".items-*" + - '.siem-signals*' + - '.lists-*' + - '.items-*' privileges: - - "read" - - "view_index_metadata" - - "write" - - "maintenance" + - 'read' + - 'view_index_metadata' + - 'write' + - 'maintenance' allow_restricted_indices: false - names: - apm-*-transaction* @@ -106,47 +108,49 @@ editor: - read - write - names: - - ".internal.alerts*" - - ".alerts*" - - ".internal.preview.alerts*" - - ".preview.alerts*" - - "risk-score.risk-score-*" + - '.internal.alerts*' + - '.alerts*' + - '.internal.preview.alerts*' + - '.preview.alerts*' + - 'risk-score.risk-score-*' privileges: - - "read" - - "view_index_metadata" - - "write" - - "maintenance" + - 'read' + - 'view_index_metadata' + - 'write' + - 'maintenance' - names: - - ".asset-criticality.asset-criticality-*" + - '.asset-criticality.asset-criticality-*' - .entities.v1.latest.security_* privileges: - - "read" - - "write" + - 'read' + - 'write' allow_restricted_indices: false - names: - - ".ml-anomalies-*" + - '.ml-anomalies-*' privileges: - read applications: - - application: "kibana-.kibana" + - application: 'kibana-.kibana' privileges: - feature_ml.read - - feature_siem.all - - feature_siem.read_alerts - - feature_siem.crud_alerts - - feature_siem.endpoint_list_all - - feature_siem.trusted_applications_all - - feature_siem.event_filters_all - - feature_siem.host_isolation_exceptions_all - - feature_siem.blocklist_all - - feature_siem.policy_management_read # Elastic Defend Policy Management - - feature_siem.host_isolation_all - - feature_siem.process_operations_all - - feature_siem.actions_log_management_all # Response actions history - - feature_siem.file_operations_all + - feature_siemV2.all + - feature_siemV2.read_alerts + - feature_siemV2.crud_alerts + - feature_siemV2.endpoint_list_all + - feature_siemV2.trusted_applications_all + - feature_siemV2.event_filters_all + - feature_siemV2.host_isolation_exceptions_all + - feature_siemV2.blocklist_all + - feature_siemV2.policy_management_read # Elastic Defend Policy Management + - feature_siemV2.host_isolation_all + - feature_siemV2.process_operations_all + - feature_siemV2.actions_log_management_all # Response actions history + - feature_siemV2.file_operations_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.all + - feature_securitySolutionNotes.all - feature_actions.read - feature_builtInAlerts.all - feature_osquery.all @@ -156,15 +160,15 @@ editor: - feature_graph.all - feature_maps.all - feature_visualize.all - resources: "*" + resources: '*' run_as: [] t1_analyst: cluster: indices: - names: - - ".alerts-security*" - - ".siem-signals-*" + - '.alerts-security*' + - '.siem-signals-*' privileges: - read - write @@ -179,24 +183,26 @@ t1_analyst: - packetbeat-* - winlogbeat-* - metrics-endpoint.metadata_current_* - - ".fleet-agents*" - - ".fleet-actions*" + - '.fleet-agents*' + - '.fleet-actions*' - risk-score.risk-score-* - .asset-criticality.asset-criticality-* - .entities.v1.latest.security_* - - ".ml-anomalies-*" + - '.ml-anomalies-*' privileges: - read applications: - - application: "kibana-.kibana" + - application: 'kibana-.kibana' privileges: - feature_ml.read - - feature_siem.read - - feature_siem.read_alerts - - feature_siem.endpoint_list_read + - feature_siemV2.read + - feature_siemV2.read_alerts + - feature_siemV2.endpoint_list_read - feature_securitySolutionCases.read - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.read + - feature_securitySolutionNotes.read - feature_actions.read - feature_builtInAlerts.read - feature_osquery.read @@ -207,7 +213,7 @@ t1_analyst: - feature_graph.all - feature_maps.all - feature_visualize.all - resources: "*" + resources: '*' t2_analyst: cluster: @@ -235,7 +241,7 @@ t2_analyst: - .fleet-actions* - risk-score.risk-score-* - .entities.v1.latest.security_* - - ".ml-anomalies-*" + - '.ml-anomalies-*' privileges: - read - names: @@ -244,15 +250,17 @@ t2_analyst: - read - write applications: - - application: "kibana-.kibana" + - application: 'kibana-.kibana' privileges: - feature_ml.read - - feature_siem.read - - feature_siem.read_alerts - - feature_siem.endpoint_list_read + - feature_siemV2.read + - feature_siemV2.read_alerts + - feature_siemV2.endpoint_list_read - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.read + - feature_securitySolutionNotes.read - feature_actions.read - feature_builtInAlerts.read - feature_osquery.read @@ -263,7 +271,7 @@ t2_analyst: - feature_graph.all - feature_maps.all - feature_visualize.all - resources: "*" + resources: '*' t3_analyst: cluster: @@ -300,31 +308,33 @@ t3_analyst: - .fleet-actions* - risk-score.risk-score-* - .entities.v1.latest.security_* - - ".ml-anomalies-*" + - '.ml-anomalies-*' privileges: - read applications: - - application: "kibana-.kibana" + - application: 'kibana-.kibana' privileges: - feature_ml.read - - feature_siem.all - - feature_siem.read_alerts - - feature_siem.crud_alerts - - feature_siem.endpoint_list_all - - feature_siem.trusted_applications_all - - feature_siem.event_filters_all - - feature_siem.host_isolation_exceptions_all - - feature_siem.blocklist_all - - feature_siem.policy_management_read # Elastic Defend Policy Management - - feature_siem.host_isolation_all - - feature_siem.process_operations_all - - feature_siem.actions_log_management_all # Response actions history - - feature_siem.file_operations_all - - feature_siem.scan_operations_all - - feature_siem.workflow_insights_all + - feature_siemV2.all + - feature_siemV2.read_alerts + - feature_siemV2.crud_alerts + - feature_siemV2.endpoint_list_all + - feature_siemV2.trusted_applications_all + - feature_siemV2.event_filters_all + - feature_siemV2.host_isolation_exceptions_all + - feature_siemV2.blocklist_all + - feature_siemV2.policy_management_read # Elastic Defend Policy Management + - feature_siemV2.host_isolation_all + - feature_siemV2.process_operations_all + - feature_siemV2.actions_log_management_all # Response actions history + - feature_siemV2.file_operations_all + - feature_siemV2.scan_operations_all + - feature_siemV2.workflow_insights_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.all + - feature_securitySolutionNotes.all - feature_actions.read - feature_builtInAlerts.all - feature_osquery.all @@ -334,7 +344,7 @@ t3_analyst: - feature_graph.all - feature_maps.all - feature_visualize.all - resources: "*" + resources: '*' threat_intelligence_analyst: cluster: @@ -370,19 +380,21 @@ threat_intelligence_analyst: - .fleet-actions* - risk-score.risk-score-* - .entities.v1.latest.security_* - - ".ml-anomalies-*" + - '.ml-anomalies-*' privileges: - read applications: - - application: "kibana-.kibana" + - application: 'kibana-.kibana' privileges: - feature_ml.read - - feature_siem.all - - feature_siem.endpoint_list_read - - feature_siem.blocklist_all + - feature_siemV2.all + - feature_siemV2.endpoint_list_read + - feature_siemV2.blocklist_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.all + - feature_securitySolutionNotes.all - feature_actions.read - feature_builtInAlerts.read - feature_osquery.all @@ -392,7 +404,7 @@ threat_intelligence_analyst: - feature_graph.all - feature_maps.all - feature_visualize.all - resources: "*" + resources: '*' rule_author: cluster: @@ -432,27 +444,29 @@ rule_author: - .fleet-actions* - risk-score.risk-score-* - .entities.v1.latest.security_* - - ".ml-anomalies-*" + - '.ml-anomalies-*' privileges: - read applications: - - application: "kibana-.kibana" + - application: 'kibana-.kibana' privileges: - feature_ml.read - - feature_siem.all - - feature_siem.read_alerts - - feature_siem.crud_alerts - - feature_siem.policy_management_all - - feature_siem.endpoint_list_all - - feature_siem.trusted_applications_all - - feature_siem.event_filters_all - - feature_siem.host_isolation_exceptions_read - - feature_siem.blocklist_all # Elastic Defend Policy Management - - feature_siem.actions_log_management_read - - feature_siem.workflow_insights_all + - feature_siemV2.all + - feature_siemV2.read_alerts + - feature_siemV2.crud_alerts + - feature_siemV2.policy_management_all + - feature_siemV2.endpoint_list_all + - feature_siemV2.trusted_applications_all + - feature_siemV2.event_filters_all + - feature_siemV2.host_isolation_exceptions_read + - feature_siemV2.blocklist_all # Elastic Defend Policy Management + - feature_siemV2.actions_log_management_read + - feature_siemV2.workflow_insights_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.all + - feature_securitySolutionNotes.all - feature_actions.read - feature_builtInAlerts.all - feature_osquery.all @@ -462,7 +476,7 @@ rule_author: - feature_graph.all - feature_maps.all - feature_visualize.all - resources: "*" + resources: '*' soc_manager: cluster: @@ -502,32 +516,34 @@ soc_manager: - .fleet-actions* - risk-score.risk-score-* - .asset-criticality.asset-criticality-* - - ".ml-anomalies-*" + - '.ml-anomalies-*' privileges: - read applications: - - application: "kibana-.kibana" + - application: 'kibana-.kibana' privileges: - feature_ml.read - - feature_siem.all - - feature_siem.read_alerts - - feature_siem.crud_alerts - - feature_siem.policy_management_all - - feature_siem.endpoint_list_all - - feature_siem.trusted_applications_all - - feature_siem.event_filters_all - - feature_siem.host_isolation_exceptions_all - - feature_siem.blocklist_all - - feature_siem.host_isolation_all - - feature_siem.process_operations_all - - feature_siem.actions_log_management_all - - feature_siem.file_operations_all - - feature_siem.execute_operations_all - - feature_siem.scan_operations_all - - feature_siem.workflow_insights_all + - feature_siemV2.all + - feature_siemV2.read_alerts + - feature_siemV2.crud_alerts + - feature_siemV2.policy_management_all + - feature_siemV2.endpoint_list_all + - feature_siemV2.trusted_applications_all + - feature_siemV2.event_filters_all + - feature_siemV2.host_isolation_exceptions_all + - feature_siemV2.blocklist_all + - feature_siemV2.host_isolation_all + - feature_siemV2.process_operations_all + - feature_siemV2.actions_log_management_all + - feature_siemV2.file_operations_all + - feature_siemV2.execute_operations_all + - feature_siemV2.scan_operations_all + - feature_siemV2.workflow_insights_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.all + - feature_securitySolutionNotes.all - feature_actions.all - feature_builtInAlerts.all - feature_osquery.all @@ -538,10 +554,10 @@ soc_manager: - feature_graph.all - feature_maps.all - feature_visualize.all - resources: "*" + resources: '*' detections_admin: - cluster: ["manage_index_templates", "manage_transform"] + cluster: ['manage_index_templates', 'manage_transform'] indices: - names: - apm-*-transaction* @@ -566,7 +582,7 @@ detections_admin: - metrics-endpoint.metadata_current_* - .fleet-agents* - .fleet-actions* - - ".ml-anomalies-*" + - '.ml-anomalies-*' privileges: - read - names: @@ -580,15 +596,17 @@ detections_admin: - read - write applications: - - application: "kibana-.kibana" + - application: 'kibana-.kibana' privileges: - feature_ml.all - - feature_siem.all - - feature_siem.read_alerts - - feature_siem.crud_alerts + - feature_siemV2.all + - feature_siemV2.read_alerts + - feature_siemV2.crud_alerts - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.all + - feature_securitySolutionNotes.all - feature_actions.all - feature_builtInAlerts.all - feature_dev_tools.all @@ -598,7 +616,7 @@ detections_admin: - feature_graph.all - feature_maps.all - feature_visualize.all - resources: "*" + resources: '*' platform_engineer: cluster: @@ -629,27 +647,29 @@ platform_engineer: - read - write - names: - - ".ml-anomalies-*" + - '.ml-anomalies-*' privileges: - read applications: - - application: "kibana-.kibana" + - application: 'kibana-.kibana' privileges: - feature_ml.all - - feature_siem.all - - feature_siem.read_alerts - - feature_siem.crud_alerts - - feature_siem.policy_management_all - - feature_siem.endpoint_list_all - - feature_siem.trusted_applications_all - - feature_siem.event_filters_all - - feature_siem.host_isolation_exceptions_all - - feature_siem.blocklist_all # Elastic Defend Policy Management - - feature_siem.actions_log_management_read - - feature_siem.workflow_insights_all + - feature_siemV2.all + - feature_siemV2.read_alerts + - feature_siemV2.crud_alerts + - feature_siemV2.policy_management_all + - feature_siemV2.endpoint_list_all + - feature_siemV2.trusted_applications_all + - feature_siemV2.event_filters_all + - feature_siemV2.host_isolation_exceptions_all + - feature_siemV2.blocklist_all # Elastic Defend Policy Management + - feature_siemV2.actions_log_management_read + - feature_siemV2.workflow_insights_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.all + - feature_securitySolutionNotes.all - feature_actions.all - feature_builtInAlerts.all - feature_fleet.all @@ -662,7 +682,7 @@ platform_engineer: - feature_graph.all - feature_maps.all - feature_visualize.all - resources: "*" + resources: '*' endpoint_operations_analyst: cluster: @@ -686,7 +706,7 @@ endpoint_operations_analyst: - .items* - risk-score.risk-score-* - .entities.v1.latest.security_* - - ".ml-anomalies-*" + - '.ml-anomalies-*' privileges: - read - names: @@ -704,27 +724,29 @@ endpoint_operations_analyst: - read - write applications: - - application: "kibana-.kibana" + - application: 'kibana-.kibana' privileges: - feature_ml.read - - feature_siem.all - - feature_siem.read_alerts - - feature_siem.policy_management_all - - feature_siem.endpoint_list_all - - feature_siem.trusted_applications_all - - feature_siem.event_filters_all - - feature_siem.host_isolation_exceptions_all - - feature_siem.blocklist_all - - feature_siem.host_isolation_all - - feature_siem.process_operations_all - - feature_siem.actions_log_management_all # Response History - - feature_siem.file_operations_all - - feature_siem.execute_operations_all # Execute - - feature_siem.scan_operations_all - - feature_siem.workflow_insights_all + - feature_siemV2.all + - feature_siemV2.read_alerts + - feature_siemV2.policy_management_all + - feature_siemV2.endpoint_list_all + - feature_siemV2.trusted_applications_all + - feature_siemV2.event_filters_all + - feature_siemV2.host_isolation_exceptions_all + - feature_siemV2.blocklist_all + - feature_siemV2.host_isolation_all + - feature_siemV2.process_operations_all + - feature_siemV2.actions_log_management_all # Response History + - feature_siemV2.file_operations_all + - feature_siemV2.execute_operations_all # Execute + - feature_siemV2.scan_operations_all + - feature_siemV2.workflow_insights_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.all + - feature_securitySolutionNotes.all - feature_actions.all - feature_builtInAlerts.all - feature_osquery.all @@ -736,7 +758,7 @@ endpoint_operations_analyst: - feature_graph.all - feature_maps.all - feature_visualize.all - resources: "*" + resources: '*' endpoint_policy_manager: cluster: @@ -758,7 +780,7 @@ endpoint_policy_manager: - winlogbeat-* - risk-score.risk-score-* - .entities.v1.latest.security_* - - ".ml-anomalies-*" + - '.ml-anomalies-*' privileges: - read - names: @@ -778,22 +800,24 @@ endpoint_policy_manager: - write - manage applications: - - application: "kibana-.kibana" + - application: 'kibana-.kibana' privileges: - feature_ml.all - - feature_siem.all - - feature_siem.read_alerts - - feature_siem.crud_alerts - - feature_siem.policy_management_all - - feature_siem.endpoint_list_all - - feature_siem.trusted_applications_all - - feature_siem.event_filters_all - - feature_siem.host_isolation_exceptions_all - - feature_siem.blocklist_all # Elastic Defend Policy Management - - feature_siem.workflow_insights_all + - feature_siemV2.all + - feature_siemV2.read_alerts + - feature_siemV2.crud_alerts + - feature_siemV2.policy_management_all + - feature_siemV2.endpoint_list_all + - feature_siemV2.trusted_applications_all + - feature_siemV2.event_filters_all + - feature_siemV2.host_isolation_exceptions_all + - feature_siemV2.blocklist_all # Elastic Defend Policy Management + - feature_siemV2.workflow_insights_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.all + - feature_securitySolutionNotes.all - feature_actions.all - feature_builtInAlerts.all - feature_osquery.all @@ -805,4 +829,4 @@ endpoint_policy_manager: - feature_graph.all - feature_maps.all - feature_visualize.all - resources: "*" + resources: '*' diff --git a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/soc_manager.ts b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/soc_manager.ts index 20cda46c70ef1..65d3327c8d000 100644 --- a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/soc_manager.ts +++ b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/soc_manager.ts @@ -17,7 +17,7 @@ export const getSocManager: () => Omit = () => { ...noResponseActionsRole.kibana[0], feature: { ...noResponseActionsRole.kibana[0].feature, - siem: [ + siemV2: [ 'minimal_all', 'policy_management_all', @@ -33,6 +33,8 @@ export const getSocManager: () => Omit = () => { 'workflow_insights_all', ], + securitySolutionTimeline: ['all'], + securitySolutionNotes: ['all'], }, }, ], diff --git a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/t1_analyst.ts b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/t1_analyst.ts index 4cc67fb69f4ea..5bdb7c3883f26 100644 --- a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/t1_analyst.ts +++ b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/t1_analyst.ts @@ -17,7 +17,9 @@ export const getT1Analyst: () => Omit = () => { ...noResponseActionsRole.kibana[0], feature: { ...noResponseActionsRole.kibana[0].feature, - siem: ['minimal_all'], + siemV2: ['minimal_all'], + securitySolutionTimeline: ['all'], + securitySolutionNotes: ['all'], }, }, ], diff --git a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/t2_analyst.ts b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/t2_analyst.ts index 32d3fac8ac680..d99ceba8014f3 100644 --- a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/t2_analyst.ts +++ b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/t2_analyst.ts @@ -17,7 +17,9 @@ export const getT2Analyst: () => Omit = () => { ...noResponseActionsRole.kibana[0], feature: { ...noResponseActionsRole.kibana[0].feature, - siem: ['minimal_all', 'actions_log_management_read'], + siemV2: ['minimal_all', 'actions_log_management_read'], + securitySolutionTimeline: ['all'], + securitySolutionNotes: ['all'], }, }, ], diff --git a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/t3_analyst.ts b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/t3_analyst.ts index 2ed145886eac8..b174994e04874 100644 --- a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/t3_analyst.ts +++ b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/t3_analyst.ts @@ -17,7 +17,7 @@ export const getT3Analyst: () => Omit = () => { ...noResponseActionsRole.kibana[0], feature: { ...noResponseActionsRole.kibana[0].feature, - siem: [ + siemV2: [ 'all', 'read_alerts', 'crud_alerts', @@ -34,6 +34,8 @@ export const getT3Analyst: () => Omit = () => { 'scan_operations_all', 'workflow_insights_all', ], + securitySolutionTimeline: ['all'], + securitySolutionNotes: ['all'], }, }, ], diff --git a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/threat_intelligence_analyst.ts b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/threat_intelligence_analyst.ts index 884b9412d06d6..3707cbfb61bfd 100644 --- a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/threat_intelligence_analyst.ts +++ b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/threat_intelligence_analyst.ts @@ -17,7 +17,9 @@ export const getThreatIntelligenceAnalyst: () => Omit = () => { ...noResponseActionsRole.kibana[0], feature: { ...noResponseActionsRole.kibana[0].feature, - siem: ['minimal_all', 'blocklist_all', 'actions_log_management_read'], + siemV2: ['minimal_all', 'blocklist_all', 'actions_log_management_read'], + securitySolutionTimeline: ['all'], + securitySolutionNotes: ['all'], }, }, ], diff --git a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/with_artifact_read_privileges_role.ts b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/with_artifact_read_privileges_role.ts index aa09afb0cb74e..5a168de59f5eb 100644 --- a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/with_artifact_read_privileges_role.ts +++ b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/with_artifact_read_privileges_role.ts @@ -17,13 +17,15 @@ export const getWithArtifactReadPrivilegesRole: () => Omit = () => ...noResponseActionsRole.kibana[0], feature: { ...noResponseActionsRole.kibana[0].feature, - siem: [ + siemV2: [ 'minimal_all', 'blocklist_read', 'trusted_applications_read', 'host_isolation_exceptions_read', 'event_filters_read', ], + securitySolutionTimeline: ['all'], + securitySolutionNotes: ['all'], }, }, ], diff --git a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/with_response_actions_role.ts b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/with_response_actions_role.ts index 7108f0181b868..a8a4bb31b2089 100644 --- a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/with_response_actions_role.ts +++ b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/with_response_actions_role.ts @@ -17,8 +17,8 @@ export const getWithResponseActionsRole: () => Omit = () => { ...noResponseActionsRole.kibana[0], feature: { ...noResponseActionsRole.kibana[0].feature, - siem: [ - ...noResponseActionsRole.kibana[0].feature.siem, + siemV2: [ + ...noResponseActionsRole.kibana[0].feature.siemV2, 'file_operations_all', 'execute_operations_all', 'scan_operations_all', @@ -27,6 +27,8 @@ export const getWithResponseActionsRole: () => Omit = () => { 'actions_log_management_all', 'actions_log_management_read', ], + securitySolutionTimeline: ['all'], + securitySolutionNotes: ['all'], }, }, ], diff --git a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/without_response_actions_role.ts b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/without_response_actions_role.ts index d57ca059de994..4e5b7aa28fff0 100644 --- a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/without_response_actions_role.ts +++ b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/without_response_actions_role.ts @@ -42,7 +42,7 @@ export const getNoResponseActionsRole: () => Omit = () => ({ osquery: ['all'], savedObjectsManagement: ['all'], savedObjectsTagging: ['all'], - siem: [ + siemV2: [ 'minimal_all', 'endpoint_list_all', 'endpoint_list_read', @@ -57,6 +57,8 @@ export const getNoResponseActionsRole: () => Omit = () => ({ 'policy_management_all', 'policy_management_read', ], + securitySolutionTimeline: ['all'], + securitySolutionNotes: ['all'], stackAlerts: ['all'], }, spaces: ['*'], diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/mocks.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/mocks.ts index 29df069020561..3b385c70e0b5f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/mocks.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/mocks.ts @@ -21,6 +21,11 @@ jest.mock('@kbn/security-solution-features/product_features', () => ({ baseKibanaSubFeatureIds: [], subFeaturesMap: new Map(), })), + getSecurityV2Feature: jest.fn(() => ({ + baseKibanaFeature: {}, + baseKibanaSubFeatureIds: [], + subFeaturesMap: new Map(), + })), getCasesFeature: jest.fn(() => ({ baseKibanaFeature: {}, baseKibanaSubFeatureIds: [], @@ -41,6 +46,16 @@ jest.mock('@kbn/security-solution-features/product_features', () => ({ baseKibanaSubFeatureIds: [], subFeaturesMap: new Map(), })), + getTimelineFeature: jest.fn(() => ({ + baseKibanaFeature: {}, + baseKibanaSubFeatureIds: [], + subFeaturesMap: new Map(), + })), + getNotesFeature: jest.fn(() => ({ + baseKibanaFeature: {}, + baseKibanaSubFeatureIds: [], + subFeaturesMap: new Map(), + })), })); export const createProductFeaturesServiceMock = ( @@ -132,6 +147,40 @@ export const createProductFeaturesServiceMock = ( ]) ) ), + timeline: jest.fn().mockReturnValue( + new Map( + enabledFeatureKeys.map((key) => [ + key, + { + privileges: { + all: { + ui: ['entity-analytics'], + }, + read: { + ui: ['entity-analytics'], + }, + }, + }, + ]) + ) + ), + notes: jest.fn().mockReturnValue( + new Map( + enabledFeatureKeys.map((key) => [ + key, + { + privileges: { + all: { + ui: ['entity-analytics'], + }, + read: { + ui: ['entity-analytics'], + }, + }, + }, + ]) + ) + ), }); } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_service.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_service.test.ts index 768228f319b24..b0c6254a139a6 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_service.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_service.test.ts @@ -46,6 +46,9 @@ jest.mock('@kbn/security-solution-features/product_features', () => ({ getCasesFeature: () => mockGetFeature(), getCasesV2Feature: () => mockGetFeature(), getSecurityFeature: () => mockGetFeature(), + getSecurityV2Feature: () => mockGetFeature(), + getTimelineFeature: () => mockGetFeature(), + getNotesFeature: () => mockGetFeature(), })); describe('ProductFeaturesService', () => { @@ -57,8 +60,8 @@ describe('ProductFeaturesService', () => { const experimentalFeatures = {} as ExperimentalFeatures; new ProductFeaturesService(loggerMock.create(), experimentalFeatures); - expect(mockGetFeature).toHaveBeenCalledTimes(5); - expect(MockedProductFeatures).toHaveBeenCalledTimes(5); + expect(mockGetFeature).toHaveBeenCalledTimes(8); + expect(MockedProductFeatures).toHaveBeenCalledTimes(8); }); it('should init all ProductFeatures when initialized', () => { @@ -90,12 +93,16 @@ describe('ProductFeaturesService', () => { const mockCasesConfig = new Map() as ProductFeaturesConfig; const mockAssistantConfig = new Map() as ProductFeaturesConfig; const mockAttackDiscoveryConfig = new Map() as ProductFeaturesConfig; + const mockTimelineConfig = new Map() as ProductFeaturesConfig; + const mockNotesConfig = new Map() as ProductFeaturesConfig; const configurator: ProductFeaturesConfigurator = { attackDiscovery: jest.fn(() => mockAttackDiscoveryConfig), security: jest.fn(() => mockSecurityConfig), cases: jest.fn(() => mockCasesConfig), securityAssistant: jest.fn(() => mockAssistantConfig), + timeline: jest.fn(() => mockTimelineConfig), + notes: jest.fn(() => mockNotesConfig), }; productFeaturesService.setProductFeaturesConfigurator(configurator); @@ -139,12 +146,16 @@ describe('ProductFeaturesService', () => { const mockAttackDiscoveryConfig = new Map([ [ProductFeatureKey.attackDiscovery, {}], ]) as ProductFeaturesConfig; + const mockTimelineConfig = new Map([[ProductFeatureKey.timeline, {}]]) as ProductFeaturesConfig; + const mockNotesConfig = new Map([[ProductFeatureKey.notes, {}]]) as ProductFeaturesConfig; const configurator: ProductFeaturesConfigurator = { attackDiscovery: jest.fn(() => mockAttackDiscoveryConfig), security: jest.fn(() => mockSecurityConfig), cases: jest.fn(() => mockCasesConfig), securityAssistant: jest.fn(() => mockAssistantConfig), + timeline: jest.fn(() => mockTimelineConfig), + notes: jest.fn(() => mockNotesConfig), }; productFeaturesService.setProductFeaturesConfigurator(configurator); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_service.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_service.ts index 2901734527a93..edcf0faa19659 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_service.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_service.ts @@ -21,13 +21,21 @@ import { getCasesFeature, getSecurityFeature, getCasesV2Feature, + getSecurityV2Feature, + getTimelineFeature, + getNotesFeature, } from '@kbn/security-solution-features/product_features'; import type { RecursiveReadonly } from '@kbn/utility-types'; import type { ExperimentalFeatures } from '../../../common'; import { APP_ID } from '../../../common'; import { ProductFeatures } from './product_features'; import type { ProductFeaturesConfigurator } from './types'; -import { securityDefaultSavedObjects } from './security_saved_objects'; +import { + securityDefaultSavedObjects, + securityNotesSavedObjects, + securityTimelineSavedObjects, + securityV1SavedObjects, +} from './security_saved_objects'; import { casesApiTags, casesUiCapabilities } from './cases_privileges'; // The prefix ("securitySolution-") used by all the Security Solution API action privileges. @@ -35,10 +43,13 @@ export const API_ACTION_PREFIX = `${APP_ID}-`; export class ProductFeaturesService { private securityProductFeatures: ProductFeatures; + private securityV2ProductFeatures: ProductFeatures; private casesProductFeatures: ProductFeatures; private casesProductV2Features: ProductFeatures; private securityAssistantProductFeatures: ProductFeatures; private attackDiscoveryProductFeatures: ProductFeatures; + private timelineProductFeatures: ProductFeatures; + private notesProductFeatures: ProductFeatures; private productFeatures?: Set; constructor( @@ -46,7 +57,7 @@ export class ProductFeaturesService { private readonly experimentalFeatures: ExperimentalFeatures ) { const securityFeature = getSecurityFeature({ - savedObjects: securityDefaultSavedObjects, + savedObjects: securityV1SavedObjects, experimentalFeatures: this.experimentalFeatures, }); this.securityProductFeatures = new ProductFeatures( @@ -55,6 +66,16 @@ export class ProductFeaturesService { securityFeature.baseKibanaFeature, securityFeature.baseKibanaSubFeatureIds ); + const securityV2Feature = getSecurityV2Feature({ + savedObjects: securityDefaultSavedObjects, + experimentalFeatures: this.experimentalFeatures, + }); + this.securityV2ProductFeatures = new ProductFeatures( + this.logger, + securityV2Feature.subFeaturesMap, + securityV2Feature.baseKibanaFeature, + securityV2Feature.baseKibanaSubFeatureIds + ); const casesFeature = getCasesFeature({ uiCapabilities: casesUiCapabilities, @@ -91,25 +112,52 @@ export class ProductFeaturesService { ); const attackDiscoveryFeature = getAttackDiscoveryFeature(); + this.attackDiscoveryProductFeatures = new ProductFeatures( this.logger, attackDiscoveryFeature.subFeaturesMap, attackDiscoveryFeature.baseKibanaFeature, attackDiscoveryFeature.baseKibanaSubFeatureIds ); + + const timelineFeature = getTimelineFeature({ + savedObjects: securityTimelineSavedObjects, + experimentalFeatures: {}, + }); + this.timelineProductFeatures = new ProductFeatures( + this.logger, + timelineFeature.subFeaturesMap, + timelineFeature.baseKibanaFeature, + timelineFeature.baseKibanaSubFeatureIds + ); + + const notesFeature = getNotesFeature({ + savedObjects: securityNotesSavedObjects, + experimentalFeatures: {}, + }); + this.notesProductFeatures = new ProductFeatures( + this.logger, + notesFeature.subFeaturesMap, + notesFeature.baseKibanaFeature, + notesFeature.baseKibanaSubFeatureIds + ); } public init(featuresSetup: FeaturesPluginSetup) { this.securityProductFeatures.init(featuresSetup); + this.securityV2ProductFeatures.init(featuresSetup); this.casesProductFeatures.init(featuresSetup); this.casesProductV2Features.init(featuresSetup); this.securityAssistantProductFeatures.init(featuresSetup); this.attackDiscoveryProductFeatures.init(featuresSetup); + this.timelineProductFeatures.init(featuresSetup); + this.notesProductFeatures.init(featuresSetup); } public setProductFeaturesConfigurator(configurator: ProductFeaturesConfigurator) { const securityProductFeaturesConfig = configurator.security(); this.securityProductFeatures.setConfig(securityProductFeaturesConfig); + this.securityV2ProductFeatures.setConfig(securityProductFeaturesConfig); const casesProductFeaturesConfig = configurator.cases(); this.casesProductFeatures.setConfig(casesProductFeaturesConfig); @@ -121,12 +169,20 @@ export class ProductFeaturesService { const attackDiscoveryProductFeaturesConfig = configurator.attackDiscovery(); this.attackDiscoveryProductFeatures.setConfig(attackDiscoveryProductFeaturesConfig); + const timelineProductFeaturesConfig = configurator.timeline(); + this.timelineProductFeatures.setConfig(timelineProductFeaturesConfig); + + const notesProductFeaturesConfig = configurator.notes(); + this.notesProductFeatures.setConfig(notesProductFeaturesConfig); + this.productFeatures = new Set( Object.freeze([ ...securityProductFeaturesConfig.keys(), ...casesProductFeaturesConfig.keys(), ...securityAssistantProductFeaturesConfig.keys(), ...attackDiscoveryProductFeaturesConfig.keys(), + ...timelineProductFeaturesConfig.keys(), + ...notesProductFeaturesConfig.keys(), ]) as readonly ProductFeatureKeyType[] ); } @@ -141,10 +197,13 @@ export class ProductFeaturesService { public isActionRegistered(action: string) { return ( this.securityProductFeatures.isActionRegistered(action) || + this.securityV2ProductFeatures.isActionRegistered(action) || this.casesProductFeatures.isActionRegistered(action) || this.casesProductV2Features.isActionRegistered(action) || this.securityAssistantProductFeatures.isActionRegistered(action) || - this.attackDiscoveryProductFeatures.isActionRegistered(action) + this.attackDiscoveryProductFeatures.isActionRegistered(action) || + this.timelineProductFeatures.isActionRegistered(action) || + this.notesProductFeatures.isActionRegistered(action) ); } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/security_saved_objects.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/security_saved_objects.ts index b739d44db3e4f..369f1a55e9a5f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/security_saved_objects.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/security_saved_objects.ts @@ -7,7 +7,12 @@ import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/common'; import { EXCEPTION_LIST_NAMESPACE_AGNOSTIC } from '@kbn/securitysolution-list-constants'; -import { savedObjectTypes } from '../../saved_objects'; +import { + savedObjectTypesWithoutTimelineAndWithoutNotes, + timelineSavedObjectTypes, + notesSavedObjectTypes, + savedObjectTypes, +} from '../../saved_objects'; // Same as the saved-object type for rules defined by Cloud Security Posture const CLOUD_POSTURE_SAVED_OBJECT_RULE_TYPE = 'csp_rule'; @@ -19,8 +24,14 @@ export const securityDefaultSavedObjects = [ 'exception-list', EXCEPTION_LIST_NAMESPACE_AGNOSTIC, DATA_VIEW_SAVED_OBJECT_TYPE, - ...savedObjectTypes, + ...savedObjectTypesWithoutTimelineAndWithoutNotes, CLOUD_POSTURE_SAVED_OBJECT_RULE_TYPE, CLOUD_SECURITY_POSTURE_SETTINGS, CLOUD_SECURITY_POSTURE_BENCHMARK_RULE_TEMPLATE, ]; + +export const securityV1SavedObjects = [...securityDefaultSavedObjects, ...savedObjectTypes]; + +export const securityTimelineSavedObjects = timelineSavedObjectTypes; + +export const securityNotesSavedObjects = notesSavedObjectTypes; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/types.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/types.ts index 9c7b20cfba960..4bc7956c7861f 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/types.ts @@ -17,4 +17,6 @@ export interface ProductFeaturesConfigurator { security: () => ProductFeaturesConfig; cases: () => ProductFeaturesConfig; securityAssistant: () => ProductFeaturesConfig; + timeline: () => ProductFeaturesConfig; + notes: () => ProductFeaturesConfig; } diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/draft_timelines/clean_draft_timelines/index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/draft_timelines/clean_draft_timelines/index.ts index 639a5a2ae0e8c..29b587cc5bae0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/draft_timelines/clean_draft_timelines/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/draft_timelines/clean_draft_timelines/index.ts @@ -33,7 +33,7 @@ export const cleanDraftTimelinesRoute = (router: SecuritySolutionPluginRouter) = path: TIMELINE_DRAFT_URL, security: { authz: { - requiredPrivileges: ['securitySolution'], + requiredPrivileges: ['timeline_write'], }, }, access: 'public', diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/draft_timelines/get_draft_timelines/index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/draft_timelines/get_draft_timelines/index.ts index 37db10f5393bc..bee539d2893d8 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/draft_timelines/get_draft_timelines/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/draft_timelines/get_draft_timelines/index.ts @@ -26,7 +26,7 @@ export const getDraftTimelinesRoute = (router: SecuritySolutionPluginRouter) => path: TIMELINE_DRAFT_URL, security: { authz: { - requiredPrivileges: ['securitySolution'], + requiredPrivileges: ['timeline_read'], }, }, access: 'public', diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/notes/delete_note.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/notes/delete_note.ts index c2bc36e18c357..620972f9cc31b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/notes/delete_note.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/notes/delete_note.ts @@ -24,7 +24,7 @@ export const deleteNoteRoute = (router: SecuritySolutionPluginRouter) => { path: NOTE_URL, security: { authz: { - requiredPrivileges: ['securitySolution'], + requiredPrivileges: ['notes_write'], }, }, access: 'public', diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.ts index 49075a1b91155..a5110c8dc73b8 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.ts @@ -39,7 +39,7 @@ export const getNotesRoute = ( path: NOTE_URL, security: { authz: { - requiredPrivileges: ['securitySolution'], + requiredPrivileges: ['notes_read'], }, }, access: 'public', diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/notes/persist_note.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/notes/persist_note.ts index 12fed18a5c396..939952f685783 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/notes/persist_note.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/notes/persist_note.ts @@ -27,7 +27,7 @@ export const persistNoteRoute = (router: SecuritySolutionPluginRouter) => { path: NOTE_URL, security: { authz: { - requiredPrivileges: ['securitySolution'], + requiredPrivileges: ['notes_write'], }, }, access: 'public', diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/pinned_events/persist_pinned_event.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/pinned_events/persist_pinned_event.ts index 7dd66f86245ab..7a2adb7b94db7 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/pinned_events/persist_pinned_event.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/pinned_events/persist_pinned_event.ts @@ -28,7 +28,7 @@ export const persistPinnedEventRoute = (router: SecuritySolutionPluginRouter) => path: PINNED_EVENT_URL, security: { authz: { - requiredPrivileges: ['securitySolution'], + requiredPrivileges: ['timeline_write'], }, }, access: 'public', diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.ts index 99c4d95942f15..af1fefa80309c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.ts @@ -36,7 +36,7 @@ export const installPrepackedTimelinesRoute = ( path: `${TIMELINE_PREPACKAGED_URL}`, security: { authz: { - requiredPrivileges: ['securitySolution'], + requiredPrivileges: ['timeline_write'], }, }, options: { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/copy_timeline/index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/copy_timeline/index.ts index 2241de72b3307..736eb00a6b1bf 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/copy_timeline/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/copy_timeline/index.ts @@ -25,7 +25,7 @@ export const copyTimelineRoute = (router: SecuritySolutionPluginRouter) => { path: TIMELINE_COPY_URL, security: { authz: { - requiredPrivileges: ['securitySolution'], + requiredPrivileges: ['timeline_write'], }, }, access: 'internal', diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/index.ts index e6af5abd78425..bd11a7571b048 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/index.ts @@ -34,7 +34,7 @@ export const createTimelinesRoute = (router: SecuritySolutionPluginRouter) => { path: TIMELINE_URL, security: { authz: { - requiredPrivileges: ['securitySolution'], + requiredPrivileges: ['timeline_write'], }, }, access: 'public', diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.ts index 5a055d54a76ce..351a9015a636a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.ts @@ -28,7 +28,7 @@ export const exportTimelinesRoute = (router: SecuritySolutionPluginRouter, confi path: TIMELINE_EXPORT_URL, security: { authz: { - requiredPrivileges: ['securitySolution'], + requiredPrivileges: ['timeline_read'], }, }, access: 'public', diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/get_timeline/index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/get_timeline/index.ts index 75d61987e775b..3849e7411dcd0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/get_timeline/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/get_timeline/index.ts @@ -27,7 +27,7 @@ export const getTimelineRoute = (router: SecuritySolutionPluginRouter) => { path: TIMELINE_URL, security: { authz: { - requiredPrivileges: ['securitySolution'], + requiredPrivileges: ['timeline_read'], }, }, access: 'public', diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/get_timelines/index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/get_timelines/index.ts index 2ebcb4c38c37e..77f4937f13f8d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/get_timelines/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/get_timelines/index.ts @@ -27,7 +27,7 @@ export const getTimelinesRoute = (router: SecuritySolutionPluginRouter) => { path: TIMELINES_URL, security: { authz: { - requiredPrivileges: ['securitySolution'], + requiredPrivileges: ['timeline_read'], }, }, access: 'public', diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.ts index 3e40a7a7ebcb0..b424c1e9a60ab 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.ts @@ -34,7 +34,7 @@ export const importTimelinesRoute = (router: SecuritySolutionPluginRouter, confi path: `${TIMELINE_IMPORT_URL}`, security: { authz: { - requiredPrivileges: ['securitySolution'], + requiredPrivileges: ['timeline_write'], }, }, options: { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/patch_timelines/index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/patch_timelines/index.ts index 340b8611901e5..cec181212f461 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/patch_timelines/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/patch_timelines/index.ts @@ -28,7 +28,7 @@ export const patchTimelinesRoute = (router: SecuritySolutionPluginRouter) => { path: TIMELINE_URL, security: { authz: { - requiredPrivileges: ['securitySolution'], + requiredPrivileges: ['timeline_write'], }, }, access: 'public', diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/persist_favorite/index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/persist_favorite/index.ts index ed3531d8bd744..31d80eebcf9aa 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/persist_favorite/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/persist_favorite/index.ts @@ -28,7 +28,7 @@ export const persistFavoriteRoute = (router: SecuritySolutionPluginRouter) => { path: TIMELINE_FAVORITE_URL, security: { authz: { - requiredPrivileges: ['securitySolution'], + requiredPrivileges: ['timeline_read'], }, }, access: 'public', diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/resolve_timeline/index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/resolve_timeline/index.ts index 8e6ff9e8adca7..26369b34c63f5 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/resolve_timeline/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/timeline/routes/timelines/resolve_timeline/index.ts @@ -28,7 +28,7 @@ export const resolveTimelineRoute = (router: SecuritySolutionPluginRouter) => { path: TIMELINE_RESOLVE_URL, security: { authz: { - requiredPrivileges: ['securitySolution'], + requiredPrivileges: ['timeline_read'], }, }, access: 'public', diff --git a/x-pack/solutions/security/plugins/security_solution/server/saved_objects.ts b/x-pack/solutions/security/plugins/security_solution/server/saved_objects.ts index 9412e62e6315c..aaee168bf5ad6 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/saved_objects.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/saved_objects.ts @@ -33,6 +33,21 @@ const types = [ export const savedObjectTypes = types.map((type) => type.name); +export const savedObjectTypesWithoutTimelineAndWithoutNotes = savedObjectTypes.filter((type) => { + switch (type) { + case noteType.name: + case pinnedEventType.name: + case timelineType.name: + return false; + default: + return true; + } +}); + +export const timelineSavedObjectTypes = [timelineType.name, pinnedEventType.name]; + +export const notesSavedObjectTypes = [noteType.name]; + export const initSavedObjects = (savedObjects: CoreSetup['savedObjects']) => { types.forEach((type) => savedObjects.registerType(type)); }; diff --git a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/index.ts b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/index.ts index ed85c32f12284..a29ea4b9e2833 100644 --- a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/index.ts +++ b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/index.ts @@ -11,6 +11,8 @@ import { getCasesProductFeaturesConfigurator } from './cases_product_features_co import { getSecurityProductFeaturesConfigurator } from './security_product_features_config'; import { getSecurityAssistantProductFeaturesConfigurator } from './assistant_product_features_config'; import { getAttackDiscoveryProductFeaturesConfigurator } from './attack_discovery_product_features_config'; +import { getTimelineProductFeaturesConfigurator } from './timeline_product_features_config'; +import { getNotesProductFeaturesConfigurator } from './notes_product_features_config'; export const getProductProductFeaturesConfigurator = ( enabledProductFeatureKeys: ProductFeatureKeys @@ -20,5 +22,7 @@ export const getProductProductFeaturesConfigurator = ( security: getSecurityProductFeaturesConfigurator(enabledProductFeatureKeys), cases: getCasesProductFeaturesConfigurator(enabledProductFeatureKeys), securityAssistant: getSecurityAssistantProductFeaturesConfigurator(enabledProductFeatureKeys), + timeline: getTimelineProductFeaturesConfigurator(enabledProductFeatureKeys), + notes: getNotesProductFeaturesConfigurator(enabledProductFeatureKeys), }; }; diff --git a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/notes_product_features_config.ts b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/notes_product_features_config.ts new file mode 100644 index 0000000000000..0b9f1e143044a --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/notes_product_features_config.ts @@ -0,0 +1,37 @@ +/* + * 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, + ProductFeaturesNotesConfig, +} from '@kbn/security-solution-features'; +import { + notesDefaultProductFeaturesConfig, + createEnabledProductFeaturesConfigMap, +} from '@kbn/security-solution-features/config'; +import type { ProductFeatureNotesFeatureKey } 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 notesProductFeaturesConfig: Record< + ProductFeatureNotesFeatureKey, + ProductFeatureKibanaConfig +> = { + ...notesDefaultProductFeaturesConfig, + // ess-specific app features configs here +}; + +export const getNotesProductFeaturesConfigurator = + (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesNotesConfig => + createEnabledProductFeaturesConfigMap(notesProductFeaturesConfig, enabledProductFeatureKeys); diff --git a/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/timeline_product_features_config.ts b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/timeline_product_features_config.ts new file mode 100644 index 0000000000000..411c06ecfe75f --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution_ess/server/product_features/timeline_product_features_config.ts @@ -0,0 +1,38 @@ +/* + * 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, + ProductFeaturesTimelineConfig, +} from '@kbn/security-solution-features'; +import { + createEnabledProductFeaturesConfigMap, + timelineDefaultProductFeaturesConfig, +} from '@kbn/security-solution-features/config'; +import type { ProductFeatureTimelineFeatureKey } 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 timelineProductFeaturesConfig: Record< + ProductFeatureTimelineFeatureKey, + ProductFeatureKibanaConfig +> = { + ...timelineDefaultProductFeaturesConfig, + // ess-specific app features configs here +}; + +export const getTimelineProductFeaturesConfigurator = + (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesTimelineConfig => + createEnabledProductFeaturesConfigMap(timelineProductFeaturesConfig, enabledProductFeatureKeys); diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/index.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/index.ts index 310ea860787ba..176003cfda14a 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/index.ts +++ b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/index.ts @@ -13,6 +13,8 @@ import { getAttackDiscoveryProductFeaturesConfigurator } from './attack_discover import { getCasesProductFeaturesConfigurator } from './cases_product_features_config'; import { getSecurityProductFeaturesConfigurator } from './security_product_features_config'; import { getSecurityAssistantProductFeaturesConfigurator } from './assistant_product_features_config'; +import { getTimelineProductFeaturesConfigurator } from './timeline_product_features_config'; +import { getNotesProductFeaturesConfigurator } from './notes_product_features_config'; import { enableRuleActions } from '../rules/enable_rule_actions'; import type { ServerlessSecurityConfig } from '../config'; import type { Tier, SecuritySolutionServerlessPluginSetupDeps } from '../types'; @@ -40,6 +42,8 @@ export const registerProductFeatures = ( ), cases: getCasesProductFeaturesConfigurator(enabledProductFeatureKeys), securityAssistant: getSecurityAssistantProductFeaturesConfigurator(enabledProductFeatureKeys), + timeline: getTimelineProductFeaturesConfigurator(enabledProductFeatureKeys), + notes: getNotesProductFeaturesConfigurator(enabledProductFeatureKeys), }); // enable rule actions based on the enabled product features diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/notes_product_features_config.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/notes_product_features_config.ts new file mode 100644 index 0000000000000..1b00761f6da4a --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/notes_product_features_config.ts @@ -0,0 +1,37 @@ +/* + * 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, + ProductFeaturesNotesConfig, +} from '@kbn/security-solution-features'; +import { + notesDefaultProductFeaturesConfig, + createEnabledProductFeaturesConfigMap, +} from '@kbn/security-solution-features/config'; +import type { ProductFeatureNotesFeatureKey } 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 notesProductFeaturesConfig: Record< + ProductFeatureNotesFeatureKey, + ProductFeatureKibanaConfig +> = { + ...notesDefaultProductFeaturesConfig, + // serverless-specific app features configs here +}; + +export const getNotesProductFeaturesConfigurator = + (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesNotesConfig => + createEnabledProductFeaturesConfigMap(notesProductFeaturesConfig, enabledProductFeatureKeys); diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/timeline_product_features_config.ts b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/timeline_product_features_config.ts new file mode 100644 index 0000000000000..fde220b83cb30 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution_serverless/server/product_features/timeline_product_features_config.ts @@ -0,0 +1,37 @@ +/* + * 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, + ProductFeaturesTimelineConfig, +} from '@kbn/security-solution-features'; +import { + timelineDefaultProductFeaturesConfig, + createEnabledProductFeaturesConfigMap, +} from '@kbn/security-solution-features/config'; +import type { ProductFeatureTimelineFeatureKey } 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 timelineProductFeaturesConfig: Record< + ProductFeatureTimelineFeatureKey, + ProductFeatureKibanaConfig +> = { + ...timelineDefaultProductFeaturesConfig, + // serverless-specific app features configs here +}; + +export const getTimelineProductFeaturesConfigurator = + (enabledProductFeatureKeys: ProductFeatureKeys) => (): ProductFeaturesTimelineConfig => + createEnabledProductFeaturesConfigMap(timelineProductFeaturesConfig, enabledProductFeatureKeys); diff --git a/x-pack/solutions/security/plugins/threat_intelligence/public/mocks/mock_security_context.tsx b/x-pack/solutions/security/plugins/threat_intelligence/public/mocks/mock_security_context.tsx index 4e1ecf0bbe885..14de05525b848 100644 --- a/x-pack/solutions/security/plugins/threat_intelligence/public/mocks/mock_security_context.tsx +++ b/x-pack/solutions/security/plugins/threat_intelligence/public/mocks/mock_security_context.tsx @@ -43,6 +43,7 @@ export const getSecuritySolutionContextMock = (): SecuritySolutionPluginContext ({ dataProviders, from, to }) => () => new Promise((resolve) => window.alert('investigate in timeline')), + hasAccessToTimeline: true, SiemSearchBar: () =>
mock siem search
, diff --git a/x-pack/solutions/security/plugins/threat_intelligence/public/mocks/test_providers.tsx b/x-pack/solutions/security/plugins/threat_intelligence/public/mocks/test_providers.tsx index c56ebb122129a..29f48c0e15df7 100644 --- a/x-pack/solutions/security/plugins/threat_intelligence/public/mocks/test_providers.tsx +++ b/x-pack/solutions/security/plugins/threat_intelligence/public/mocks/test_providers.tsx @@ -138,13 +138,26 @@ export const mockedServices = { }, }; -export const TestProvidersComponent: FC> = ({ children }) => ( +interface TestProvidersProps { + securityContextOverrides?: Partial; +} + +export const TestProvidersComponent: FC> = ({ + children, + securityContextOverrides, +}) => ( - + diff --git a/x-pack/solutions/security/plugins/threat_intelligence/public/modules/indicators/components/table/table.tsx b/x-pack/solutions/security/plugins/threat_intelligence/public/modules/indicators/components/table/table.tsx index 2fc5c93739d4d..b4578d4423478 100644 --- a/x-pack/solutions/security/plugins/threat_intelligence/public/modules/indicators/components/table/table.tsx +++ b/x-pack/solutions/security/plugins/threat_intelligence/public/modules/indicators/components/table/table.tsx @@ -36,6 +36,9 @@ import { useFieldTypes } from '../../../../hooks/use_field_types'; import { getFieldSchema } from '../../utils/get_field_schema'; import { Pagination } from '../../services/fetch_indicators'; import { TABLE_TEST_ID, TABLE_UPDATE_PROGRESS_TEST_ID } from './test_ids'; +import { useSecurityContext } from '../../../../hooks/use_security_context'; + +const actionsColumnIconWidth = 28; export interface IndicatorsTableProps { indicators: Indicator[]; @@ -70,6 +73,8 @@ export const IndicatorsTable: VFC = ({ browserFields, columnSettings: { columns, columnVisibility, handleResetColumns, handleToggleColumn, sorting }, }) => { + const securitySolutionContext = useSecurityContext(); + const [expanded, setExpanded] = useState(); const fieldTypes = useFieldTypes(); @@ -96,7 +101,9 @@ export const IndicatorsTable: VFC = ({ () => [ { id: 'Actions', - width: 84, + width: securitySolutionContext?.hasAccessToTimeline + ? 3 * actionsColumnIconWidth + : 2 * actionsColumnIconWidth, headerCellRender: () => ( = ({ rowCellRender: renderCellValue, }, ], - [renderCellValue] + [renderCellValue, securitySolutionContext?.hasAccessToTimeline] ); const mappedColumns = useMemo( diff --git a/x-pack/solutions/security/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline.test.tsx b/x-pack/solutions/security/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline.test.tsx index a5d8ae3e3996d..2372c8803b08a 100644 --- a/x-pack/solutions/security/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline.test.tsx +++ b/x-pack/solutions/security/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline.test.tsx @@ -153,4 +153,17 @@ describe(' ', () => { ); expect(container).toBeEmptyDOMElement(); }); + + it('should render empty when the user does not have access to timeline', () => { + const mockField: string = 'threat.indicator.ip'; + const mockData: Indicator = generateMockIndicator(); + + const { container } = render( + + + + ); + + expect(container).toBeEmptyDOMElement(); + }); }); diff --git a/x-pack/solutions/security/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline.tsx b/x-pack/solutions/security/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline.tsx index 214a6472bb600..b964273118d04 100644 --- a/x-pack/solutions/security/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline.tsx +++ b/x-pack/solutions/security/plugins/threat_intelligence/public/modules/timeline/components/add_to_timeline.tsx @@ -19,6 +19,7 @@ import { generateDataProvider } from '../utils/data_provider'; import { fieldAndValueValid, getIndicatorFieldAndValue } from '../../indicators/utils/field_value'; import { Indicator } from '../../../../common/types/indicator'; import { useKibana } from '../../../hooks/use_kibana'; +import { useSecurityContext } from '../../../hooks/use_security_context'; import { useStyles } from './styles'; import { useAddToTimeline } from '../hooks/use_add_to_timeline'; import { TITLE } from './translations'; @@ -62,9 +63,10 @@ export const AddToTimelineButtonIcon: VFC = ({ }) => { const addToTimelineButton = useKibana().services.timelines.getHoverActions().getAddToTimelineButton; + const securitySolutionContext = useSecurityContext(); const { addToTimelineProps } = useAddToTimeline({ indicator: data, field }); - if (!addToTimelineProps) { + if (!securitySolutionContext?.hasAccessToTimeline || !addToTimelineProps) { return null; } @@ -91,7 +93,7 @@ export const AddToTimelineButtonEmpty: VFC = ({ 'data-test-subj': dataTestSubj, }) => { const styles = useStyles(); - + const securitySolutionContext = useSecurityContext(); const buttonRef = useRef(null); const { timelines, analytics, i18n: i18nStart, theme } = useKibana().services; @@ -101,7 +103,7 @@ export const AddToTimelineButtonEmpty: VFC = ({ const { key, value } = typeof data === 'string' ? { key: field, value: data } : getIndicatorFieldAndValue(data, field); - if (!fieldAndValueValid(key, value)) { + if (!securitySolutionContext?.hasAccessToTimeline || !fieldAndValueValid(key, value)) { return null; } @@ -152,7 +154,7 @@ export const AddToTimelineContextMenu: VFC = ({ 'data-test-subj': dataTestSubj, }) => { const styles = useStyles(); - + const securitySolutionContext = useSecurityContext(); const contextMenuRef = useRef(null); const { timelines, analytics, i18n: i18nStart, theme } = useKibana().services; @@ -163,7 +165,7 @@ export const AddToTimelineContextMenu: VFC = ({ const { key, value } = typeof data === 'string' ? { key: field, value: data } : getIndicatorFieldAndValue(data, field); - if (!fieldAndValueValid(key, value)) { + if (!securitySolutionContext?.hasAccessToTimeline || !fieldAndValueValid(key, value)) { return null; } @@ -213,9 +215,10 @@ export const AddToTimelineCellAction: VFC = ({ }) => { const addToTimelineButton = useKibana().services.timelines.getHoverActions().getAddToTimelineButton; + const securitySolutionContext = useSecurityContext(); const { addToTimelineProps } = useAddToTimeline({ indicator: data, field }); - if (!addToTimelineProps) { + if (!securitySolutionContext?.hasAccessToTimeline || !addToTimelineProps) { return null; } addToTimelineProps.Component = Component; diff --git a/x-pack/solutions/security/plugins/threat_intelligence/public/modules/timeline/components/investigate_in_timeline.test.tsx b/x-pack/solutions/security/plugins/threat_intelligence/public/modules/timeline/components/investigate_in_timeline.test.tsx index 1fcbb3d6c6c49..d7419a475d9a2 100644 --- a/x-pack/solutions/security/plugins/threat_intelligence/public/modules/timeline/components/investigate_in_timeline.test.tsx +++ b/x-pack/solutions/security/plugins/threat_intelligence/public/modules/timeline/components/investigate_in_timeline.test.tsx @@ -74,4 +74,15 @@ describe(' { + const mockData: Indicator = generateMockUrlIndicator(); + + const { container } = render( + + + + ); + expect(container).toBeEmptyDOMElement(); + }); }); diff --git a/x-pack/solutions/security/plugins/threat_intelligence/public/modules/timeline/components/investigate_in_timeline.tsx b/x-pack/solutions/security/plugins/threat_intelligence/public/modules/timeline/components/investigate_in_timeline.tsx index e894fa99c9e6c..81678c7632b8b 100644 --- a/x-pack/solutions/security/plugins/threat_intelligence/public/modules/timeline/components/investigate_in_timeline.tsx +++ b/x-pack/solutions/security/plugins/threat_intelligence/public/modules/timeline/components/investigate_in_timeline.tsx @@ -9,6 +9,7 @@ import React, { VFC } from 'react'; import { EuiButtonIcon, EuiContextMenuItem, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { useInvestigateInTimeline } from '../hooks/use_investigate_in_timeline'; +import { useSecurityContext } from '../../../hooks/use_security_context'; import { Indicator } from '../../../../common/types/indicator'; import { BUTTON_ICON_LABEL } from './translations'; @@ -41,7 +42,9 @@ export const InvestigateInTimelineContextMenu: VFC = 'data-test-subj': dataTestSub, }) => { const { investigateInTimelineFn } = useInvestigateInTimeline({ indicator: data }); - if (!investigateInTimelineFn) { + const securitySolutionContext = useSecurityContext(); + + if (!securitySolutionContext?.hasAccessToTimeline || !investigateInTimelineFn) { return null; } @@ -77,7 +80,9 @@ export const InvestigateInTimelineButtonIcon: VFC = 'data-test-subj': dataTestSub, }) => { const { investigateInTimelineFn } = useInvestigateInTimeline({ indicator: data }); - if (!investigateInTimelineFn) { + const securitySolutionContext = useSecurityContext(); + + if (!securitySolutionContext?.hasAccessToTimeline || !investigateInTimelineFn) { return null; } diff --git a/x-pack/solutions/security/plugins/threat_intelligence/public/types.ts b/x-pack/solutions/security/plugins/threat_intelligence/public/types.ts index deffb286068fa..7c7f012be63b7 100644 --- a/x-pack/solutions/security/plugins/threat_intelligence/public/types.ts +++ b/x-pack/solutions/security/plugins/threat_intelligence/public/types.ts @@ -130,6 +130,11 @@ export interface SecuritySolutionPluginContext { to, }: UseInvestigateInTimelineProps) => () => Promise; + /** + * Whether the current user has access to timeline + */ + hasAccessToTimeline: boolean; + useQuery: () => Query; useFilters: () => Filter[]; diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/helper.ts b/x-pack/test/api_integration/apis/cloud_security_posture/helper.ts index 3ad0ef88ef75a..4d244391cf3d3 100644 --- a/x-pack/test/api_integration/apis/cloud_security_posture/helper.ts +++ b/x-pack/test/api_integration/apis/cloud_security_posture/helper.ts @@ -194,7 +194,7 @@ export const createCSPRole = async ( await security.role.create(roleName, { kibana: [ { - feature: { siem: ['read'], fleetv2: ['all'], fleet: ['read'] }, + feature: { siemV2: ['read'], fleetv2: ['all'], fleet: ['read'] }, spaces: ['*'], }, ], diff --git a/x-pack/test/api_integration/apis/features/features/features.ts b/x-pack/test/api_integration/apis/features/features/features.ts index 4ca972549f0cb..fe33d3483150b 100644 --- a/x-pack/test/api_integration/apis/features/features/features.ts +++ b/x-pack/test/api_integration/apis/features/features/features.ts @@ -132,11 +132,13 @@ export default function ({ getService }: FtrProviderContext) { 'uptime', 'searchInferenceEndpoints', 'searchPlayground', - 'siem', + 'siemV2', 'slo', 'securitySolutionAssistant', 'securitySolutionAttackDiscovery', 'securitySolutionCasesV2', + 'securitySolutionTimeline', + 'securitySolutionNotes', 'fleet', 'fleetv2', 'entityManager', @@ -188,10 +190,13 @@ export default function ({ getService }: FtrProviderContext) { 'searchSynonyms', 'searchPlayground', 'siem', + 'siemV2', 'slo', 'securitySolutionAssistant', 'securitySolutionAttackDiscovery', 'securitySolutionCasesV2', + 'securitySolutionTimeline', + 'securitySolutionNotes', 'fleet', 'fleetv2', 'entityManager', diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index 015dbb27b6455..523ade57c53c6 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -103,6 +103,31 @@ export default function ({ getService }: FtrProviderContext) { 'execute_operations_all', 'scan_operations_all', ], + siemV2: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'endpoint_list_all', + 'endpoint_list_read', + 'trusted_applications_all', + 'trusted_applications_read', + 'host_isolation_exceptions_all', + 'host_isolation_exceptions_read', + 'blocklist_all', + 'blocklist_read', + 'event_filters_all', + 'event_filters_read', + 'policy_management_all', + 'policy_management_read', + 'actions_log_management_all', + 'actions_log_management_read', + 'host_isolation_all', + 'process_operations_all', + 'file_operations_all', + 'execute_operations_all', + 'scan_operations_all', + ], uptime: [ 'all', 'read', @@ -138,6 +163,8 @@ export default function ({ getService }: FtrProviderContext) { 'create_comment', 'case_reopen', ], + securitySolutionTimeline: ['all', 'read', 'minimal_all', 'minimal_read'], + securitySolutionNotes: ['all', 'read', 'minimal_all', 'minimal_read'], infrastructure: ['all', 'read', 'minimal_all', 'minimal_read'], logs: ['all', 'read', 'minimal_all', 'minimal_read'], dataQuality: ['all', 'read', 'minimal_all', 'minimal_read'], diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts index 6b4f4b505e3ba..0af7857f616c0 100644 --- a/x-pack/test/api_integration/apis/security/privileges_basic.ts +++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts @@ -48,10 +48,13 @@ export default function ({ getService }: FtrProviderContext) { enterpriseSearchAnalytics: ['all', 'read', 'minimal_all', 'minimal_read'], ml: ['all', 'read', 'minimal_all', 'minimal_read'], siem: ['all', 'read', 'minimal_all', 'minimal_read'], + siemV2: ['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'], securitySolutionCasesV2: ['all', 'read', 'minimal_all', 'minimal_read'], + securitySolutionNotes: ['all', 'read', 'minimal_all', 'minimal_read'], + securitySolutionTimeline: ['all', 'read', 'minimal_all', 'minimal_read'], searchPlayground: ['all', 'read', 'minimal_all', 'minimal_read'], searchInferenceEndpoints: ['all', 'read', 'minimal_all', 'minimal_read'], fleetv2: ['all', 'read', 'minimal_all', 'minimal_read'], @@ -192,6 +195,31 @@ export default function ({ getService }: FtrProviderContext) { 'execute_operations_all', 'scan_operations_all', ], + siemV2: [ + 'actions_log_management_all', + 'actions_log_management_read', + 'all', + 'blocklist_all', + 'blocklist_read', + 'endpoint_list_all', + 'endpoint_list_read', + 'event_filters_all', + 'event_filters_read', + 'host_isolation_all', + 'host_isolation_exceptions_all', + 'host_isolation_exceptions_read', + 'minimal_all', + 'minimal_read', + 'policy_management_all', + 'policy_management_read', + 'process_operations_all', + 'read', + 'trusted_applications_all', + 'trusted_applications_read', + 'file_operations_all', + 'execute_operations_all', + 'scan_operations_all', + ], uptime: [ 'all', 'can_manage_private_locations', @@ -227,6 +255,8 @@ export default function ({ getService }: FtrProviderContext) { 'create_comment', 'case_reopen', ], + securitySolutionTimeline: ['all', 'read', 'minimal_all', 'minimal_read'], + securitySolutionNotes: ['all', 'read', 'minimal_all', 'minimal_read'], infrastructure: ['all', 'read', 'minimal_all', 'minimal_read'], logs: ['all', 'read', 'minimal_all', 'minimal_read'], dataQuality: ['all', 'read', 'minimal_all', 'minimal_read'], diff --git a/x-pack/test/cloud_security_posture_api/routes/helper/user_roles_utilites.ts b/x-pack/test/cloud_security_posture_api/routes/helper/user_roles_utilites.ts index 2aed6367236e4..14a09c951c5be 100644 --- a/x-pack/test/cloud_security_posture_api/routes/helper/user_roles_utilites.ts +++ b/x-pack/test/cloud_security_posture_api/routes/helper/user_roles_utilites.ts @@ -89,7 +89,7 @@ export function CspSecurityCommonProvider(providerContext: FtrProviderContext) { { base: [], feature: { - siem: ['read'], + siemV2: ['read'], fleet: ['all'], fleetv2: ['all'], savedObjectsManagement: ['all'], @@ -107,7 +107,7 @@ export function CspSecurityCommonProvider(providerContext: FtrProviderContext) { { base: [], feature: { - siem: ['read'], + siemV2: ['read'], fleet: ['all'], fleetv2: ['all'], }, @@ -140,7 +140,7 @@ export function CspSecurityCommonProvider(providerContext: FtrProviderContext) { { base: [], feature: { - siem: ['all'], + siemV2: ['all'], fleet: ['all'], fleetv2: ['all'], savedObjectsManagement: ['all'], diff --git a/x-pack/test/fleet_api_integration/apis/test_users.ts b/x-pack/test/fleet_api_integration/apis/test_users.ts index 74581fd681af7..dbab463508fa2 100644 --- a/x-pack/test/fleet_api_integration/apis/test_users.ts +++ b/x-pack/test/fleet_api_integration/apis/test_users.ts @@ -179,7 +179,7 @@ export const testUsers: { permissions: { feature: { fleet: ['read'], - siem: [ + siemV2: [ 'minimal_all', 'trusted_applications_read', 'host_isolation_exceptions_read', @@ -187,6 +187,8 @@ export const testUsers: { 'event_filters_read', 'policy_management_read', ], + securitySolutionNotes: ['all'], + securitySolutionTimeline: ['all'], }, spaces: ['*'], }, @@ -198,7 +200,9 @@ export const testUsers: { permissions: { feature: { fleet: ['all'], - siem: ['minimal_all', 'policy_management_all'], + siemV2: ['minimal_all', 'policy_management_all'], + securitySolutionNotes: ['all'], + securitySolutionTimeline: ['all'], }, spaces: ['*'], }, @@ -210,7 +214,9 @@ export const testUsers: { permissions: { feature: { fleet: ['all'], - siem: ['minimal_all', 'policy_management_read'], + siemV2: ['minimal_all', 'policy_management_read'], + securitySolutionNotes: ['all'], + securitySolutionTimeline: ['all'], }, spaces: ['*'], }, @@ -222,7 +228,9 @@ export const testUsers: { permissions: { feature: { fleet: ['read'], - siem: ['minimal_all'], + siemV2: ['minimal_all'], + securitySolutionNotes: ['all'], + securitySolutionTimeline: ['all'], }, spaces: ['*'], }, @@ -233,7 +241,9 @@ export const testUsers: { endpoint_integr_read_only_fleet_none: { permissions: { feature: { - siem: ['minimal_all'], + siemV2: ['minimal_all'], + securitySolutionNotes: ['all'], + securitySolutionTimeline: ['all'], }, spaces: ['*'], }, diff --git a/x-pack/test/security_api_integration/tests/features/deprecated_features.ts b/x-pack/test/security_api_integration/tests/features/deprecated_features.ts index 35e502a58a343..acb50a8514a1a 100644 --- a/x-pack/test/security_api_integration/tests/features/deprecated_features.ts +++ b/x-pack/test/security_api_integration/tests/features/deprecated_features.ts @@ -183,6 +183,7 @@ export default function ({ getService }: FtrProviderContext) { "generalCases", "observabilityCases", "securitySolutionCases", + "siem", ] `); }); @@ -204,6 +205,7 @@ export default function ({ getService }: FtrProviderContext) { const featureIdsImplicitlyReplacedWithMultipleFeatures = new Set([ 'case_2_feature_a', 'case_4_feature_a', + 'siem', ]); for (const feature of features) { if ( diff --git a/x-pack/test/security_solution_api_integration/config/privileges/roles.ts b/x-pack/test/security_solution_api_integration/config/privileges/roles.ts new file mode 100644 index 0000000000000..54e32092d05ed --- /dev/null +++ b/x-pack/test/security_solution_api_integration/config/privileges/roles.ts @@ -0,0 +1,225 @@ +/* + * 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 { Role } from '../services/types'; + +/** + * Roles for privilege tests + */ + +export const secAllV1: Role = { + name: 'sec_all_v1', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + siem: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; +export const secReadV1: Role = { + name: 'sec_read_v1', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['read'], + }, + ], + }, + kibana: [ + { + feature: { + siem: ['read'], + }, + spaces: ['*'], + }, + ], + }, +}; +export const secNoneV1: Role = { + name: 'sec_none_v1', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['read'], + }, + ], + }, + kibana: [ + { + feature: { + siem: ['none'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const secTimelineAllV2: Role = { + name: 'sec_timeline_all', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + siemV2: ['all'], + securitySolutionTimeline: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const secTimelineReadV2: Role = { + name: 'sec_timeline_read', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['read'], + }, + ], + }, + kibana: [ + { + feature: { + siemV2: ['read'], + securitySolutionTimeline: ['read'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const secTimelineNoneV2: Role = { + name: 'sec_timeline_none', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['read'], + }, + ], + }, + kibana: [ + { + feature: { + siemV2: ['read'], + securitySolutionTimeline: ['none'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const secNotesAllV2: Role = { + name: 'sec_notes_all', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + siemV2: ['all'], + securitySolutionNotes: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const secNotesReadV2: Role = { + name: 'sec_notes_read', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['read'], + }, + ], + }, + kibana: [ + { + feature: { + siemV2: ['read'], + securitySolutionNotes: ['read'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const secNotesNoneV2: Role = { + name: 'sec_notes_none', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['read'], + }, + ], + }, + kibana: [ + { + feature: { + siemV2: ['none'], + securitySolutionNotes: ['none'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const roles: Role[] = [ + secTimelineAllV2, + secTimelineReadV2, + secTimelineNoneV2, + secNotesAllV2, + secNotesReadV2, + secNotesNoneV2, + secAllV1, + secReadV1, + secNoneV1, +]; diff --git a/x-pack/test/security_solution_api_integration/config/privileges/users.ts b/x-pack/test/security_solution_api_integration/config/privileges/users.ts new file mode 100644 index 0000000000000..a107df1c43386 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/config/privileges/users.ts @@ -0,0 +1,89 @@ +/* + * 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 { User } from '../services/types'; +import { + secAllV1, + secNoneV1, + secReadV1, + secNotesAllV2, + secNotesReadV2, + secNotesNoneV2, + secTimelineAllV2, + secTimelineReadV2, + secTimelineNoneV2, +} from './roles'; + +/** + * Users for privilege tests + */ + +export const secAllV1User: User = { + username: 'sec_v1_all', + password: 'password', + roles: [secAllV1.name], +}; + +export const secReadV1User: User = { + username: 'sec_v1_read', + password: 'password', + roles: [secReadV1.name], +}; + +export const secNoneV1User: User = { + username: 'sec_v1_none', + password: 'password', + roles: [secNoneV1.name], +}; + +export const secNotesAllUser: User = { + username: 'sec_Notes_All', + password: 'password', + roles: [secNotesAllV2.name], +}; + +export const secNotesReadUser: User = { + username: 'sec_Notes_Read', + password: 'password', + roles: [secNotesReadV2.name], +}; + +export const secNotesNoneUser: User = { + username: 'sec_Notes_None', + password: 'password', + roles: [secNotesNoneV2.name], +}; + +export const secTimelineAllUser: User = { + username: 'sec_timeline_All', + password: 'password', + roles: [secTimelineAllV2.name], +}; + +export const secTimelineReadUser: User = { + username: 'sec_timeline_Read', + password: 'password', + roles: [secTimelineReadV2.name], +}; + +export const secTimelineNoneUser: User = { + username: 'sec_timeline_None', + password: 'password', + roles: [secTimelineNoneV2.name], +}; + +export const allUsers: User[] = [ + secAllV1User, + secReadV1User, + secNoneV1User, + secNotesAllUser, + secNotesNoneUser, + secNotesReadUser, + secTimelineAllUser, + secTimelineNoneUser, + secTimelineReadUser, +]; diff --git a/x-pack/test/security_solution_api_integration/config/services/security_solution_edr_workflows_roles_users.ts b/x-pack/test/security_solution_api_integration/config/services/security_solution_edr_workflows_roles_users.ts index 92e0cc9ba1f13..a61d9b24e1a41 100644 --- a/x-pack/test/security_solution_api_integration/config/services/security_solution_edr_workflows_roles_users.ts +++ b/x-pack/test/security_solution_api_integration/config/services/security_solution_edr_workflows_roles_users.ts @@ -53,8 +53,8 @@ export function RolesUsersProvider({ getService }: FtrProviderContext) { if (predefinedRole) { const roleConfig = rolesMapping[predefinedRole]; if (extraPrivileges) { - roleConfig.kibana[0].feature.siem = [ - ...roleConfig.kibana[0].feature.siem, + roleConfig.kibana[0].feature.siemV2 = [ + ...roleConfig.kibana[0].feature.siemV2, ...extraPrivileges, ]; } @@ -74,7 +74,7 @@ export function RolesUsersProvider({ getService }: FtrProviderContext) { spaces: ['*'], base: [], feature: { - siem: customRole.extraPrivileges, + siemV2: customRole.extraPrivileges, }, }, ], diff --git a/x-pack/test/security_solution_api_integration/config/services/security_solution_ess_utils.ts b/x-pack/test/security_solution_api_integration/config/services/security_solution_ess_utils.ts index 971fe91ea8d74..ecc8b6747d04e 100644 --- a/x-pack/test/security_solution_api_integration/config/services/security_solution_ess_utils.ts +++ b/x-pack/test/security_solution_api_integration/config/services/security_solution_ess_utils.ts @@ -8,7 +8,7 @@ import { format as formatUrl } from 'url'; import supertest from 'supertest'; import { FtrProviderContextWithSpaces } from '../../ftr_provider_context_with_spaces'; -import { SecuritySolutionESSUtilsInterface } from './types'; +import { SecuritySolutionESSUtilsInterface, Role, User } from './types'; export function SecuritySolutionESSUtils({ getService, @@ -16,21 +16,63 @@ export function SecuritySolutionESSUtils({ const config = getService('config'); const search = getService('search'); const supertestWithoutAuth = getService('supertest'); + const security = getService('security'); + + const createSuperTest = async (role?: string, password: string = 'changeme') => { + if (!role) { + return supertestWithoutAuth; + } + const kbnUrl = formatUrl({ + ...config.get('servers.kibana'), + auth: false, + }); + + return supertest.agent(kbnUrl).auth(role, password); + }; return { getUsername: (_role?: string) => Promise.resolve(config.get('servers.kibana.username') as string), createSearch: (_role?: string) => Promise.resolve(search), - createSuperTest: async (role?: string, password: string = 'changeme') => { - if (!role) { - return supertestWithoutAuth; + + createSuperTest, + + createSuperTestWithUser: (user: User) => { + return createSuperTest(user.username, user.password); + }, + + cleanUpCustomRole: () => { + // In ESS this is a no-op + return Promise.resolve(); + }, + + async createUser(user: User): Promise { + const { username, roles, password } = user; + await security.user.create(username, { roles, password: password ?? 'changeme' }); + }, + + /** + * Deletes specified users by username + * @param names[] + */ + async deleteUsers(names: string[]): Promise { + for (const name of names) { + await security.user.delete(name); } - const kbnUrl = formatUrl({ - ...config.get('servers.kibana'), - auth: false, - }); + }, - return supertest.agent(kbnUrl).auth(role, password); + async createRole(name: string, role: Role) { + return await security.role.create(name, role.privileges); + }, + + /** + * Deletes specified roles by name + * @param roles[] + */ + async deleteRoles(roles: string[]): Promise { + for (const role of roles) { + await security.role.delete(role); + } }, }; } diff --git a/x-pack/test/security_solution_api_integration/config/services/security_solution_serverless_utils.ts b/x-pack/test/security_solution_api_integration/config/services/security_solution_serverless_utils.ts index 1c41ba20bb6f7..6e7e0561e6b21 100644 --- a/x-pack/test/security_solution_api_integration/config/services/security_solution_serverless_utils.ts +++ b/x-pack/test/security_solution_api_integration/config/services/security_solution_serverless_utils.ts @@ -12,7 +12,8 @@ import { RoleCredentials } from '@kbn/test-suites-serverless/shared/services'; import type { SendOptions } from '@kbn/ftr-common-functional-services'; import type { SendOptions as SecureSearchSendOptions } from '@kbn/test-suites-serverless/shared/services/search_secure'; import type { FtrProviderContext } from '../../ftr_provider_context'; -import type { SecuritySolutionUtilsInterface } from './types'; +import type { SecuritySolutionUtilsInterface, User } from './types'; +import { roles } from '../privileges/roles'; export function SecuritySolutionServerlessUtils({ getService, @@ -71,6 +72,32 @@ export function SecuritySolutionServerlessUtils({ */ createSuperTest, + createSuperTestWithUser: async (user: User) => { + if (user.roles.length > 1) { + throw new Error( + `This test service only supports authentication for users with a single role. Error for ${ + user.username + } with roles ${user.roles.join(',')}.` + ); + } + const userRoleName = user.roles[0]; + const roleDefinition = roles.find((role) => role.name === userRoleName); + if (!roleDefinition) { + throw new Error(`Could not find a role definition for ${userRoleName}`); + } + await svlUserManager.setCustomRole(roleDefinition.privileges); + const roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('customRole'); + const superTest = supertest + .agent(kbnUrl) + .set(svlCommonApi.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader); + return superTest; + }, + + cleanUpCustomRole: async () => { + await svlUserManager.deleteCustomRole(); + }, + createSearch: async (role = 'admin') => { const apiKeyHeader = rolesCredentials.get(role)?.apiKeyHeader; diff --git a/x-pack/test/security_solution_api_integration/config/services/types.ts b/x-pack/test/security_solution_api_integration/config/services/types.ts index d173ffb6181fb..2112b05c2e20f 100644 --- a/x-pack/test/security_solution_api_integration/config/services/types.ts +++ b/x-pack/test/security_solution_api_integration/config/services/types.ts @@ -18,11 +18,53 @@ export interface SecuritySolutionServerlessSearch extends Omit Promise; createSuperTest: (role?: string) => Promise>; + createSuperTestWithUser: (user: User) => Promise>; createSearch: (role?: string) => Promise; + cleanUpCustomRole: () => Promise; +} + +interface FeaturesPrivileges { + [featureId: string]: string[]; +} + +interface ElasticsearchIndices { + names: string[]; + privileges: string[]; +} + +export interface Role { + name: string; + privileges: { + elasticsearch: ElasticSearchPrivilege; + kibana: KibanaPrivilege[]; + }; +} +export interface ElasticSearchPrivilege { + cluster?: string[]; + indices?: ElasticsearchIndices[]; +} + +export interface KibanaPrivilege { + spaces: string[]; + base?: string[]; + feature?: FeaturesPrivileges; +} + +export interface User { + username: string; + password: string; + description?: string; + roles: string[]; } export interface SecuritySolutionESSUtilsInterface { getUsername: (role?: string) => Promise; createSearch: (role?: string) => Promise; createSuperTest: (role?: string, password?: string) => Promise>; + createSuperTestWithUser: (user: User) => Promise>; + cleanUpCustomRole: () => Promise; + createUser: (user: User) => Promise; + deleteUsers: (userNames: string[]) => Promise; + createRole: (name: string, role: Role) => Promise; + deleteRoles: (roleNames: string[]) => Promise; } diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/alerts/trial_license_complete_tier/document_level_security.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/alerts/trial_license_complete_tier/document_level_security.ts index 2c8c0c2a599c4..af35ceba994e5 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/alerts/trial_license_complete_tier/document_level_security.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/alerts/trial_license_complete_tier/document_level_security.ts @@ -24,7 +24,7 @@ const roleToAccessSecuritySolution = { kibana: [ { feature: { - siem: ['all'], + siemV2: ['all'], }, spaces: ['*'], }, @@ -47,7 +47,7 @@ const roleToAccessSecuritySolutionWithDls = { kibana: [ { feature: { - siem: ['all'], + siemV2: ['all'], }, spaces: ['*'], }, diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/asset_criticality_privileges.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/asset_criticality_privileges.ts index bd22e51a6a551..24c5349691e4d 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/asset_criticality_privileges.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/asset_criticality_privileges.ts @@ -18,7 +18,7 @@ const ROLES = [ kibana: [ { feature: { - siem: ['read'], + siemV2: ['read'], }, spaces: ['default'], }, diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/risk_engine_privileges.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/risk_engine_privileges.ts index 6b4639030e785..93b9573d3e3aa 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/risk_engine_privileges.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/risk_engine_privileges.ts @@ -16,7 +16,7 @@ const ROLES = [ kibana: [ { feature: { - siem: ['read'], + siemV2: ['read'], }, spaces: ['default'], }, diff --git a/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/auth/roles.ts b/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/auth/roles.ts index 9e81e7d11fffd..0d04c7b3f4fb0 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/auth/roles.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/auth/roles.ts @@ -40,7 +40,7 @@ export const securitySolutionOnlyAll: Role = { kibana: [ { feature: { - siem: ['all'], + siemV2: ['all'], securitySolutionAssistant: ['all'], securitySolutionAttackDiscovery: ['all'], aiAssistantManagementSelection: ['all'], @@ -60,7 +60,7 @@ export const securitySolutionOnlyAllSpace2: Role = { kibana: [ { feature: { - siem: ['all'], + siemV2: ['all'], securitySolutionAssistant: ['all'], securitySolutionAttackDiscovery: ['all'], aiAssistantManagementSelection: ['all'], @@ -80,7 +80,7 @@ export const securitySolutionOnlyRead: Role = { kibana: [ { feature: { - siem: ['read'], + siemV2: ['read'], securitySolutionAssistant: ['all'], securitySolutionAttackDiscovery: ['all'], aiAssistantManagementSelection: ['all'], @@ -100,7 +100,7 @@ export const securitySolutionOnlyReadSpace2: Role = { kibana: [ { feature: { - siem: ['read'], + siemV2: ['read'], securitySolutionAssistant: ['all'], securitySolutionAttackDiscovery: ['all'], aiAssistantManagementSelection: ['all'], @@ -123,7 +123,7 @@ export const securitySolutionOnlyAllSpacesAll: Role = { kibana: [ { feature: { - siem: ['all'], + siemV2: ['all'], securitySolutionAssistant: ['all'], securitySolutionAttackDiscovery: ['all'], aiAssistantManagementSelection: ['all'], @@ -148,7 +148,7 @@ export const securitySolutionOnlyAllSpacesAllWithReadESIndices: Role = { kibana: [ { feature: { - siem: ['all'], + siemV2: ['all'], securitySolutionAssistant: ['all'], securitySolutionAttackDiscovery: ['all'], aiAssistantManagementSelection: ['all'], @@ -168,7 +168,7 @@ export const securitySolutionOnlyReadSpacesAll: Role = { kibana: [ { feature: { - siem: ['read'], + siemV2: ['read'], securitySolutionAssistant: ['all'], securitySolutionAttackDiscovery: ['all'], aiAssistantManagementSelection: ['all'], @@ -188,7 +188,7 @@ export const securitySolutionOnlyAllSpacesAllAssistantMinimalAll: Role = { kibana: [ { feature: { - siem: ['all'], + siemV2: ['all'], securitySolutionAssistant: ['minimal_all'], securitySolutionAttackDiscovery: ['all'], aiAssistantManagementSelection: ['all'], diff --git a/x-pack/test/security_solution_api_integration/test_suites/investigation/timeline/tests/index.ts b/x-pack/test/security_solution_api_integration/test_suites/investigation/timeline/tests/index.ts index b337faad85f07..57e42c95460aa 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/investigation/timeline/tests/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/investigation/timeline/tests/index.ts @@ -15,5 +15,7 @@ export default function ({ loadTestFile }: FtrProviderContextWithSpaces) { loadTestFile(require.resolve('./timeline_migrations')); loadTestFile(require.resolve('./import_timelines')); loadTestFile(require.resolve('./install_prepackaged_timelines')); + loadTestFile(require.resolve('./timeline_privileges')); + loadTestFile(require.resolve('./notes_privileges')); }); } diff --git a/x-pack/test/security_solution_api_integration/test_suites/investigation/timeline/tests/notes_privileges.ts b/x-pack/test/security_solution_api_integration/test_suites/investigation/timeline/tests/notes_privileges.ts new file mode 100644 index 0000000000000..f0f7c36b924f3 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/investigation/timeline/tests/notes_privileges.ts @@ -0,0 +1,119 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContextWithSpaces } from '../../../../ftr_provider_context_with_spaces'; +import { createNote, deleteNote, getNote } from '../../utils/notes'; +import * as users from '../../../../config/privileges/users'; +import { roles } from '../../../../config/privileges/roles'; + +const canOnlyReadUsers = [users.secReadV1User, users.secNotesReadUser]; +const canWriteUsers = [users.secAllV1User, users.secNotesAllUser]; +const canWriteOrReadUsers = [...canOnlyReadUsers, ...canWriteUsers]; +const cannotAccessUsers = [users.secNoneV1User, users.secNotesNoneUser]; +const cannotWriteUsers = [...canOnlyReadUsers, ...cannotAccessUsers]; + +export default function ({ getService }: FtrProviderContextWithSpaces) { + const utils = getService('securitySolutionUtils'); + const config = getService('config'); + const isServerless = config.get('serverless'); + const isEss = !isServerless; + + describe('Notes privileges', () => { + before(async () => { + if (isEss) { + await Promise.all( + roles.map((role) => { + return utils.createRole(role.name, role); + }) + ); + await Promise.all( + users.allUsers.map((user) => { + return utils.createUser(user); + }) + ); + } + }); + after(async () => { + if (isEss) { + await utils.deleteUsers(users.allUsers.map((user) => user.username)); + await utils.deleteRoles(roles.map((role) => role.name)); + } + }); + afterEach(async () => { + await utils.cleanUpCustomRole(); + }); + + describe('read notes', () => { + let getNoteId = () => ''; + before(async () => { + const superTest = await utils.createSuperTestWithUser(users.secNotesAllUser); + const { + body: { noteId }, + } = await createNote(superTest, { text: 'test', documentId: '123' }); + getNoteId = () => noteId; + }); + + canWriteOrReadUsers.forEach((user) => { + it(`user "${user.username}" can read notes`, async () => { + const superTest = await utils.createSuperTestWithUser(user); + await getNote(superTest, getNoteId()).expect(200); + }); + }); + + cannotAccessUsers.forEach((user) => { + it(`user "${user.username}" cannot read notes`, async () => { + const superTest = await utils.createSuperTestWithUser(user); + await getNote(superTest, getNoteId()).expect(403); + }); + }); + }); + + describe('create notes', () => { + canWriteUsers.forEach((user) => { + it(`user "${user.username}" can create notes`, async () => { + const superTest = await utils.createSuperTestWithUser(user); + const { status } = await createNote(superTest, { text: 'test', documentId: '123' }); + expect(status).to.be(200); + }); + }); + + cannotWriteUsers.forEach((user) => { + it(`user "${user.username}" cannot create notes`, async () => { + const superTest = await utils.createSuperTestWithUser(user); + const { status } = await createNote(superTest, { text: 'test', documentId: '123' }); + expect(status).to.be(403); + }); + }); + }); + + describe('delete notes', () => { + let getNoteId = () => ''; + before(async () => { + const superTest = await utils.createSuperTestWithUser(users.secNotesAllUser); + const { + body: { noteId }, + } = await createNote(superTest, { text: 'test', documentId: '123' }); + getNoteId = () => noteId; + }); + + canWriteUsers.forEach((user) => { + it(`user "${user.username}" can delete notes`, async () => { + const superTest = await utils.createSuperTestWithUser(user); + await deleteNote(superTest, getNoteId()).expect(200); + }); + }); + + cannotWriteUsers.forEach((user) => { + it(`user "${user.username}" cannot delete notes`, async () => { + const superTest = await utils.createSuperTestWithUser(user); + await deleteNote(superTest, getNoteId()).expect(403); + }); + }); + }); + }); +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/investigation/timeline/tests/timeline_privileges.ts b/x-pack/test/security_solution_api_integration/test_suites/investigation/timeline/tests/timeline_privileges.ts new file mode 100644 index 0000000000000..7ee34ef482ec0 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/investigation/timeline/tests/timeline_privileges.ts @@ -0,0 +1,274 @@ +/* + * 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 expect from '@kbn/expect'; +import { CreateTimelinesResponse } from '@kbn/security-solution-plugin/common/api/timeline'; +import { FtrProviderContextWithSpaces } from '../../../../ftr_provider_context_with_spaces'; +import { + getTimelines, + createBasicTimeline, + deleteTimeline, + patchTimeline, + favoriteTimeline, + pinEvent, + copyTimeline, + resolveTimeline, + installPrepackedTimelines, +} from '../../utils/timelines'; +import * as users from '../../../../config/privileges/users'; +import { roles } from '../../../../config/privileges/roles'; + +const canOnlyReadUsers = [users.secReadV1User, users.secTimelineReadUser]; +const canWriteUsers = [users.secAllV1User, users.secTimelineAllUser]; +const canWriteOrReadUsers = [...canOnlyReadUsers, ...canWriteUsers]; +const cannotAccessUsers = [users.secNoneV1User, users.secTimelineNoneUser]; +const cannotWriteUsers = [...canOnlyReadUsers, ...cannotAccessUsers]; + +export default function ({ getService }: FtrProviderContextWithSpaces) { + const utils = getService('securitySolutionUtils'); + const config = getService('config'); + const isServerless = config.get('serverless'); + const isEss = !isServerless; + + describe('Timeline privileges', () => { + before(async () => { + if (isEss) { + await Promise.all( + roles.map((role) => { + return utils.createRole(role.name, role); + }) + ); + await Promise.all( + users.allUsers.map((user) => { + return utils.createUser(user); + }) + ); + } + }); + after(async () => { + if (isEss) { + await utils.deleteUsers(users.allUsers.map((user) => user.username)); + await utils.deleteRoles(roles.map((role) => role.name)); + } + }); + afterEach(async () => { + await utils.cleanUpCustomRole(); + }); + + describe('read timelines', () => { + canWriteOrReadUsers.forEach((user) => { + it(`user "${user.username}" can read timelines`, async () => { + const superTest = await utils.createSuperTestWithUser(user); + await getTimelines(superTest).expect(200); + }); + }); + + cannotAccessUsers.forEach((user) => { + it(`user "${user.username}" cannot read timelines`, async () => { + const superTest = await utils.createSuperTestWithUser(user); + await getTimelines(superTest).expect(403); + }); + }); + }); + + describe('resolve timelines', () => { + let getTimelineId = () => ''; + before(async () => { + const superTest = await utils.createSuperTestWithUser(users.secTimelineAllUser); + const { + body: { savedObjectId }, + } = await createBasicTimeline(superTest, 'test timeline'); + getTimelineId = () => savedObjectId; + }); + canWriteOrReadUsers.forEach((user) => { + it(`user "${user.username}" can resolve timelines`, async () => { + const superTest = await utils.createSuperTestWithUser(user); + await resolveTimeline(superTest, getTimelineId()).expect(200); + }); + }); + + cannotAccessUsers.forEach((user) => { + it(`user "${user.username}" cannot resolve timelines`, async () => { + const superTest = await utils.createSuperTestWithUser(user); + await resolveTimeline(superTest, getTimelineId()).expect(403); + }); + }); + }); + + describe('create and delete timelines', () => { + canWriteUsers.forEach((user) => { + it(`user "${user.username}" can create and delete timelines`, async () => { + const superTest = await utils.createSuperTestWithUser(user); + + const createResponse = await createBasicTimeline(superTest, 'test timeline'); + expect(createResponse.status).to.be(200); + + await deleteTimeline(superTest, createResponse.body.savedObjectId).expect(200); + }); + }); + + describe('insufficient privileges', () => { + let getTimelineToDeleteId = () => ''; + before(async () => { + // create a timeline with a privileged user + const privilegedSuperTest = await utils.createSuperTestWithUser(users.secTimelineAllUser); + const { + body: { savedObjectId: timelineId }, + } = await createBasicTimeline(privilegedSuperTest, 'test timeline'); + getTimelineToDeleteId = () => timelineId; + }); + + cannotWriteUsers.forEach((user) => { + it(`user "${user.username}" cannot create timelines`, async () => { + const superTest = await utils.createSuperTestWithUser(user); + const createResponse = await createBasicTimeline(superTest, 'test timeline'); + expect(createResponse.status).to.be(403); + }); + + it(`user "${user.username}" cannot delete timelines`, async () => { + const superTest = await utils.createSuperTestWithUser(user); + await deleteTimeline(superTest, getTimelineToDeleteId()).expect(403); + }); + }); + }); + }); + + describe('update timelines', () => { + canWriteUsers.forEach((user) => { + it(`user "${user.username}" can update timelines`, async () => { + const superTest = await utils.createSuperTestWithUser(user); + const { + body: { savedObjectId: timelineId, version }, + } = await createBasicTimeline(superTest, 'test timeline'); + + await patchTimeline(superTest, timelineId, version, { + title: 'updated title', + }).expect(200); + }); + }); + + describe('insufficient privileges', () => { + let getTimelineId = () => ''; + let getVersion = () => ''; + before(async () => { + const superTest = await utils.createSuperTestWithUser(users.secTimelineAllUser); + const { + body: { savedObjectId, version }, + } = await createBasicTimeline(superTest, 'test timeline'); + getTimelineId = () => savedObjectId; + getVersion = () => version; + }); + cannotWriteUsers.forEach((user) => { + it(`user "${user.username}" cannot create timelines`, async () => { + const superTest = await utils.createSuperTestWithUser(user); + await patchTimeline(superTest, getTimelineId(), getVersion(), { + title: 'updated title', + }).expect(403); + }); + }); + }); + }); + + describe('favorite/unfavorite timelines', () => { + let getTimelineId = () => ''; + before(async () => { + const superTest = await utils.createSuperTestWithUser(users.secTimelineAllUser); + const { + body: { savedObjectId }, + } = await createBasicTimeline(superTest, 'test timeline'); + getTimelineId = () => savedObjectId; + }); + canWriteUsers.forEach((user) => { + it(`user "${user.username}" can favorite/unfavorite timelines`, async () => { + const superTest = await utils.createSuperTestWithUser(user); + await favoriteTimeline(superTest, getTimelineId()).expect(200); + + // unfavorite + await favoriteTimeline(superTest, getTimelineId()).expect(200); + }); + }); + + cannotWriteUsers.forEach((user) => { + it(`user "${user.username}" cannot favorite/unfavorite timelines`, async () => { + const superTest = await utils.createSuperTestWithUser(user); + + await favoriteTimeline(superTest, getTimelineId()).expect(403); + }); + }); + }); + + describe('pin/unpin events', () => { + let getTimelineId = () => ''; + const eventId = 'anId'; + before(async () => { + const superTest = await utils.createSuperTestWithUser(users.secTimelineAllUser); + const { + body: { savedObjectId }, + } = await createBasicTimeline(superTest, 'test timeline'); + getTimelineId = () => savedObjectId; + }); + canWriteUsers.forEach((user) => { + it(`user "${user.username}" can pin/unpin events`, async () => { + const superTest = await utils.createSuperTestWithUser(user); + await pinEvent(superTest, getTimelineId(), eventId).expect(200); + + // unpin + await pinEvent(superTest, getTimelineId(), eventId).expect(200); + }); + }); + + cannotWriteUsers.forEach((user) => { + it(`user "${user.username}" cannot pin/unpin events`, async () => { + const superTest = await utils.createSuperTestWithUser(user); + + await pinEvent(superTest, getTimelineId(), eventId).expect(403); + }); + }); + }); + + describe('copy timeline', () => { + let getTimeline: () => CreateTimelinesResponse = () => + ({} as unknown as CreateTimelinesResponse); + before(async () => { + const superTest = await utils.createSuperTestWithUser(users.secTimelineAllUser); + const { body } = await createBasicTimeline(superTest, 'test timeline'); + getTimeline = () => body; + }); + canWriteUsers.forEach((user) => { + it(`user "${user.username}" can copy timeline`, async () => { + const superTest = await utils.createSuperTestWithUser(user); + const timeline = getTimeline(); + await copyTimeline(superTest, timeline.savedObjectId, timeline).expect(200); + }); + }); + + cannotWriteUsers.forEach((user) => { + it(`user "${user.username}" cannot copy timeline`, async () => { + const superTest = await utils.createSuperTestWithUser(user); + const timeline = getTimeline(); + await copyTimeline(superTest, timeline.savedObjectId, timeline).expect(403); + }); + }); + }); + + describe('install prepackaged timelines', () => { + canWriteUsers.forEach((user) => { + it(`user "${user.username}" can install prepackaged timelines`, async () => { + const superTest = await utils.createSuperTestWithUser(user); + await installPrepackedTimelines(superTest).expect(200); + }); + }); + + cannotWriteUsers.forEach((user) => { + it(`user "${user.username}" cannot install prepackaged timelines`, async () => { + const superTest = await utils.createSuperTestWithUser(user); + await installPrepackedTimelines(superTest).expect(403); + }); + }); + }); + }); +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/investigation/utils/notes.ts b/x-pack/test/security_solution_api_integration/test_suites/investigation/utils/notes.ts index 84db87aefb8f7..57599ca28aef8 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/investigation/utils/notes.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/investigation/utils/notes.ts @@ -58,3 +58,15 @@ export const createNote = async ( note: note.text, }, } as PersistNoteRouteRequestBody); + +export const getNote = (supertest: SuperTest.Agent, noteId: string) => + supertest + .get(`${NOTE_URL}?noteId=${noteId}`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31'); + +export const deleteNote = (supertest: SuperTest.Agent, noteId: string) => + supertest.delete(NOTE_URL).set('kbn-xsrf', 'true').set('elastic-api-version', '2023-10-31').send({ + noteId, + noteIds: null, + }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/investigation/utils/timelines.ts b/x-pack/test/security_solution_api_integration/test_suites/investigation/utils/timelines.ts index 4a5d849e8565d..3b63b1a7b72fb 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/investigation/utils/timelines.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/investigation/utils/timelines.ts @@ -9,20 +9,26 @@ import type SuperTest from 'supertest'; import { v4 as uuidv4 } from 'uuid'; import { GetTimelinesResponse, + SavedTimeline, SavedTimelineWithSavedObjectId, TimelineTypeEnum, } from '@kbn/security-solution-plugin/common/api/timeline'; -import { TIMELINE_URL } from '@kbn/security-solution-plugin/common/constants'; +import { + TIMELINE_URL, + TIMELINES_URL, + TIMELINE_FAVORITE_URL, + PINNED_EVENT_URL, + TIMELINE_COPY_URL, + TIMELINE_RESOLVE_URL, + TIMELINE_PREPACKAGED_URL, +} from '@kbn/security-solution-plugin/common/constants'; /** * Deletes the first 100 timelines. * This works in ess, serverless and on the MKI environments as it avoids having to look at hidden indexes. */ export const deleteTimelines = async (supertest: SuperTest.Agent): Promise => { - const response = await supertest - .get('/api/timelines') - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31'); + const response = await getTimelines(supertest); const { timeline: timelines } = response.body as GetTimelinesResponse; await supertest @@ -35,6 +41,26 @@ export const deleteTimelines = async (supertest: SuperTest.Agent): Promise }); }; +export const deleteTimeline = (supertest: SuperTest.Agent, savedObjectId: string) => + supertest + .delete(TIMELINE_URL) + .set('kbn-xsrf', 'true') + .send({ + savedObjectIds: [savedObjectId], + }); + +export const patchTimeline = ( + supertest: SuperTest.Agent, + timelineId: string, + version: string, + timelineObj: unknown +) => + supertest.patch(TIMELINE_URL).set('kbn-xsrf', 'true').send({ + timelineId, + version, + timeline: timelineObj, + }); + export const createBasicTimeline = async (supertest: SuperTest.Agent, titleToSaved: string) => await supertest .post(TIMELINE_URL) @@ -64,3 +90,47 @@ export const createBasicTimelineTemplate = async ( timelineType: TimelineTypeEnum.template, }, }); + +export const getTimelines = (supertest: SuperTest.Agent) => + supertest.get(TIMELINES_URL).set('kbn-xsrf', 'true').set('elastic-api-version', '2023-10-31'); + +export const resolveTimeline = (supertest: SuperTest.Agent, timelineId: string) => + supertest + .get(`${TIMELINE_RESOLVE_URL}?id=${timelineId}`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31'); + +export const favoriteTimeline = (supertest: SuperTest.Agent, timelineId: string) => + supertest + .patch(TIMELINE_FAVORITE_URL) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31') + .send({ + timelineId, + templateTimelineId: null, + templateTimelineVersion: null, + timelineType: null, + }); + +export const pinEvent = (supertest: SuperTest.Agent, timelineId: string, eventId: string) => + supertest + .patch(PINNED_EVENT_URL) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31') + .send({ + timelineId, + eventId, + }); + +export const copyTimeline = ( + supertest: SuperTest.Agent, + timelineId: string, + timelineObj: SavedTimeline +) => + supertest.post(TIMELINE_COPY_URL).set('kbn-xsrf', 'true').set('elastic-api-version', '1').send({ + timelineIdToCopy: timelineId, + timeline: timelineObj, + }); + +export const installPrepackedTimelines = (supertest: SuperTest.Agent) => + supertest.post(TIMELINE_PREPACKAGED_URL).set('kbn-xsrf', 'true').send(); diff --git a/x-pack/test/security_solution_api_integration/test_suites/lists_and_exception_lists/lists_items/trial_license_complete_tier/lists/read_list_privileges.ts b/x-pack/test/security_solution_api_integration/test_suites/lists_and_exception_lists/lists_items/trial_license_complete_tier/lists/read_list_privileges.ts index 0ab363ce961d0..22cfa186d6531 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/lists_and_exception_lists/lists_items/trial_license_complete_tier/lists/read_list_privileges.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/lists_and_exception_lists/lists_items/trial_license_complete_tier/lists/read_list_privileges.ts @@ -38,7 +38,7 @@ export default ({ getService }: FtrProviderContextWithSpaces) => { { feature: { dashboard: ['all'], - siem: ['all', 'read'], + siemV2: ['all', 'read'], }, spaces: [space1Id], }, diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/privileges.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/privileges.cy.ts new file mode 100644 index 0000000000000..0f31dc585f127 --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/privileges.cy.ts @@ -0,0 +1,65 @@ +/* + * 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 { ROLES } from '@kbn/security-solution-plugin/common/test'; + +import { login } from '../../../tasks/login'; +import { visitWithTimeRange } from '../../../tasks/navigation'; + +import { hostsUrl } from '../../../urls/navigation'; +import { ACTIVE_TIMELINE_BOTTOM_BAR } from '../../../screens/timeline'; +import { TIMELINES } from '../../../screens/security_header'; +import { + NAV_SEARCH_INPUT, + NAV_SEARCH_NO_RESULTS, + NAV_SEARCH_RESULTS, +} from '../../../screens/search_bar'; + +describe('Privileges', { tags: ['@ess', '@skipInServerless'] }, () => { + describe('Timeline', () => { + it('should not show timeline elements for users with insufficient privileges', () => { + login(ROLES.timeline_none); + visitWithTimeRange(hostsUrl('allHosts')); + // no timeline bottom bar + cy.get(ACTIVE_TIMELINE_BOTTOM_BAR).should('not.exist'); + // no link to the timelines page + cy.get(TIMELINES).should('not.exist'); + // no search result for timeline in the nav search + cy.get(NAV_SEARCH_INPUT).type('Timelines'); + cy.get(NAV_SEARCH_NO_RESULTS).should('exist'); + }); + + it('should show timeline elements for users with sufficient privileges', () => { + login(); + visitWithTimeRange(hostsUrl('allHosts')); + cy.get(ACTIVE_TIMELINE_BOTTOM_BAR).should('exist'); + cy.get(TIMELINES).should('exist'); + cy.get(NAV_SEARCH_INPUT).type('Timelines'); + cy.get(NAV_SEARCH_RESULTS).contains('Timelines'); + }); + }); + + // Somehow, these tests fail on CI... + describe.skip('Notes', () => { + it('should show notes in search for users with privileges', () => { + login(ROLES.t3_analyst); + visitWithTimeRange(hostsUrl('allHosts')); + cy.get(NAV_SEARCH_INPUT).focus(); + cy.get(NAV_SEARCH_INPUT).type('Notes'); + cy.get(NAV_SEARCH_RESULTS).contains('Notes'); + }); + + it('should not show notes in search for users with insufficient privileges', () => { + login(ROLES.notes_none); + visitWithTimeRange(hostsUrl('allHosts')); + // no search result for notes in the nav search + cy.get(NAV_SEARCH_INPUT).focus(); + cy.get(NAV_SEARCH_INPUT).type('Notes'); + cy.get(NAV_SEARCH_NO_RESULTS).should('exist'); + }); + }); +}); diff --git a/x-pack/test/security_solution_cypress/cypress/screens/search_bar.ts b/x-pack/test/security_solution_cypress/cypress/screens/search_bar.ts index 766f7da5f131f..cce6478cc6457 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/search_bar.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/search_bar.ts @@ -48,3 +48,9 @@ export const AUTO_SUGGEST_HOST_NAME_VALUE = `[data-test-subj='autocompleteSugges export const EDIT_AS_QUERY_DSL = getDataTestSubjectSelector('editQueryDSL'); export const KIBANA_CODE_EDITOR = getDataTestSubjectSelector('kibanaCodeEditor'); + +export const NAV_SEARCH_INPUT = '[data-test-subj="nav-search-input"]'; + +export const NAV_SEARCH_RESULTS = '[aria-label="Filter options"]'; + +export const NAV_SEARCH_NO_RESULTS = '[data-test-subj="nav-search-no-results"]'; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/privileges.ts b/x-pack/test/security_solution_cypress/cypress/tasks/privileges.ts index bbbaaa1e240a6..0fd8e6f11b8be 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/privileges.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/privileges.ts @@ -62,7 +62,9 @@ export const secAll: Role = { kibana: [ { feature: { - siem: ['all'], + siemV2: ['all'], + securitySolutionTimeline: ['all'], + securitySolutionNotes: ['all'], securitySolutionAssistant: ['all'], securitySolutionAttackDiscovery: ['all'], securitySolutionCases: ['all'], @@ -96,7 +98,9 @@ export const secReadCasesAll: Role = { kibana: [ { feature: { - siem: ['read'], + siemV2: ['read'], + securitySolutionTimeline: ['all'], + securitySolutionNotes: ['all'], securitySolutionAssistant: ['all'], securitySolutionAttackDiscovery: ['all'], securitySolutionCases: ['all'], @@ -130,7 +134,9 @@ export const secAllCasesOnlyReadDelete: Role = { kibana: [ { feature: { - siem: ['all'], + siemV2: ['all'], + securitySolutionTimeline: ['all'], + securitySolutionNotes: ['all'], securitySolutionAssistant: ['all'], securitySolutionAttackDiscovery: ['all'], securitySolutionCases: ['cases_read', 'cases_delete'], @@ -164,7 +170,9 @@ export const secAllCasesNoDelete: Role = { kibana: [ { feature: { - siem: ['all'], + siemV2: ['all'], + securitySolutionTimeline: ['all'], + securitySolutionNotes: ['all'], securitySolutionAssistant: ['all'], securitySolutionAttackDiscovery: ['all'], securitySolutionCases: ['minimal_all'], diff --git a/x-pack/test/session_view/basic/tests/index.ts b/x-pack/test/session_view/basic/tests/index.ts index 71a28bb106186..d471882963566 100644 --- a/x-pack/test/session_view/basic/tests/index.ts +++ b/x-pack/test/session_view/basic/tests/index.ts @@ -57,7 +57,7 @@ export const securitySolutionOnlyReadSpacesAll: Role = { kibana: [ { feature: { - siem: ['read'], + siemV2: ['read'], }, spaces: ['*'], }, diff --git a/x-pack/test/spaces_api_integration/common/suites/create.ts b/x-pack/test/spaces_api_integration/common/suites/create.ts index d84945fbfe032..5599d065eb93c 100644 --- a/x-pack/test/spaces_api_integration/common/suites/create.ts +++ b/x-pack/test/spaces_api_integration/common/suites/create.ts @@ -86,7 +86,10 @@ export function createTestSuiteFactory(esArchiver: any, supertest: SuperTest) 'securitySolutionAttackDiscovery', 'securitySolutionCases', 'securitySolutionCasesV2', + 'securitySolutionNotes', + 'securitySolutionTimeline', 'siem', + 'siemV2', 'slo', 'uptime', ], diff --git a/x-pack/test/spaces_api_integration/common/suites/get_all.ts b/x-pack/test/spaces_api_integration/common/suites/get_all.ts index 9d51cbb12e469..9f4abd8001f6d 100644 --- a/x-pack/test/spaces_api_integration/common/suites/get_all.ts +++ b/x-pack/test/spaces_api_integration/common/suites/get_all.ts @@ -85,7 +85,10 @@ const ALL_SPACE_RESULTS: Space[] = [ 'securitySolutionAttackDiscovery', 'securitySolutionCases', 'securitySolutionCasesV2', + 'securitySolutionNotes', + 'securitySolutionTimeline', 'siem', + 'siemV2', 'slo', 'uptime', ], diff --git a/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts b/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts index 1af3b0e593e06..99f1985b8164f 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts @@ -88,10 +88,13 @@ export default function ({ getService }: FtrProviderContext) { searchInferenceEndpoints: 0, searchPlayground: 0, siem: 0, + siemV2: 0, securitySolutionCases: 0, securitySolutionCasesV2: 0, securitySolutionAssistant: 0, securitySolutionAttackDiscovery: 0, + securitySolutionTimeline: 0, + securitySolutionNotes: 0, discover: 0, visualize: 0, dashboard: 0, diff --git a/x-pack/test_serverless/api_integration/test_suites/security/platform_security/authorization.ts b/x-pack/test_serverless/api_integration/test_suites/security/platform_security/authorization.ts index 96452e95d6d34..5f3deb035cb27 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/platform_security/authorization.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/platform_security/authorization.ts @@ -30,7 +30,7 @@ export default function ({ getService }: FtrProviderContext) { // The following features are composed of other features in a way that is // specific to the security solution. - const compositeFeatureIds = ['dashboard', 'discover', 'reporting', 'siem']; + const compositeFeatureIds = ['dashboard', 'discover', 'reporting', 'siemV2']; const features = Object.fromEntries( Object.entries(body.features).filter(([key]) => compositeFeatureIds.includes(key)) @@ -195,18 +195,18 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:cloud/close_point_in_time", ], }, - "siem": Object { + "siemV2": Object { "actions_log_management_all": Array [ "login:", "api:securitySolution-writeActionsLogManagement", "api:securitySolution-readActionsLogManagement", - "ui:siem/writeActionsLogManagement", - "ui:siem/readActionsLogManagement", + "ui:siemV2/writeActionsLogManagement", + "ui:siemV2/readActionsLogManagement", ], "actions_log_management_read": Array [ "login:", "api:securitySolution-readActionsLogManagement", - "ui:siem/readActionsLogManagement", + "ui:siemV2/readActionsLogManagement", ], "all": Array [ "login:", @@ -281,30 +281,6 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:index-pattern/delete", "saved_object:index-pattern/bulk_delete", "saved_object:index-pattern/share_to_space", - "saved_object:siem-ui-timeline-note/bulk_get", - "saved_object:siem-ui-timeline-note/get", - "saved_object:siem-ui-timeline-note/find", - "saved_object:siem-ui-timeline-note/open_point_in_time", - "saved_object:siem-ui-timeline-note/close_point_in_time", - "saved_object:siem-ui-timeline-note/create", - "saved_object:siem-ui-timeline-note/bulk_create", - "saved_object:siem-ui-timeline-note/update", - "saved_object:siem-ui-timeline-note/bulk_update", - "saved_object:siem-ui-timeline-note/delete", - "saved_object:siem-ui-timeline-note/bulk_delete", - "saved_object:siem-ui-timeline-note/share_to_space", - "saved_object:siem-ui-timeline-pinned-event/bulk_get", - "saved_object:siem-ui-timeline-pinned-event/get", - "saved_object:siem-ui-timeline-pinned-event/find", - "saved_object:siem-ui-timeline-pinned-event/open_point_in_time", - "saved_object:siem-ui-timeline-pinned-event/close_point_in_time", - "saved_object:siem-ui-timeline-pinned-event/create", - "saved_object:siem-ui-timeline-pinned-event/bulk_create", - "saved_object:siem-ui-timeline-pinned-event/update", - "saved_object:siem-ui-timeline-pinned-event/bulk_update", - "saved_object:siem-ui-timeline-pinned-event/delete", - "saved_object:siem-ui-timeline-pinned-event/bulk_delete", - "saved_object:siem-ui-timeline-pinned-event/share_to_space", "saved_object:siem-detection-engine-rule-actions/bulk_get", "saved_object:siem-detection-engine-rule-actions/get", "saved_object:siem-detection-engine-rule-actions/find", @@ -329,18 +305,6 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:security-rule/delete", "saved_object:security-rule/bulk_delete", "saved_object:security-rule/share_to_space", - "saved_object:siem-ui-timeline/bulk_get", - "saved_object:siem-ui-timeline/get", - "saved_object:siem-ui-timeline/find", - "saved_object:siem-ui-timeline/open_point_in_time", - "saved_object:siem-ui-timeline/close_point_in_time", - "saved_object:siem-ui-timeline/create", - "saved_object:siem-ui-timeline/bulk_create", - "saved_object:siem-ui-timeline/update", - "saved_object:siem-ui-timeline/bulk_update", - "saved_object:siem-ui-timeline/delete", - "saved_object:siem-ui-timeline/bulk_delete", - "saved_object:siem-ui-timeline/share_to_space", "saved_object:endpoint:user-artifact-manifest/bulk_get", "saved_object:endpoint:user-artifact-manifest/get", "saved_object:endpoint:user-artifact-manifest/find", @@ -486,14 +450,14 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:cloud/find", "saved_object:cloud/open_point_in_time", "saved_object:cloud/close_point_in_time", - "ui:siem/show", - "ui:siem/crud", - "ui:siem/entity-analytics", - "ui:siem/investigation-guide", - "ui:siem/investigation-guide-interactions", - "ui:siem/threat-intelligence", - "ui:siem/showEndpointExceptions", - "ui:siem/crudEndpointExceptions", + "ui:siemV2/show", + "ui:siemV2/crud", + "ui:siemV2/entity-analytics", + "ui:siemV2/investigation-guide", + "ui:siemV2/investigation-guide-interactions", + "ui:siemV2/threat-intelligence", + "ui:siemV2/showEndpointExceptions", + "ui:siemV2/crudEndpointExceptions", "alerting:siem.notifications/siem/rule/get", "alerting:siem.notifications/siem/rule/getRuleState", "alerting:siem.notifications/siem/rule/getAlertSummary", @@ -959,39 +923,39 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:exception-list-agnostic/delete", "saved_object:exception-list-agnostic/bulk_delete", "saved_object:exception-list-agnostic/share_to_space", - "ui:siem/writeBlocklist", - "ui:siem/readBlocklist", + "ui:siemV2/writeBlocklist", + "ui:siemV2/readBlocklist", ], "blocklist_read": Array [ "login:", "api:lists-read", "api:lists-summary", "api:securitySolution-readBlocklist", - "ui:siem/readBlocklist", + "ui:siemV2/readBlocklist", ], "endpoint_exceptions_all": Array [ "login:", "api:securitySolution-showEndpointExceptions", "api:securitySolution-crudEndpointExceptions", - "ui:siem/showEndpointExceptions", - "ui:siem/crudEndpointExceptions", + "ui:siemV2/showEndpointExceptions", + "ui:siemV2/crudEndpointExceptions", ], "endpoint_exceptions_read": Array [ "login:", "api:securitySolution-showEndpointExceptions", - "ui:siem/showEndpointExceptions", + "ui:siemV2/showEndpointExceptions", ], "endpoint_list_all": Array [ "login:", "api:securitySolution-writeEndpointList", "api:securitySolution-readEndpointList", - "ui:siem/writeEndpointList", - "ui:siem/readEndpointList", + "ui:siemV2/writeEndpointList", + "ui:siemV2/readEndpointList", ], "endpoint_list_read": Array [ "login:", "api:securitySolution-readEndpointList", - "ui:siem/readEndpointList", + "ui:siemV2/readEndpointList", ], "event_filters_all": Array [ "login:", @@ -1012,32 +976,32 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:exception-list-agnostic/delete", "saved_object:exception-list-agnostic/bulk_delete", "saved_object:exception-list-agnostic/share_to_space", - "ui:siem/writeEventFilters", - "ui:siem/readEventFilters", + "ui:siemV2/writeEventFilters", + "ui:siemV2/readEventFilters", ], "event_filters_read": Array [ "login:", "api:lists-read", "api:lists-summary", "api:securitySolution-readEventFilters", - "ui:siem/readEventFilters", + "ui:siemV2/readEventFilters", ], "execute_operations_all": Array [ "login:", "api:securitySolution-writeExecuteOperations", - "ui:siem/writeExecuteOperations", + "ui:siemV2/writeExecuteOperations", ], "file_operations_all": Array [ "login:", "api:securitySolution-writeFileOperations", - "ui:siem/writeFileOperations", + "ui:siemV2/writeFileOperations", ], "host_isolation_all": Array [ "login:", "api:securitySolution-writeHostIsolationRelease", "api:securitySolution-writeHostIsolation", - "ui:siem/writeHostIsolationRelease", - "ui:siem/writeHostIsolation", + "ui:siemV2/writeHostIsolationRelease", + "ui:siemV2/writeHostIsolation", ], "host_isolation_exceptions_all": Array [ "login:", @@ -1060,10 +1024,10 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:exception-list-agnostic/delete", "saved_object:exception-list-agnostic/bulk_delete", "saved_object:exception-list-agnostic/share_to_space", - "ui:siem/readHostIsolationExceptions", - "ui:siem/deleteHostIsolationExceptions", - "ui:siem/accessHostIsolationExceptions", - "ui:siem/writeHostIsolationExceptions", + "ui:siemV2/readHostIsolationExceptions", + "ui:siemV2/deleteHostIsolationExceptions", + "ui:siemV2/accessHostIsolationExceptions", + "ui:siemV2/writeHostIsolationExceptions", ], "host_isolation_exceptions_read": Array [ "login:", @@ -1071,8 +1035,8 @@ export default function ({ getService }: FtrProviderContext) { "api:lists-summary", "api:securitySolution-readHostIsolationExceptions", "api:securitySolution-accessHostIsolationExceptions", - "ui:siem/readHostIsolationExceptions", - "ui:siem/accessHostIsolationExceptions", + "ui:siemV2/readHostIsolationExceptions", + "ui:siemV2/accessHostIsolationExceptions", ], "minimal_all": Array [ "login:", @@ -1145,30 +1109,6 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:index-pattern/delete", "saved_object:index-pattern/bulk_delete", "saved_object:index-pattern/share_to_space", - "saved_object:siem-ui-timeline-note/bulk_get", - "saved_object:siem-ui-timeline-note/get", - "saved_object:siem-ui-timeline-note/find", - "saved_object:siem-ui-timeline-note/open_point_in_time", - "saved_object:siem-ui-timeline-note/close_point_in_time", - "saved_object:siem-ui-timeline-note/create", - "saved_object:siem-ui-timeline-note/bulk_create", - "saved_object:siem-ui-timeline-note/update", - "saved_object:siem-ui-timeline-note/bulk_update", - "saved_object:siem-ui-timeline-note/delete", - "saved_object:siem-ui-timeline-note/bulk_delete", - "saved_object:siem-ui-timeline-note/share_to_space", - "saved_object:siem-ui-timeline-pinned-event/bulk_get", - "saved_object:siem-ui-timeline-pinned-event/get", - "saved_object:siem-ui-timeline-pinned-event/find", - "saved_object:siem-ui-timeline-pinned-event/open_point_in_time", - "saved_object:siem-ui-timeline-pinned-event/close_point_in_time", - "saved_object:siem-ui-timeline-pinned-event/create", - "saved_object:siem-ui-timeline-pinned-event/bulk_create", - "saved_object:siem-ui-timeline-pinned-event/update", - "saved_object:siem-ui-timeline-pinned-event/bulk_update", - "saved_object:siem-ui-timeline-pinned-event/delete", - "saved_object:siem-ui-timeline-pinned-event/bulk_delete", - "saved_object:siem-ui-timeline-pinned-event/share_to_space", "saved_object:siem-detection-engine-rule-actions/bulk_get", "saved_object:siem-detection-engine-rule-actions/get", "saved_object:siem-detection-engine-rule-actions/find", @@ -1193,18 +1133,6 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:security-rule/delete", "saved_object:security-rule/bulk_delete", "saved_object:security-rule/share_to_space", - "saved_object:siem-ui-timeline/bulk_get", - "saved_object:siem-ui-timeline/get", - "saved_object:siem-ui-timeline/find", - "saved_object:siem-ui-timeline/open_point_in_time", - "saved_object:siem-ui-timeline/close_point_in_time", - "saved_object:siem-ui-timeline/create", - "saved_object:siem-ui-timeline/bulk_create", - "saved_object:siem-ui-timeline/update", - "saved_object:siem-ui-timeline/bulk_update", - "saved_object:siem-ui-timeline/delete", - "saved_object:siem-ui-timeline/bulk_delete", - "saved_object:siem-ui-timeline/share_to_space", "saved_object:endpoint:user-artifact-manifest/bulk_get", "saved_object:endpoint:user-artifact-manifest/get", "saved_object:endpoint:user-artifact-manifest/find", @@ -1350,12 +1278,12 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:cloud/find", "saved_object:cloud/open_point_in_time", "saved_object:cloud/close_point_in_time", - "ui:siem/show", - "ui:siem/crud", - "ui:siem/entity-analytics", - "ui:siem/investigation-guide", - "ui:siem/investigation-guide-interactions", - "ui:siem/threat-intelligence", + "ui:siemV2/show", + "ui:siemV2/crud", + "ui:siemV2/entity-analytics", + "ui:siemV2/investigation-guide", + "ui:siemV2/investigation-guide-interactions", + "ui:siemV2/threat-intelligence", "alerting:siem.notifications/siem/rule/get", "alerting:siem.notifications/siem/rule/getRuleState", "alerting:siem.notifications/siem/rule/getAlertSummary", @@ -1836,16 +1764,6 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:index-pattern/find", "saved_object:index-pattern/open_point_in_time", "saved_object:index-pattern/close_point_in_time", - "saved_object:siem-ui-timeline-note/bulk_get", - "saved_object:siem-ui-timeline-note/get", - "saved_object:siem-ui-timeline-note/find", - "saved_object:siem-ui-timeline-note/open_point_in_time", - "saved_object:siem-ui-timeline-note/close_point_in_time", - "saved_object:siem-ui-timeline-pinned-event/bulk_get", - "saved_object:siem-ui-timeline-pinned-event/get", - "saved_object:siem-ui-timeline-pinned-event/find", - "saved_object:siem-ui-timeline-pinned-event/open_point_in_time", - "saved_object:siem-ui-timeline-pinned-event/close_point_in_time", "saved_object:siem-detection-engine-rule-actions/bulk_get", "saved_object:siem-detection-engine-rule-actions/get", "saved_object:siem-detection-engine-rule-actions/find", @@ -1856,11 +1774,6 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:security-rule/find", "saved_object:security-rule/open_point_in_time", "saved_object:security-rule/close_point_in_time", - "saved_object:siem-ui-timeline/bulk_get", - "saved_object:siem-ui-timeline/get", - "saved_object:siem-ui-timeline/find", - "saved_object:siem-ui-timeline/open_point_in_time", - "saved_object:siem-ui-timeline/close_point_in_time", "saved_object:endpoint:user-artifact-manifest/bulk_get", "saved_object:endpoint:user-artifact-manifest/get", "saved_object:endpoint:user-artifact-manifest/find", @@ -1936,11 +1849,11 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:cloud/find", "saved_object:cloud/open_point_in_time", "saved_object:cloud/close_point_in_time", - "ui:siem/show", - "ui:siem/entity-analytics", - "ui:siem/investigation-guide", - "ui:siem/investigation-guide-interactions", - "ui:siem/threat-intelligence", + "ui:siemV2/show", + "ui:siemV2/entity-analytics", + "ui:siemV2/investigation-guide", + "ui:siemV2/investigation-guide-interactions", + "ui:siemV2/threat-intelligence", "alerting:siem.notifications/siem/rule/get", "alerting:siem.notifications/siem/rule/getRuleState", "alerting:siem.notifications/siem/rule/getAlertSummary", @@ -2155,8 +2068,8 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:policy-settings-protection-updates-note/delete", "saved_object:policy-settings-protection-updates-note/bulk_delete", "saved_object:policy-settings-protection-updates-note/share_to_space", - "ui:siem/writePolicyManagement", - "ui:siem/readPolicyManagement", + "ui:siemV2/writePolicyManagement", + "ui:siemV2/readPolicyManagement", ], "policy_management_read": Array [ "login:", @@ -2166,12 +2079,12 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:policy-settings-protection-updates-note/find", "saved_object:policy-settings-protection-updates-note/open_point_in_time", "saved_object:policy-settings-protection-updates-note/close_point_in_time", - "ui:siem/readPolicyManagement", + "ui:siemV2/readPolicyManagement", ], "process_operations_all": Array [ "login:", "api:securitySolution-writeProcessOperations", - "ui:siem/writeProcessOperations", + "ui:siemV2/writeProcessOperations", ], "read": Array [ "login:", @@ -2208,16 +2121,6 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:index-pattern/find", "saved_object:index-pattern/open_point_in_time", "saved_object:index-pattern/close_point_in_time", - "saved_object:siem-ui-timeline-note/bulk_get", - "saved_object:siem-ui-timeline-note/get", - "saved_object:siem-ui-timeline-note/find", - "saved_object:siem-ui-timeline-note/open_point_in_time", - "saved_object:siem-ui-timeline-note/close_point_in_time", - "saved_object:siem-ui-timeline-pinned-event/bulk_get", - "saved_object:siem-ui-timeline-pinned-event/get", - "saved_object:siem-ui-timeline-pinned-event/find", - "saved_object:siem-ui-timeline-pinned-event/open_point_in_time", - "saved_object:siem-ui-timeline-pinned-event/close_point_in_time", "saved_object:siem-detection-engine-rule-actions/bulk_get", "saved_object:siem-detection-engine-rule-actions/get", "saved_object:siem-detection-engine-rule-actions/find", @@ -2228,11 +2131,6 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:security-rule/find", "saved_object:security-rule/open_point_in_time", "saved_object:security-rule/close_point_in_time", - "saved_object:siem-ui-timeline/bulk_get", - "saved_object:siem-ui-timeline/get", - "saved_object:siem-ui-timeline/find", - "saved_object:siem-ui-timeline/open_point_in_time", - "saved_object:siem-ui-timeline/close_point_in_time", "saved_object:endpoint:user-artifact-manifest/bulk_get", "saved_object:endpoint:user-artifact-manifest/get", "saved_object:endpoint:user-artifact-manifest/find", @@ -2308,12 +2206,12 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:cloud/find", "saved_object:cloud/open_point_in_time", "saved_object:cloud/close_point_in_time", - "ui:siem/show", - "ui:siem/entity-analytics", - "ui:siem/investigation-guide", - "ui:siem/investigation-guide-interactions", - "ui:siem/threat-intelligence", - "ui:siem/showEndpointExceptions", + "ui:siemV2/show", + "ui:siemV2/entity-analytics", + "ui:siemV2/investigation-guide", + "ui:siemV2/investigation-guide-interactions", + "ui:siemV2/threat-intelligence", + "ui:siemV2/showEndpointExceptions", "alerting:siem.notifications/siem/rule/get", "alerting:siem.notifications/siem/rule/getRuleState", "alerting:siem.notifications/siem/rule/getAlertSummary", @@ -2515,7 +2413,7 @@ export default function ({ getService }: FtrProviderContext) { "scan_operations_all": Array [ "login:", "api:securitySolution-writeScanOperations", - "ui:siem/writeScanOperations", + "ui:siemV2/writeScanOperations", ], "trusted_applications_all": Array [ "login:", @@ -2536,15 +2434,15 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:exception-list-agnostic/delete", "saved_object:exception-list-agnostic/bulk_delete", "saved_object:exception-list-agnostic/share_to_space", - "ui:siem/writeTrustedApplications", - "ui:siem/readTrustedApplications", + "ui:siemV2/writeTrustedApplications", + "ui:siemV2/readTrustedApplications", ], "trusted_applications_read": Array [ "login:", "api:lists-read", "api:lists-summary", "api:securitySolution-readTrustedApplications", - "ui:siem/readTrustedApplications", + "ui:siemV2/readTrustedApplications", ], }, } diff --git a/x-pack/test_serverless/shared/lib/security/kibana_roles/project_controller_security_roles.yml b/x-pack/test_serverless/shared/lib/security/kibana_roles/project_controller_security_roles.yml index 8da8650a75fa3..86c9d1acb57b0 100644 --- a/x-pack/test_serverless/shared/lib/security/kibana_roles/project_controller_security_roles.yml +++ b/x-pack/test_serverless/shared/lib/security/kibana_roles/project_controller_security_roles.yml @@ -6,19 +6,19 @@ viewer: cluster: [] indices: - names: - - ".siem-signals*" - - ".lists-*" - - ".items-*" + - '.siem-signals*' + - '.lists-*' + - '.items-*' privileges: - - "read" - - "view_index_metadata" + - 'read' + - 'view_index_metadata' allow_restricted_indices: false - names: - - ".alerts*" - - ".preview.alerts*" + - '.alerts*' + - '.preview.alerts*' privileges: - - "read" - - "view_index_metadata" + - 'read' + - 'view_index_metadata' allow_restricted_indices: false - names: - apm-*-transaction* @@ -30,24 +30,26 @@ viewer: - packetbeat-* - winlogbeat-* - metrics-endpoint.metadata_current_* - - ".fleet-agents*" - - ".fleet-actions*" - - "risk-score.risk-score-*" - - ".asset-criticality.asset-criticality-*" - - ".entities.v1.latest.security_*" - - ".ml-anomalies-*" + - '.fleet-agents*' + - '.fleet-actions*' + - 'risk-score.risk-score-*' + - '.asset-criticality.asset-criticality-*' + - '.entities.v1.latest.security_*' + - '.ml-anomalies-*' privileges: - read applications: - - application: "kibana-.kibana" + - application: 'kibana-.kibana' privileges: - feature_ml.read - - feature_siem.read - - feature_siem.read_alerts - - feature_siem.endpoint_list_read + - feature_siemV2.read + - feature_siemV2.read_alerts + - feature_siemV2.endpoint_list_read - feature_securitySolutionCases.read - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.read + - feature_securitySolutionNotes.read - feature_actions.read - feature_builtInAlerts.read - feature_osquery.read @@ -57,7 +59,7 @@ viewer: - feature_graph.all - feature_maps.all - feature_visualize.all - resources: "*" + resources: '*' run_as: [] # modeled after t3_analyst @@ -65,14 +67,14 @@ editor: cluster: [] indices: - names: - - ".siem-signals*" - - ".lists-*" - - ".items-*" + - '.siem-signals*' + - '.lists-*' + - '.items-*' privileges: - - "read" - - "view_index_metadata" - - "write" - - "maintenance" + - 'read' + - 'view_index_metadata' + - 'write' + - 'maintenance' allow_restricted_indices: false - names: - apm-*-transaction* @@ -87,47 +89,49 @@ editor: - read - write - names: - - ".internal.alerts*" - - ".alerts*" - - ".internal.preview.alerts*" - - ".preview.alerts*" - - "risk-score.risk-score-*" + - '.internal.alerts*' + - '.alerts*' + - '.internal.preview.alerts*' + - '.preview.alerts*' + - 'risk-score.risk-score-*' privileges: - - "read" - - "view_index_metadata" - - "write" - - "maintenance" + - 'read' + - 'view_index_metadata' + - 'write' + - 'maintenance' - names: - - ".asset-criticality.asset-criticality-*" - - ".entities.v1.latest.security_*" + - '.asset-criticality.asset-criticality-*' + - '.entities.v1.latest.security_*' privileges: - - "read" - - "write" + - 'read' + - 'write' allow_restricted_indices: false - names: - - ".ml-anomalies-*" + - '.ml-anomalies-*' privileges: - read applications: - - application: "kibana-.kibana" + - application: 'kibana-.kibana' privileges: - feature_ml.read - - feature_siem.all - - feature_siem.read_alerts - - feature_siem.crud_alerts - - feature_siem.endpoint_list_all - - feature_siem.trusted_applications_all - - feature_siem.event_filters_all - - feature_siem.host_isolation_exceptions_all - - feature_siem.blocklist_all - - feature_siem.policy_management_read # Elastic Defend Policy Management - - feature_siem.host_isolation_all - - feature_siem.process_operations_all - - feature_siem.actions_log_management_all # Response actions history - - feature_siem.file_operations_all + - feature_siemV2.all + - feature_siemV2.read_alerts + - feature_siemV2.crud_alerts + - feature_siemV2.endpoint_list_all + - feature_siemV2.trusted_applications_all + - feature_siemV2.event_filters_all + - feature_siemV2.host_isolation_exceptions_all + - feature_siemV2.blocklist_all + - feature_siemV2.policy_management_read # Elastic Defend Policy Management + - feature_siemV2.host_isolation_all + - feature_siemV2.process_operations_all + - feature_siemV2.actions_log_management_all # Response actions history + - feature_siemV2.file_operations_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.all + - feature_securitySolutionNotes.all - feature_actions.read - feature_builtInAlerts.all - feature_osquery.all @@ -137,15 +141,15 @@ editor: - feature_graph.all - feature_maps.all - feature_visualize.all - resources: "*" + resources: '*' run_as: [] t1_analyst: cluster: indices: - names: - - ".alerts-security*" - - ".siem-signals-*" + - '.alerts-security*' + - '.siem-signals-*' privileges: - read - write @@ -162,24 +166,26 @@ t1_analyst: - packetbeat-* - winlogbeat-* - metrics-endpoint.metadata_current_* - - ".fleet-agents*" - - ".fleet-actions*" + - '.fleet-agents*' + - '.fleet-actions*' - risk-score.risk-score-* - .asset-criticality.asset-criticality-* - .entities.v1.latest.security_* - - ".ml-anomalies-*" + - '.ml-anomalies-*' privileges: - read applications: - - application: "kibana-.kibana" + - application: 'kibana-.kibana' privileges: - feature_ml.read - - feature_siem.read - - feature_siem.read_alerts - - feature_siem.endpoint_list_read + - feature_siemV2.read + - feature_siemV2.read_alerts + - feature_siemV2.endpoint_list_read - feature_securitySolutionCases.read - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.read + - feature_securitySolutionNotes.read - feature_actions.read - feature_builtInAlerts.read - feature_osquery.read @@ -190,7 +196,7 @@ t1_analyst: - feature_graph.all - feature_maps.all - feature_visualize.all - resources: "*" + resources: '*' t2_analyst: cluster: @@ -218,7 +224,7 @@ t2_analyst: - .fleet-actions* - risk-score.risk-score-* - .entities.v1.latest.security_* - - ".ml-anomalies-*" + - '.ml-anomalies-*' privileges: - read - names: @@ -227,15 +233,17 @@ t2_analyst: - read - write applications: - - application: "kibana-.kibana" + - application: 'kibana-.kibana' privileges: - feature_ml.read - - feature_siem.read - - feature_siem.read_alerts - - feature_siem.endpoint_list_read + - feature_siemV2.read + - feature_siemV2.read_alerts + - feature_siemV2.endpoint_list_read - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.read + - feature_securitySolutionNotes.read - feature_actions.read - feature_builtInAlerts.read - feature_osquery.read @@ -246,7 +254,7 @@ t2_analyst: - feature_graph.all - feature_maps.all - feature_visualize.all - resources: "*" + resources: '*' t3_analyst: cluster: @@ -284,31 +292,33 @@ t3_analyst: - .fleet-actions* - risk-score.risk-score-* - .entities.v1.latest.security_* - - ".ml-anomalies-*" + - '.ml-anomalies-*' privileges: - read applications: - - application: "kibana-.kibana" + - application: 'kibana-.kibana' privileges: - feature_ml.read - - feature_siem.all - - feature_siem.read_alerts - - feature_siem.crud_alerts - - feature_siem.endpoint_list_all - - feature_siem.trusted_applications_all - - feature_siem.event_filters_all - - feature_siem.host_isolation_exceptions_all - - feature_siem.blocklist_all - - feature_siem.policy_management_read # Elastic Defend Policy Management - - feature_siem.host_isolation_all - - feature_siem.process_operations_all - - feature_siem.actions_log_management_all # Response actions history - - feature_siem.file_operations_all - - feature_siem.scan_operations_all - - feature_siem.workflow_insights_all + - feature_siemV2.all + - feature_siemV2.read_alerts + - feature_siemV2.crud_alerts + - feature_siemV2.endpoint_list_all + - feature_siemV2.trusted_applications_all + - feature_siemV2.event_filters_all + - feature_siemV2.host_isolation_exceptions_all + - feature_siemV2.blocklist_all + - feature_siemV2.policy_management_read # Elastic Defend Policy Management + - feature_siemV2.host_isolation_all + - feature_siemV2.process_operations_all + - feature_siemV2.actions_log_management_all # Response actions history + - feature_siemV2.file_operations_all + - feature_siemV2.scan_operations_all + - feature_siemV2.workflow_insights_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.all + - feature_securitySolutionNotes.all - feature_actions.read - feature_builtInAlerts.all - feature_osquery.all @@ -318,7 +328,7 @@ t3_analyst: - feature_graph.all - feature_maps.all - feature_visualize.all - resources: "*" + resources: '*' threat_intelligence_analyst: cluster: @@ -354,19 +364,21 @@ threat_intelligence_analyst: - .fleet-actions* - risk-score.risk-score-* - .entities.v1.latest.security_* - - ".ml-anomalies-*" + - '.ml-anomalies-*' privileges: - read applications: - - application: "kibana-.kibana" + - application: 'kibana-.kibana' privileges: - feature_ml.read - - feature_siem.all - - feature_siem.endpoint_list_read - - feature_siem.blocklist_all + - feature_siemV2.all + - feature_siemV2.endpoint_list_read + - feature_siemV2.blocklist_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.all + - feature_securitySolutionNotes.all - feature_actions.read - feature_builtInAlerts.read - feature_osquery.all @@ -376,7 +388,7 @@ threat_intelligence_analyst: - feature_graph.all - feature_maps.all - feature_visualize.all - resources: "*" + resources: '*' rule_author: cluster: @@ -417,27 +429,29 @@ rule_author: - .fleet-actions* - risk-score.risk-score-* - .entities.v1.latest.security_* - - ".ml-anomalies-*" + - '.ml-anomalies-*' privileges: - read applications: - - application: "kibana-.kibana" + - application: 'kibana-.kibana' privileges: - feature_ml.read - - feature_siem.all - - feature_siem.read_alerts - - feature_siem.crud_alerts - - feature_siem.policy_management_all - - feature_siem.endpoint_list_all - - feature_siem.trusted_applications_all - - feature_siem.event_filters_all - - feature_siem.host_isolation_exceptions_read - - feature_siem.blocklist_all # Elastic Defend Policy Management - - feature_siem.actions_log_management_read - - feature_siem.workflow_insights_all + - feature_siemV2.all + - feature_siemV2.read_alerts + - feature_siemV2.crud_alerts + - feature_siemV2.policy_management_all + - feature_siemV2.endpoint_list_all + - feature_siemV2.trusted_applications_all + - feature_siemV2.event_filters_all + - feature_siemV2.host_isolation_exceptions_read + - feature_siemV2.blocklist_all # Elastic Defend Policy Management + - feature_siemV2.actions_log_management_read + - feature_siemV2.workflow_insights_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.all + - feature_securitySolutionNotes.all - feature_actions.read - feature_builtInAlerts.all - feature_osquery.all @@ -447,7 +461,7 @@ rule_author: - feature_graph.all - feature_maps.all - feature_visualize.all - resources: "*" + resources: '*' soc_manager: cluster: @@ -488,34 +502,36 @@ soc_manager: - .fleet-actions* - risk-score.risk-score-* - .asset-criticality.asset-criticality-* - - ".ml-anomalies-*" + - '.ml-anomalies-*' privileges: - read applications: - - application: "kibana-.kibana" + - application: 'kibana-.kibana' privileges: - feature_ml.read - feature_generalCases.all - - feature_siem.all - - feature_siem.read_alerts - - feature_siem.crud_alerts - - feature_siem.policy_management_all - - feature_siem.endpoint_list_all - - feature_siem.trusted_applications_all - - feature_siem.event_filters_all - - feature_siem.host_isolation_exceptions_all - - feature_siem.blocklist_all - - feature_siem.host_isolation_all - - feature_siem.process_operations_all - - feature_siem.actions_log_management_all - - feature_siem.file_operations_all - - feature_siem.execute_operations_all - - feature_siem.scan_operations_all - - feature_siem.workflow_insights_all + - feature_siemV2.all + - feature_siemV2.read_alerts + - feature_siemV2.crud_alerts + - feature_siemV2.policy_management_all + - feature_siemV2.endpoint_list_all + - feature_siemV2.trusted_applications_all + - feature_siemV2.event_filters_all + - feature_siemV2.host_isolation_exceptions_all + - feature_siemV2.blocklist_all + - feature_siemV2.host_isolation_all + - feature_siemV2.process_operations_all + - feature_siemV2.actions_log_management_all + - feature_siemV2.file_operations_all + - feature_siemV2.execute_operations_all + - feature_siemV2.scan_operations_all + - feature_siemV2.workflow_insights_all - feature_securitySolutionCases.all - feature_observabilityCases.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.all + - feature_securitySolutionNotes.all - feature_actions.all - feature_builtInAlerts.all - feature_osquery.all @@ -526,10 +542,10 @@ soc_manager: - feature_graph.all - feature_maps.all - feature_visualize.all - resources: "*" + resources: '*' detections_admin: - cluster: ["manage_index_templates", "manage_transform"] + cluster: ['manage_index_templates', 'manage_transform'] indices: - names: - apm-*-transaction* @@ -554,7 +570,7 @@ detections_admin: - metrics-endpoint.metadata_current_* - .fleet-agents* - .fleet-actions* - - ".ml-anomalies-*" + - '.ml-anomalies-*' privileges: - read - names: @@ -568,15 +584,17 @@ detections_admin: - read - write applications: - - application: "kibana-.kibana" + - application: 'kibana-.kibana' privileges: - feature_ml.all - - feature_siem.all - - feature_siem.read_alerts - - feature_siem.crud_alerts + - feature_siemV2.all + - feature_siemV2.read_alerts + - feature_siemV2.crud_alerts - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.all + - feature_securitySolutionNotes.all - feature_actions.all - feature_builtInAlerts.all - feature_dev_tools.all @@ -586,7 +604,7 @@ detections_admin: - feature_graph.all - feature_maps.all - feature_visualize.all - resources: "*" + resources: '*' platform_engineer: cluster: @@ -617,27 +635,29 @@ platform_engineer: - read - write - names: - - ".ml-anomalies-*" + - '.ml-anomalies-*' privileges: - read applications: - - application: "kibana-.kibana" + - application: 'kibana-.kibana' privileges: - feature_ml.all - - feature_siem.all - - feature_siem.read_alerts - - feature_siem.crud_alerts - - feature_siem.policy_management_all - - feature_siem.endpoint_list_all - - feature_siem.trusted_applications_all - - feature_siem.event_filters_all - - feature_siem.host_isolation_exceptions_all - - feature_siem.blocklist_all # Elastic Defend Policy Management - - feature_siem.actions_log_management_read - - feature_siem.workflow_insights_all + - feature_siemV2.all + - feature_siemV2.read_alerts + - feature_siemV2.crud_alerts + - feature_siemV2.policy_management_all + - feature_siemV2.endpoint_list_all + - feature_siemV2.trusted_applications_all + - feature_siemV2.event_filters_all + - feature_siemV2.host_isolation_exceptions_all + - feature_siemV2.blocklist_all # Elastic Defend Policy Management + - feature_siemV2.actions_log_management_read + - feature_siemV2.workflow_insights_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.all + - feature_securitySolutionNotes.all - feature_actions.all - feature_builtInAlerts.all - feature_fleet.all @@ -650,7 +670,7 @@ platform_engineer: - feature_graph.all - feature_maps.all - feature_visualize.all - resources: "*" + resources: '*' endpoint_operations_analyst: cluster: @@ -674,7 +694,7 @@ endpoint_operations_analyst: - .items* - risk-score.risk-score-* - .entities.v1.latest.security_* - - ".ml-anomalies-*" + - '.ml-anomalies-*' privileges: - read - names: @@ -692,27 +712,29 @@ endpoint_operations_analyst: - read - write applications: - - application: "kibana-.kibana" + - application: 'kibana-.kibana' privileges: - feature_ml.read - - feature_siem.all - - feature_siem.read_alerts - - feature_siem.policy_management_all - - feature_siem.endpoint_list_all - - feature_siem.trusted_applications_all - - feature_siem.event_filters_all - - feature_siem.host_isolation_exceptions_all - - feature_siem.blocklist_all - - feature_siem.host_isolation_all - - feature_siem.process_operations_all - - feature_siem.actions_log_management_all # Response History - - feature_siem.file_operations_all - - feature_siem.execute_operations_all # Execute - - feature_siem.scan_operations_all - - feature_siem.workflow_insights_all + - feature_siemV2.all + - feature_siemV2.read_alerts + - feature_siemV2.policy_management_all + - feature_siemV2.endpoint_list_all + - feature_siemV2.trusted_applications_all + - feature_siemV2.event_filters_all + - feature_siemV2.host_isolation_exceptions_all + - feature_siemV2.blocklist_all + - feature_siemV2.host_isolation_all + - feature_siemV2.process_operations_all + - feature_siemV2.actions_log_management_all # Response History + - feature_siemV2.file_operations_all + - feature_siemV2.execute_operations_all # Execute + - feature_siemV2.scan_operations_all + - feature_siemV2.workflow_insights_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.all + - feature_securitySolutionNotes.all - feature_actions.all - feature_builtInAlerts.all - feature_osquery.all @@ -724,7 +746,7 @@ endpoint_operations_analyst: - feature_graph.all - feature_maps.all - feature_visualize.all - resources: "*" + resources: '*' endpoint_policy_manager: cluster: @@ -746,7 +768,7 @@ endpoint_policy_manager: - winlogbeat-* - risk-score.risk-score-* - .entities.v1.latest.security_* - - ".ml-anomalies-*" + - '.ml-anomalies-*' privileges: - read - names: @@ -766,22 +788,24 @@ endpoint_policy_manager: - write - manage applications: - - application: "kibana-.kibana" + - application: 'kibana-.kibana' privileges: - feature_ml.all - - feature_siem.all - - feature_siem.read_alerts - - feature_siem.crud_alerts - - feature_siem.policy_management_all - - feature_siem.endpoint_list_all - - feature_siem.trusted_applications_all - - feature_siem.event_filters_all - - feature_siem.host_isolation_exceptions_all - - feature_siem.blocklist_all # Elastic Defend Policy Management - - feature_siem.workflow_insights_all + - feature_siemV2.all + - feature_siemV2.read_alerts + - feature_siemV2.crud_alerts + - feature_siemV2.policy_management_all + - feature_siemV2.endpoint_list_all + - feature_siemV2.trusted_applications_all + - feature_siemV2.event_filters_all + - feature_siemV2.host_isolation_exceptions_all + - feature_siemV2.blocklist_all # Elastic Defend Policy Management + - feature_siemV2.workflow_insights_all - feature_securitySolutionCases.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all + - feature_securitySolutionTimeline.all + - feature_securitySolutionNotes.all - feature_actions.all - feature_builtInAlerts.all - feature_osquery.all @@ -793,4 +817,4 @@ endpoint_policy_manager: - feature_graph.all - feature_maps.all - feature_visualize.all - resources: "*" + resources: '*'