[8.x] [SecuritySolution] Breaking out timeline & note privileges (#201780) (#207367)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[SecuritySolution] Breaking out timeline & note privileges
(#201780)](https://github.com/elastic/kibana/pull/201780)

<!--- Backport version: 9.6.4 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Jan
Monschke","email":"jan.monschke@elastic.co"},"sourceCommit":{"committedDate":"2025-01-20T13:09:16Z","message":"[SecuritySolution]
Breaking out timeline & note privileges (#201780)\n\n## Summary\n\nEpic:
https://github.com/elastic/security-team/issues/7998\n\nIn this PR we're
breaking out the `timeline` and `notes` features into\ntheir own feature
privilege definition. Previously, access to both\nfeatures was granted
implicitly through the `siem` feature. However, we\nfound that this
level of access control is not sufficient for all\nclients who wanted a
more fine-grained way to grant access to parts of\nsecurity
solution.\n\nIn order to break out `timeline` and `notes` from `siem`,
we had to\ndeprecate it feature privilege definition for. That is why
you'll find\nplenty of changes of `siem` to `siemV2` in this PR. We're
making use of\nthe feature privilege's `replacedBy` functionality,
allowing for a\nseamless migration of deprecated roles.\n\nThis means
that roles that previously granted `siem.all` are now
granted\n`siemV2.all`, `timeline.all` and `notes.all` (same for
`*.read`).\nExisting users are not impacted and should all still have
the correct\naccess. We added tests to make sure this is working as
expected.\n\nAlongside the `ui` privileges, this PR also adds dedicated
API tags.\nThose tags haven been added to the new and previous version
of the\nprivilege definitions to allow for a clean
migration:\n\n```mermaid\nflowchart LR\n subgraph v1\n A(siem) -->
Y(all)\n A --> X(read)\n Y -->|api| W(timeline_write / timeline_read /
notes_read / notes_write)\n X -->|api| V(timeline_read /notes_read)\n
end\n\n subgraph v2\n A-->|replacedBy| C[siemV2]\n A-->|replacedBy|
E[timeline]\n A-->|replacedBy| G[notes]\n \n\n E --> L(all)\n E -->
M(read)\n L -->|api| N(timeline_write / timeline_read)\n M -->|api|
P(timeline_read)\n\n G --> Q(all)\n G --> I(read)\n\n Q -->|api|
R(notes_write / notes_read)\n I -->|api| S(notes_read)\n end\n```\n\n###
Visual changes\n\n#### Hidden/disabled elements\n\nMost of the changes
are happening \"under\" the hood and are only\nexpressed in case a user
has a role with `timeline.none` or\n`notes.none`. This would hide and/or
disable elements that would usually\nallow them to interact with either
timeline or the notes feature (within\ntimeline or the event flyout
currently).\n\nAs an example, this is how the hover actions look for a
user with and\nwithout timeline access:\n\n| With timeline access |
Without timeline access |\n| --- | --- |\n| <img width=\"616\"
alt=\"Screenshot 2024-12-18 at 17 22
49\"\nsrc=\"https://github.com/user-attachments/assets/a767fbb5-49c8-422a-817e-23e7fe1f0042\"\n/>
| <img width=\"724\" alt=\"Screenshot 2024-12-18 at 17 23
29\"\nsrc=\"https://github.com/user-attachments/assets/3490306a-d1c3-41aa-af5b-05a1dd804b47\"\n/>
|\n\n#### Roles\n\nAnother visible change of this PR is the addition of
`Timeline` and\n`Notes` in the edit-role screen:\n\n| Before | After
|\n| ------- | ------ |\n| <img width=\"746\" alt=\"Screenshot
2024-12-12 at 16 31
43\"\nsrc=\"https://github.com/user-attachments/assets/20a80dd4-c214-48a5-8c6e-3dc19c0cbc43\"\n/>
| <img width=\"738\" alt=\"Screenshot 2024-12-12 at 16 32
53\"\nsrc=\"https://github.com/user-attachments/assets/afb1eab4-1729-4c4e-9f51-fddabc32b1dd\"\n/>
|\n\nWe made sure that for migrated roles that hard `security.all`
selected,\nthis screen correctly shows `security.all`, `timeline.all`
and\n`notes.all` after the privilege migration.\n\n#### Timeline
toast\n\nThere are tons of places in security solution where
`Investigate / Add\nto timeline` are shown. We did our best to disable
all of these actions\nbut there is no guarantee that this PR catches all
the places where we\nlink to timeline (actions). One layer of extra
protection is that the\nAPI endpoints don't give access to timelines to
users without the\ncorrect privileges. Another one is a Redux middleware
that makes sure\ntimelines cannot be shown in missed cases. The
following toast will be\nshown instead of the timeline:\n\n<img
width=\"354\" alt=\"Screenshot 2024-12-19 at 10 34
23\"\nsrc=\"https://github.com/user-attachments/assets/1304005e-2753-4268-b6e7-bd7e22d8a1e3\"\n/>\n\n###
Changes to predefined security roles\n\nAll predefined security roles
have been updated to grant the new\nprivileges (in ESS and serverless).
In accordance with the migration,\nall roles with `siem.all` have been
assigned `siemV2.all`,\n`timeline.all` and `notes.all` (and `*.read`
respectively).\n\n### Checklist\n\nCheck the PR satisfies following
conditions. \n\nReviewers should verify this PR satisfies this list as
well.\n\n- [x] Any text added follows [EUI's
writing\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\nsentence case text and includes
[i18n\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\n-
[x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common scenarios\n- [x] This was
checked for breaking HTTP API changes, and any breaking\nchanges have
been approved by the breaking-change committee.
The\n`release_note:breaking` label should be applied in these
situations.\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\nCo-authored-by:
PhilippeOberti <philippe.oberti@elastic.co>\nCo-authored-by: Steph
Milovic
<stephanie.milovic@elastic.co>","sha":"1b167d9dc23a9e0e8e47992a37563ca89ccf3c7d","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Fleet","v9.0.0","release_note:feature","Team:Threat
Hunting:Investigations","backport:prev-minor","ci:cloud-deploy","ci:project-persist-deployment","v8.18.0"],"title":"[SecuritySolution]
Breaking out timeline & note
privileges","number":201780,"url":"https://github.com/elastic/kibana/pull/201780","mergeCommit":{"message":"[SecuritySolution]
Breaking out timeline & note privileges (#201780)\n\n## Summary\n\nEpic:
https://github.com/elastic/security-team/issues/7998\n\nIn this PR we're
breaking out the `timeline` and `notes` features into\ntheir own feature
privilege definition. Previously, access to both\nfeatures was granted
implicitly through the `siem` feature. However, we\nfound that this
level of access control is not sufficient for all\nclients who wanted a
more fine-grained way to grant access to parts of\nsecurity
solution.\n\nIn order to break out `timeline` and `notes` from `siem`,
we had to\ndeprecate it feature privilege definition for. That is why
you'll find\nplenty of changes of `siem` to `siemV2` in this PR. We're
making use of\nthe feature privilege's `replacedBy` functionality,
allowing for a\nseamless migration of deprecated roles.\n\nThis means
that roles that previously granted `siem.all` are now
granted\n`siemV2.all`, `timeline.all` and `notes.all` (same for
`*.read`).\nExisting users are not impacted and should all still have
the correct\naccess. We added tests to make sure this is working as
expected.\n\nAlongside the `ui` privileges, this PR also adds dedicated
API tags.\nThose tags haven been added to the new and previous version
of the\nprivilege definitions to allow for a clean
migration:\n\n```mermaid\nflowchart LR\n subgraph v1\n A(siem) -->
Y(all)\n A --> X(read)\n Y -->|api| W(timeline_write / timeline_read /
notes_read / notes_write)\n X -->|api| V(timeline_read /notes_read)\n
end\n\n subgraph v2\n A-->|replacedBy| C[siemV2]\n A-->|replacedBy|
E[timeline]\n A-->|replacedBy| G[notes]\n \n\n E --> L(all)\n E -->
M(read)\n L -->|api| N(timeline_write / timeline_read)\n M -->|api|
P(timeline_read)\n\n G --> Q(all)\n G --> I(read)\n\n Q -->|api|
R(notes_write / notes_read)\n I -->|api| S(notes_read)\n end\n```\n\n###
Visual changes\n\n#### Hidden/disabled elements\n\nMost of the changes
are happening \"under\" the hood and are only\nexpressed in case a user
has a role with `timeline.none` or\n`notes.none`. This would hide and/or
disable elements that would usually\nallow them to interact with either
timeline or the notes feature (within\ntimeline or the event flyout
currently).\n\nAs an example, this is how the hover actions look for a
user with and\nwithout timeline access:\n\n| With timeline access |
Without timeline access |\n| --- | --- |\n| <img width=\"616\"
alt=\"Screenshot 2024-12-18 at 17 22
49\"\nsrc=\"https://github.com/user-attachments/assets/a767fbb5-49c8-422a-817e-23e7fe1f0042\"\n/>
| <img width=\"724\" alt=\"Screenshot 2024-12-18 at 17 23
29\"\nsrc=\"https://github.com/user-attachments/assets/3490306a-d1c3-41aa-af5b-05a1dd804b47\"\n/>
|\n\n#### Roles\n\nAnother visible change of this PR is the addition of
`Timeline` and\n`Notes` in the edit-role screen:\n\n| Before | After
|\n| ------- | ------ |\n| <img width=\"746\" alt=\"Screenshot
2024-12-12 at 16 31
43\"\nsrc=\"https://github.com/user-attachments/assets/20a80dd4-c214-48a5-8c6e-3dc19c0cbc43\"\n/>
| <img width=\"738\" alt=\"Screenshot 2024-12-12 at 16 32
53\"\nsrc=\"https://github.com/user-attachments/assets/afb1eab4-1729-4c4e-9f51-fddabc32b1dd\"\n/>
|\n\nWe made sure that for migrated roles that hard `security.all`
selected,\nthis screen correctly shows `security.all`, `timeline.all`
and\n`notes.all` after the privilege migration.\n\n#### Timeline
toast\n\nThere are tons of places in security solution where
`Investigate / Add\nto timeline` are shown. We did our best to disable
all of these actions\nbut there is no guarantee that this PR catches all
the places where we\nlink to timeline (actions). One layer of extra
protection is that the\nAPI endpoints don't give access to timelines to
users without the\ncorrect privileges. Another one is a Redux middleware
that makes sure\ntimelines cannot be shown in missed cases. The
following toast will be\nshown instead of the timeline:\n\n<img
width=\"354\" alt=\"Screenshot 2024-12-19 at 10 34
23\"\nsrc=\"https://github.com/user-attachments/assets/1304005e-2753-4268-b6e7-bd7e22d8a1e3\"\n/>\n\n###
Changes to predefined security roles\n\nAll predefined security roles
have been updated to grant the new\nprivileges (in ESS and serverless).
In accordance with the migration,\nall roles with `siem.all` have been
assigned `siemV2.all`,\n`timeline.all` and `notes.all` (and `*.read`
respectively).\n\n### Checklist\n\nCheck the PR satisfies following
conditions. \n\nReviewers should verify this PR satisfies this list as
well.\n\n- [x] Any text added follows [EUI's
writing\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\nsentence case text and includes
[i18n\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\n-
[x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common scenarios\n- [x] This was
checked for breaking HTTP API changes, and any breaking\nchanges have
been approved by the breaking-change committee.
The\n`release_note:breaking` label should be applied in these
situations.\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\nCo-authored-by:
PhilippeOberti <philippe.oberti@elastic.co>\nCo-authored-by: Steph
Milovic
<stephanie.milovic@elastic.co>","sha":"1b167d9dc23a9e0e8e47992a37563ca89ccf3c7d"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/201780","number":201780,"mergeCommit":{"message":"[SecuritySolution]
Breaking out timeline & note privileges (#201780)\n\n## Summary\n\nEpic:
https://github.com/elastic/security-team/issues/7998\n\nIn this PR we're
breaking out the `timeline` and `notes` features into\ntheir own feature
privilege definition. Previously, access to both\nfeatures was granted
implicitly through the `siem` feature. However, we\nfound that this
level of access control is not sufficient for all\nclients who wanted a
more fine-grained way to grant access to parts of\nsecurity
solution.\n\nIn order to break out `timeline` and `notes` from `siem`,
we had to\ndeprecate it feature privilege definition for. That is why
you'll find\nplenty of changes of `siem` to `siemV2` in this PR. We're
making use of\nthe feature privilege's `replacedBy` functionality,
allowing for a\nseamless migration of deprecated roles.\n\nThis means
that roles that previously granted `siem.all` are now
granted\n`siemV2.all`, `timeline.all` and `notes.all` (same for
`*.read`).\nExisting users are not impacted and should all still have
the correct\naccess. We added tests to make sure this is working as
expected.\n\nAlongside the `ui` privileges, this PR also adds dedicated
API tags.\nThose tags haven been added to the new and previous version
of the\nprivilege definitions to allow for a clean
migration:\n\n```mermaid\nflowchart LR\n subgraph v1\n A(siem) -->
Y(all)\n A --> X(read)\n Y -->|api| W(timeline_write / timeline_read /
notes_read / notes_write)\n X -->|api| V(timeline_read /notes_read)\n
end\n\n subgraph v2\n A-->|replacedBy| C[siemV2]\n A-->|replacedBy|
E[timeline]\n A-->|replacedBy| G[notes]\n \n\n E --> L(all)\n E -->
M(read)\n L -->|api| N(timeline_write / timeline_read)\n M -->|api|
P(timeline_read)\n\n G --> Q(all)\n G --> I(read)\n\n Q -->|api|
R(notes_write / notes_read)\n I -->|api| S(notes_read)\n end\n```\n\n###
Visual changes\n\n#### Hidden/disabled elements\n\nMost of the changes
are happening \"under\" the hood and are only\nexpressed in case a user
has a role with `timeline.none` or\n`notes.none`. This would hide and/or
disable elements that would usually\nallow them to interact with either
timeline or the notes feature (within\ntimeline or the event flyout
currently).\n\nAs an example, this is how the hover actions look for a
user with and\nwithout timeline access:\n\n| With timeline access |
Without timeline access |\n| --- | --- |\n| <img width=\"616\"
alt=\"Screenshot 2024-12-18 at 17 22
49\"\nsrc=\"https://github.com/user-attachments/assets/a767fbb5-49c8-422a-817e-23e7fe1f0042\"\n/>
| <img width=\"724\" alt=\"Screenshot 2024-12-18 at 17 23
29\"\nsrc=\"https://github.com/user-attachments/assets/3490306a-d1c3-41aa-af5b-05a1dd804b47\"\n/>
|\n\n#### Roles\n\nAnother visible change of this PR is the addition of
`Timeline` and\n`Notes` in the edit-role screen:\n\n| Before | After
|\n| ------- | ------ |\n| <img width=\"746\" alt=\"Screenshot
2024-12-12 at 16 31
43\"\nsrc=\"https://github.com/user-attachments/assets/20a80dd4-c214-48a5-8c6e-3dc19c0cbc43\"\n/>
| <img width=\"738\" alt=\"Screenshot 2024-12-12 at 16 32
53\"\nsrc=\"https://github.com/user-attachments/assets/afb1eab4-1729-4c4e-9f51-fddabc32b1dd\"\n/>
|\n\nWe made sure that for migrated roles that hard `security.all`
selected,\nthis screen correctly shows `security.all`, `timeline.all`
and\n`notes.all` after the privilege migration.\n\n#### Timeline
toast\n\nThere are tons of places in security solution where
`Investigate / Add\nto timeline` are shown. We did our best to disable
all of these actions\nbut there is no guarantee that this PR catches all
the places where we\nlink to timeline (actions). One layer of extra
protection is that the\nAPI endpoints don't give access to timelines to
users without the\ncorrect privileges. Another one is a Redux middleware
that makes sure\ntimelines cannot be shown in missed cases. The
following toast will be\nshown instead of the timeline:\n\n<img
width=\"354\" alt=\"Screenshot 2024-12-19 at 10 34
23\"\nsrc=\"https://github.com/user-attachments/assets/1304005e-2753-4268-b6e7-bd7e22d8a1e3\"\n/>\n\n###
Changes to predefined security roles\n\nAll predefined security roles
have been updated to grant the new\nprivileges (in ESS and serverless).
In accordance with the migration,\nall roles with `siem.all` have been
assigned `siemV2.all`,\n`timeline.all` and `notes.all` (and `*.read`
respectively).\n\n### Checklist\n\nCheck the PR satisfies following
conditions. \n\nReviewers should verify this PR satisfies this list as
well.\n\n- [x] Any text added follows [EUI's
writing\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\nsentence case text and includes
[i18n\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\n-
[x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common scenarios\n- [x] This was
checked for breaking HTTP API changes, and any breaking\nchanges have
been approved by the breaking-change committee.
The\n`release_note:breaking` label should be applied in these
situations.\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\nCo-authored-by:
PhilippeOberti <philippe.oberti@elastic.co>\nCo-authored-by: Steph
Milovic
<stephanie.milovic@elastic.co>","sha":"1b167d9dc23a9e0e8e47992a37563ca89ccf3c7d"}},{"branch":"8.x","label":"v8.18.0","branchLabelMappingKey":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->
This commit is contained in:
Jan Monschke 2025-01-22 12:20:34 +01:00 committed by GitHub
parent f9bed85a13
commit 8e02172e2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
263 changed files with 5429 additions and 1209 deletions

View file

@ -22,7 +22,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.

View file

@ -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
@ -172,12 +176,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
@ -227,12 +233,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
@ -286,24 +294,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
@ -360,12 +370,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
@ -421,20 +433,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
@ -489,25 +503,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
@ -562,12 +578,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
@ -614,20 +632,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
@ -684,24 +704,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
@ -765,19 +787,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

View file

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

View file

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

View file

@ -42039,7 +42039,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": "検索",

View file

@ -42135,7 +42135,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": "搜索",

View file

@ -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'
$ref: '../components/schemas/401_response.yaml'

View file

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

View file

@ -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<string, boolean>
>((acc, privilegeName) => {
acc[privilegeName] = (capabilities.siem[privilegeName] as boolean) || false;
acc[privilegeName] = (capabilities.siemV2[privilegeName] as boolean) || false;
return acc;
}, {});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<SecuritySubFeatureId> => ({
@ -20,3 +28,11 @@ export const getSecurityFeature = (
baseKibanaSubFeatureIds: getSecurityBaseKibanaSubFeatureIds(params),
subFeaturesMap: getSecuritySubFeaturesMap(params),
});
export const getSecurityV2Feature = (
params: SecurityFeatureParams
): ProductFeatureParams<SecuritySubFeatureId> => ({
baseKibanaFeature: getSecurityV2BaseKibanaFeature(params),
baseKibanaSubFeatureIds: getSecurityV2BaseKibanaSubFeatureIds(params),
subFeaturesMap: getSecurityV2SubFeaturesMap(params),
});

View file

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

View file

@ -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 Defends 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<SecuritySubFeatureId, SubFeatureConfig> => {
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<SecuritySubFeatureId, SubFeatureConfig>(
securitySubFeaturesList
);
return Object.freeze(securitySubFeaturesMap);
};

View file

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

View file

@ -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<SecuritySubFeatureId, SubFeatureConfig> => {
const enableSpaceAwarenessIfNeeded = (subFeature: SubFeatureConfig): SubFeatureConfig => {

View file

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

View file

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

View file

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

View file

@ -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<T extends string = string> = Map<T, SubFeatureConfig>;
export interface ProductFeatureParams<T extends string = string> {

View file

@ -47,7 +47,7 @@ const getTestComponent =
...coreStart.application,
capabilities: {
...coreStart.application.capabilities,
siem: { crud: true },
siemV2: { crud: true },
},
},
};

View file

@ -47,7 +47,7 @@ const getWrapper =
...coreStart.application,
capabilities: {
...coreStart.application.capabilities,
siem: { crud: canUpdate },
siemV2: { crud: canUpdate },
},
},
};

View file

@ -22,7 +22,10 @@ export const APP_UI_ID = 'securitySolutionUI' as const;
export const ASSISTANT_FEATURE_ID = 'securitySolutionAssistant' as const;
export const ATTACK_DISCOVERY_FEATURE_ID = 'securitySolutionAttackDiscovery' as const;
export const CASES_FEATURE_ID = '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;
@ -66,7 +69,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;

View file

@ -10,6 +10,7 @@ export {
APP_ID,
CASES_FEATURE_ID,
SERVER_APP_ID,
SECURITY_FEATURE_ID,
APP_PATH,
MANAGE_PATH,
ADD_DATA_PATH,

View file

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

View file

@ -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',
}
/**

View file

@ -114,6 +114,7 @@ export interface ActionProps {
toggleShowNotes?: () => void;
width?: number;
disablePinAction?: boolean;
disableTimelineAction?: boolean;
}
interface AdditionalControlColumnProps {

View file

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

View file

@ -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<SecurityCellAction> => {
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) &&

View file

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

View file

@ -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<SecurityCellAction> => {
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) &&

View file

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

View file

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

View file

@ -13,6 +13,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, isInSecurityApp } from '../../utils';
import {
@ -75,6 +76,7 @@ export const createAddToTimelineLensAction = ({
applicationService.currentAppId$.subscribe((appId) => {
currentAppId = appId;
});
const timelineCapabilities = extractTimelineCapabilities(applicationService.capabilities);
return createAction<CellValueContext>({
id: ACTION_ID,
@ -83,6 +85,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) &&

View file

@ -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(<RedirectRoute capabilities={mockCapabilities} />);
@ -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(<RedirectRoute capabilities={mockCapabilities} />);
@ -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(<RedirectRoute capabilities={mockCapabilities} />);
@ -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(<RedirectRoute capabilities={mockCapabilities} />);
@ -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(<RedirectRoute capabilities={mockCapabilities} />);
@ -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(<RedirectRoute capabilities={mockCapabilities} />);
@ -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(<RedirectRoute capabilities={mockCapabilities} />);

View file

@ -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<AppRoutesProps> = 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 <Redirect to={ONBOARDING_PATH} />;
}
if (capabilities[CASES_FEATURE_ID].read_cases === true) {

View file

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

View file

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

View file

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

View file

@ -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<PropsWithChildren<SendToTimelineButtonProp
const { discoverStateContainer, defaultDiscoverAppState } = useDiscoverInTimelineContext();
const { dataViewId: timelineDataViewId } = useSourcererDataView(SourcererScopeName.timeline);
const { setDiscoverAppState } = useDiscoverState();
const {
application: { capabilities },
} = useKibana().services;
const { read: hasAccessToTimeline } = extractTimelineCapabilities(capabilities);
const signalIndexName = useSelector(sourcererSelectors.signalIndexName);
const defaultDataView = useSelector(sourcererSelectors.defaultDataView);
@ -242,7 +247,7 @@ export const SendToTimelineButton: FC<PropsWithChildren<SendToTimelineButtonProp
: ACTION_CANNOT_INVESTIGATE_IN_TIMELINE;
const isDisabled = !isTimelineBottomBarVisible;
if (dataProviders?.[0]?.queryType === 'esql' || dataProviders?.[0]?.queryType === 'sql') {
if (!hasAccessToTimeline) {
return null;
}

View file

@ -6,13 +6,13 @@
*/
import { ATTACK_DISCOVERY_FEATURE_ID } from '../../common/constants';
import { SERVER_APP_ID } from '../../common';
import { SECURITY_FEATURE_ID } from '../../common';
import { links } from './links';
describe('links', () => {
it('for serverless, it specifies capabilities as an AND condition, via a nested array', () => {
expect(links.capabilities).toEqual<string[][]>([
[`${SERVER_APP_ID}.show`, `${ATTACK_DISCOVERY_FEATURE_ID}.attack-discovery`],
[`${SECURITY_FEATURE_ID}.show`, `${ATTACK_DISCOVERY_FEATURE_ID}.attack-discovery`],
]);
});

View file

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

View file

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

View file

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

View file

@ -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<LinkItem> = {
hideTimeline: true,
capabilities: [`${SERVER_APP_ID}.show`],
capabilities: [`${SECURITY_FEATURE_ID}.show`],
};
export const cloudDefendLink: LinkItem = {

View file

@ -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<LinkItem> = {
hideTimeline: true,
capabilities: [`${SERVER_APP_ID}.show`],
capabilities: [`${SECURITY_FEATURE_ID}.show`],
};
export const findingsLinks: LinkItem = {

View file

@ -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(
<TestProviders>
<RowAction {...defaultProps} />
</TestProviders>
);
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(
<TestProviders>
<RowAction {...defaultProps} />
</TestProviders>
);
expect(wrapper.queryByTestId('timeline-notes-button-small')).not.toBeInTheDocument();
expect(wrapper.queryByTestId('send-alert-to-timeline-button')).not.toBeInTheDocument();
});
});
});

View file

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

View file

@ -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', () => {
</TestProviders>
);
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(
<TestProviders>
<InvestigateInTimelineButton asEmptyButton={true} dataProviders={dataProviders} />
</TestProviders>
);
expect(screen.queryByLabelText(ACTION_INVESTIGATE_IN_TIMELINE)).toBeDisabled();
});
});
});

View file

@ -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 ? (
<EuiButtonEmpty
@ -73,11 +85,19 @@ export const InvestigateInTimelineButton: FC<
flush={flush ?? 'right'}
size="xs"
iconType={iconType}
disabled={disabled}
data-test-subj={dataTestSubj}
>
{children}
</EuiButtonEmpty>
) : (
<EuiButton aria-label={ACTION_INVESTIGATE_IN_TIMELINE} onClick={openTimelineCallback} {...rest}>
<EuiButton
aria-label={ACTION_INVESTIGATE_IN_TIMELINE}
disabled={disabled}
onClick={openTimelineCallback}
data-test-subj={dataTestSubj}
{...rest}
>
{children}
</EuiButton>
);

View file

@ -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(
<TestProviders>
<EventsQueryTabBody {...commonProps} />
</TestProviders>
);
expect(mockGetDefaultControlColumn).toHaveBeenCalledWith(4);
});
it('should have 6 columns on Action bar for Enterprise user', () => {
const licenseServiceMock = licenseService as jest.Mocked<typeof licenseService>;
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<typeof licenseService>;
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<typeof licenseService>;
licenseServiceMock.isEnterprise.mockReturnValue(true);
(useUserPrivileges as jest.Mock).mockReturnValue({ notesPrivileges: { read: false } });
render(
<TestProviders>
<EventsQueryTabBody {...commonProps} />
</TestProviders>
);
expect(mockGetDefaultControlColumn).toHaveBeenCalledWith(5);
});
});

View file

@ -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<EventsQueryTabBodyComponentProps> = ({
additionalFilters,
deleteQuery,
@ -70,17 +80,27 @@ const EventsQueryTabBodyComponent: React.FC<EventsQueryTabBodyComponentProps> =
startDate,
tableId,
}) => {
let ACTION_BUTTON_COUNT = MAX_ACTION_BUTTON_COUNT;
const dispatch = useDispatch();
const { globalFullScreen } = useGlobalFullScreen();
const [defaultNumberFormat] = useUiSetting$<string>(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]

View file

@ -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(
<TestProviders>
<Actions {...defaultProps} />
</TestProviders>
);
expect(
wrapper.find('[data-test-subj="send-alert-to-timeline-button"]').exists()
).toBeTruthy();
});
test('should hide timeline action when disableTimelineAction = true', () => {
const wrapper = mount(
<TestProviders>
<Actions {...defaultProps} disableTimelineAction />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="send-alert-to-timeline-button"]').exists()).toBeFalsy();
});
});
});

View file

@ -73,6 +73,7 @@ const ActionsComponent: React.FC<ActionProps> = ({
refetch,
toggleShowNotes,
disablePinAction = true,
disableTimelineAction = false,
}) => {
const dispatch = useDispatch();
@ -339,7 +340,7 @@ const ActionsComponent: React.FC<ActionProps> = ({
</GuidedOnboardingTourStep>
)}
<>
{timelineId !== TimelineId.active && (
{!disableTimelineAction && timelineId !== TimelineId.active && (
<InvestigateInTimelineAction
ariaLabel={i18n.SEND_ALERT_TO_TIMELINE_FOR_ROW({ ariaRowindex, columnValues })}
key="investigate-in-timeline"

View file

@ -53,7 +53,7 @@ describe('AddEventNoteAction', () => {
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(),
});

View file

@ -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<AddEventNoteActionProps> = ({
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 (
<ActionIconItem>
<NotesButton
ariaLabel={ariaLabel}
data-test-subj="add-note"
isDisabled={kibanaSecuritySolutionsPrivileges.crud === false}
isDisabled={disabled}
timelineType={timelineType}
toggleShowNotes={toggleShowNotes}
toolTip={timelineType === TimelineTypeEnum.template ? NOTES_DISABLE_TOOLTIP : NOTES_TOOLTIP}
toolTip={tooltip}
eventId={eventId}
notesCount={notesCount}
/>

View file

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

View file

@ -31,7 +31,7 @@ const PinEventActionComponent: React.FC<PinEventActionProps> = ({
eventIsPinned,
timelineType,
}) => {
const { kibanaSecuritySolutionsPrivileges } = useUserPrivileges();
const { timelinePrivileges } = useUserPrivileges();
const tooltipContent = useMemo(
() =>
getPinTooltip({
@ -51,7 +51,7 @@ const PinEventActionComponent: React.FC<PinEventActionProps> = ({
ariaLabel={ariaLabel}
allowUnpinning={!eventHasNotes(noteIds)}
data-test-subj="pin-event"
isDisabled={kibanaSecuritySolutionsPrivileges.crud === false}
isDisabled={timelinePrivileges.crud === false}
isAlert={isAlert}
onClick={onPinClicked}
pinned={eventIsPinned}

View file

@ -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<MarkdownEditorRef, MarkdownEditorProp
const insightsUpsellingMessage = useUpsellingMessage('investigation_guide');
const interactionsUpsellingMessage = useUpsellingMessage('investigation_guide_interactions');
const {
timelinePrivileges: { read: canSeeTimeline },
} = useUserPrivileges();
const uiPluginsWithState = useMemo(() => {
return includePlugins
? uiPlugins({
insightsUpsellingMessage,
interactionsUpsellingMessage,
canSeeTimeline,
})
: undefined;
}, [includePlugins, insightsUpsellingMessage, interactionsUpsellingMessage]);
}, [includePlugins, canSeeTimeline, insightsUpsellingMessage, interactionsUpsellingMessage]);
// @ts-expect-error update types
useImperativeHandle(ref, () => {

View file

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

View file

@ -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: (
<EuiCodeBlock language="md" paddingSize="s" fontSize="l">

View file

@ -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<TimelineProps> = ({
id,
@ -22,6 +23,10 @@ export const TimelineMarkDownRendererComponent: React.FC<TimelineProps> = ({
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<TimelineProps> = ({
<EuiToolTip content={interactionsUpsellingMessage ?? i18n.TIMELINE_ID(id ?? '')}>
<EuiLink
onClick={onClickTimeline}
disabled={!!interactionsUpsellingMessage}
disabled={isDisabled}
data-test-subj={`markdown-timeline-link-${id}`}
>
{title}

View file

@ -53,7 +53,7 @@ describe('When using useEndpointPrivileges hook', () => {
catalogue: {},
management: {},
navLinks: {},
siem: {
siemV2: {
crud: true,
show: true,
},

View file

@ -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<typeof useFetchListPrivileges>;
detectionEnginePrivileges: ReturnType<typeof useFetchDetectionEnginePrivileges>;
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<UserPrivilegesState>(
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}

View file

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

View file

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

View file

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

View file

@ -137,7 +137,7 @@ const TestProvidersWithPrivilegesComponent: React.FC<Props> = ({
<UserPrivilegesProvider
kibanaCapabilities={
{
siem: { show: true, crud: true },
siemV2: { show: true, crud: true },
[CASES_FEATURE_ID]: { read_cases: true, crud_cases: false },
[ASSISTANT_FEATURE_ID]: { 'ai-assistant': true },
} as unknown as Capabilities

View file

@ -29,7 +29,6 @@ import {
DEFAULT_DATA_VIEW_ID,
DEFAULT_INDEX_KEY,
DETECTION_ENGINE_INDEX_URL,
SERVER_APP_ID,
} from '../../../common/constants';
import { telemetryMiddleware } from '../lib/telemetry';
import * as timelineActions from '../../timelines/store/actions';
@ -56,6 +55,7 @@ import { sourcererActions } from '../../sourcerer/store';
import { createMiddlewares } from './middlewares';
import { addNewTimeline } from '../../timelines/store/helpers';
import { initialNotesState } from '../../notes/store/notes.slice';
import { hasAccessToSecuritySolution } from '../../helpers_access';
let store: Store<State, Action> | 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',

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -28,7 +28,7 @@ jest.mock('../../../../common/lib/kibana', () => ({
application: {
getUrlForApp: jest.fn(),
capabilities: {
siem: {
siemV2: {
crud: true,
},
actions: {

View file

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

View file

@ -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<StepDefineRuleProps> = ({
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<StepDefineRuleProps> = ({
component={PickTimeline}
componentProps={{
idAria: 'detectionEngineStepDefineRuleTimeline',
isDisabled: isLoading,
isDisabled: isLoading || !canAttachTimelineTemplates,
dataTestSubj: 'detectionEngineStepDefineRuleTimeline',
}}
/>

View file

@ -15,6 +15,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 {
@ -83,6 +84,9 @@ export const useBulkActions = ({
const { executeBulkAction } = useExecuteBulkAction();
const { bulkExport } = useBulkExport();
const downloadExportedRules = useDownloadExportedRules();
const {
timelinePrivileges: { crud: canCreateTimelines },
} = useUserPrivileges();
const {
state: { isAllSelected, rules, loadingRuleIds, selectedRuleIds },
@ -431,7 +435,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
@ -595,6 +599,7 @@ export const useBulkActions = ({
filterOptions,
completeBulkEditForm,
startServices,
canCreateTimelines,
]
);

View file

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

View file

@ -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(
<TestProviders>
<InvestigateInTimelineAction {...props} />
</TestProviders>
);
expect(wrapper.getByTestId('send-alert-to-timeline-button')).toBeDisabled();
});
});

View file

@ -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<InvestigateInTimelineAction
ecsRowData,
onInvestigateInTimelineAlertClick,
});
const {
timelinePrivileges: { read },
} = useUserPrivileges();
const cannotReadTimeline = !read;
return (
<ActionIconItem
@ -40,7 +45,7 @@ const InvestigateInTimelineActionComponent: React.FC<InvestigateInTimelineAction
dataTestSubj="send-alert-to-timeline"
iconType="timeline"
onClick={investigateInTimelineAlertClick}
isDisabled={false}
isDisabled={cannotReadTimeline}
buttonType={buttonType}
/>
);

View file

@ -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',
@ -273,6 +275,9 @@ describe('useInvestigateInTimeline', () => {
},
},
});
(useUserPrivileges as jest.Mock).mockReturnValue({
timelinePrivileges: { read: true },
});
});
afterEach(() => {
jest.clearAllMocks();
@ -396,4 +401,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);
});
});
});

View file

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

View file

@ -26,7 +26,7 @@ describe('useUserInfo', () => {
services: {
application: {
capabilities: {
siem: {
siemV2: {
crud: true,
},
},
@ -68,7 +68,7 @@ describe('useUserInfo', () => {
const wrapper = ({ children }: React.PropsWithChildren) => (
<TestProviders>
<UserPrivilegesProvider
kibanaCapabilities={{ siem: { show: true, crud: true } } as unknown as Capabilities}
kibanaCapabilities={{ siemV2: { show: true, crud: true } } as unknown as Capabilities}
>
<ManageUserInfo>{children}</ManageUserInfo>
</UserPrivilegesProvider>

View file

@ -76,6 +76,8 @@ const userPrivilegesInitial: ReturnType<typeof useUserPrivileges> = {
canAccessFleet: false,
}),
kibanaSecuritySolutionsPrivileges: { crud: true, read: true },
timelinePrivileges: { crud: true, read: true },
notesPrivileges: { crud: true, read: true },
};
describe('useAlertsPrivileges', () => {

View file

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

View file

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

View file

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

View file

@ -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[] }) => (
<EuiContextMenu initialPanelId={0} panels={panels} />
);
@ -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');
});
});

View file

@ -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: (
<FormattedMessage
id="xpack.securitySolution.flyout.entityDetails.riskInputs.actions.addToNewCase"
defaultMessage="Add to new case"
/>
),
items: [
...(canAddToTimeline ? [timelinePanel] : []),
...(hasCasesPermissions
? [
{
name: (
<FormattedMessage
id="xpack.securitySolution.flyout.entityDetails.riskInputs.actions.addToNewCase"
defaultMessage="Add to new case"
/>
),
onClick: addToNewCaseClick,
},
onClick: addToNewCaseClick,
},
{
name: (
<FormattedMessage
id="xpack.securitySolution.flyout.entityDetails.riskInputs.actions.addToExistingCase"
defaultMessage="Add to existing case"
/>
),
{
name: (
<FormattedMessage
id="xpack.securitySolution.flyout.entityDetails.riskInputs.actions.addToExistingCase"
defaultMessage="Add to existing case"
/>
),
onClick: addToExistingCase,
},
]
: [timelinePanel],
onClick: addToExistingCase,
},
]
: []),
],
},
];
}, [addToExistingCase, addToNewCaseClick, addToNewTimeline, inputs, hasCasesPermissions]);
}, [
addToExistingCase,
addToNewCaseClick,
addToNewTimeline,
inputs,
hasCasesPermissions,
canAddToTimeline,
]);
};

View file

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

View file

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

View file

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

Some files were not shown because too many files have changed in this diff Show more