[8.x] [SecuritySolution] Add Service entity type to Entity Analytics (#204437) (#206725)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[SecuritySolution] Add Service entity type to Entity Analytics
(#204437)](https://github.com/elastic/kibana/pull/204437)

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

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

<!--BACKPORT [{"author":{"name":"Pablo
Machado","email":"pablo.nevesmachado@elastic.co"},"sourceCommit":{"committedDate":"2025-01-14T13:46:35Z","message":"[SecuritySolution]
Add Service entity type to Entity Analytics (#204437)\n\n## Summary\n\n*
Refactor types to prevent usages of `host` and `user`. \n * Use
`EntityType` instead. \n* Use a generic function that receives
`EntityType` as a parameter\ninstead of custom user and host
functions\n* Consolidate duplicated entity types\n* Add service to
entity types and update all references on the\nEntityAnalyticsDashboards
page, Risk score page and Entity Store page.\n* Refactor Risk score APIs
to be more generic and accept EntityType and\na param\n* Refactor if
statement like `isUserRiskScore` to be more generic and\naccept
`service`\n* Delete `RiskScoreEntity` in favour of `EntityType`.\n*
Update the branch to support the universal entity\n\n### Not included\n*
Service Flyout\n\n### Images\n\n![Screenshot 2025-01-06 at 15
57\n18](https://github.com/user-attachments/assets/a79444c0-d0a8-4838-bea0-5f0afdb58df4)\n![Screenshot
2025-01-06 at 16
02\n03](https://github.com/user-attachments/assets/1d007591-7ba5-416a-96b3-dc72db63d87b)\n![Screenshot
2025-01-06 at 16
08\n22](https://github.com/user-attachments/assets/014c49c0-ea6c-4e9a-bb88-75c862c16dd8)\n\n###
Generic Entity Support\nWe need to support risk score and asset
criticality for\nGeneric/Universal entities according
to\nhttps://github.com/elastic/security-team/issues/10740\n\n> We expect
that the below will be supported:\n> \n> Entity flyout for
service/generic entity\n> Entity risk scoring for service/generic
entity\n> Asset criticality assignments for service/generic
entity\n\nThis PR already implements that support. However, I have
introduced a\nfunction per feature that returns the enabled entity
types. At the\nmoment, I defined universal/generic entities as
unsupported for this PR\nto preserve the current behaviour. But to allow
universal/generic\nentities, we only need to delete a couple of
lines.\n\nRisk Score will need extra work because the entity types are
hard-coded\non some parts of the code.\n\n### How to test it\n1\n* Start
kabana with security solution data \n * You can use the document
generator with `yarn start entity-store`\n* Enable Entity Store and Risk
engine\n* Test the EA features, and they should work normally\n\n\n2\n*
Start kabana with security solution data \n * You can use the document
generator with `yarn start entity-store`\n* Enable the
`serviceEntityStoreEnabled` flag\n* Enable Entity Store and Risk
engine\n* Test the EA features, and you should see a new type of Entity
called\n'service'\n* Service Entity should work with all Entity
analytics features\n\n\n3 \n* Start kabana with security solution data
\n * You can use the document generator with `yarn start
entity-store`\n* Enable the `assetInventoryStoreEnabled` flag\n* Enable
Entity Store and Risk engine\n* Test the EA features, and you should not
see universal/generic entity\nexcept for the entity store status
pages\n\n\n\n\n### Checklist\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- [ ] [Flaky
Test\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\nused on any tests changed\n- [x] The PR description includes the
appropriate Release Notes section,\nand the correct `release_note:*`
label is applied per
the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n\n##
Release note\nAdd the \"service\" type to Security Entity Analytics -
Entity Store. It\nwill find services by the `service.name` field,
calculate risk score,\nand allow asset criticality
assignment.\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"6c31cf73ccae9f917038c51b4ad9e5c896f4bd28","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["v9.0.0","Team:
SecuritySolution","release_note:feature","Feature:Entity
Analytics","Team:Entity
Analytics","backport:version","v8.18.0"],"title":"[SecuritySolution] Add
Service entity type to Entity
Analytics","number":204437,"url":"https://github.com/elastic/kibana/pull/204437","mergeCommit":{"message":"[SecuritySolution]
Add Service entity type to Entity Analytics (#204437)\n\n## Summary\n\n*
Refactor types to prevent usages of `host` and `user`. \n * Use
`EntityType` instead. \n* Use a generic function that receives
`EntityType` as a parameter\ninstead of custom user and host
functions\n* Consolidate duplicated entity types\n* Add service to
entity types and update all references on the\nEntityAnalyticsDashboards
page, Risk score page and Entity Store page.\n* Refactor Risk score APIs
to be more generic and accept EntityType and\na param\n* Refactor if
statement like `isUserRiskScore` to be more generic and\naccept
`service`\n* Delete `RiskScoreEntity` in favour of `EntityType`.\n*
Update the branch to support the universal entity\n\n### Not included\n*
Service Flyout\n\n### Images\n\n![Screenshot 2025-01-06 at 15
57\n18](https://github.com/user-attachments/assets/a79444c0-d0a8-4838-bea0-5f0afdb58df4)\n![Screenshot
2025-01-06 at 16
02\n03](https://github.com/user-attachments/assets/1d007591-7ba5-416a-96b3-dc72db63d87b)\n![Screenshot
2025-01-06 at 16
08\n22](https://github.com/user-attachments/assets/014c49c0-ea6c-4e9a-bb88-75c862c16dd8)\n\n###
Generic Entity Support\nWe need to support risk score and asset
criticality for\nGeneric/Universal entities according
to\nhttps://github.com/elastic/security-team/issues/10740\n\n> We expect
that the below will be supported:\n> \n> Entity flyout for
service/generic entity\n> Entity risk scoring for service/generic
entity\n> Asset criticality assignments for service/generic
entity\n\nThis PR already implements that support. However, I have
introduced a\nfunction per feature that returns the enabled entity
types. At the\nmoment, I defined universal/generic entities as
unsupported for this PR\nto preserve the current behaviour. But to allow
universal/generic\nentities, we only need to delete a couple of
lines.\n\nRisk Score will need extra work because the entity types are
hard-coded\non some parts of the code.\n\n### How to test it\n1\n* Start
kabana with security solution data \n * You can use the document
generator with `yarn start entity-store`\n* Enable Entity Store and Risk
engine\n* Test the EA features, and they should work normally\n\n\n2\n*
Start kabana with security solution data \n * You can use the document
generator with `yarn start entity-store`\n* Enable the
`serviceEntityStoreEnabled` flag\n* Enable Entity Store and Risk
engine\n* Test the EA features, and you should see a new type of Entity
called\n'service'\n* Service Entity should work with all Entity
analytics features\n\n\n3 \n* Start kabana with security solution data
\n * You can use the document generator with `yarn start
entity-store`\n* Enable the `assetInventoryStoreEnabled` flag\n* Enable
Entity Store and Risk engine\n* Test the EA features, and you should not
see universal/generic entity\nexcept for the entity store status
pages\n\n\n\n\n### Checklist\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- [ ] [Flaky
Test\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\nused on any tests changed\n- [x] The PR description includes the
appropriate Release Notes section,\nand the correct `release_note:*`
label is applied per
the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n\n##
Release note\nAdd the \"service\" type to Security Entity Analytics -
Entity Store. It\nwill find services by the `service.name` field,
calculate risk score,\nand allow asset criticality
assignment.\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"6c31cf73ccae9f917038c51b4ad9e5c896f4bd28"}},"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/204437","number":204437,"mergeCommit":{"message":"[SecuritySolution]
Add Service entity type to Entity Analytics (#204437)\n\n## Summary\n\n*
Refactor types to prevent usages of `host` and `user`. \n * Use
`EntityType` instead. \n* Use a generic function that receives
`EntityType` as a parameter\ninstead of custom user and host
functions\n* Consolidate duplicated entity types\n* Add service to
entity types and update all references on the\nEntityAnalyticsDashboards
page, Risk score page and Entity Store page.\n* Refactor Risk score APIs
to be more generic and accept EntityType and\na param\n* Refactor if
statement like `isUserRiskScore` to be more generic and\naccept
`service`\n* Delete `RiskScoreEntity` in favour of `EntityType`.\n*
Update the branch to support the universal entity\n\n### Not included\n*
Service Flyout\n\n### Images\n\n![Screenshot 2025-01-06 at 15
57\n18](https://github.com/user-attachments/assets/a79444c0-d0a8-4838-bea0-5f0afdb58df4)\n![Screenshot
2025-01-06 at 16
02\n03](https://github.com/user-attachments/assets/1d007591-7ba5-416a-96b3-dc72db63d87b)\n![Screenshot
2025-01-06 at 16
08\n22](https://github.com/user-attachments/assets/014c49c0-ea6c-4e9a-bb88-75c862c16dd8)\n\n###
Generic Entity Support\nWe need to support risk score and asset
criticality for\nGeneric/Universal entities according
to\nhttps://github.com/elastic/security-team/issues/10740\n\n> We expect
that the below will be supported:\n> \n> Entity flyout for
service/generic entity\n> Entity risk scoring for service/generic
entity\n> Asset criticality assignments for service/generic
entity\n\nThis PR already implements that support. However, I have
introduced a\nfunction per feature that returns the enabled entity
types. At the\nmoment, I defined universal/generic entities as
unsupported for this PR\nto preserve the current behaviour. But to allow
universal/generic\nentities, we only need to delete a couple of
lines.\n\nRisk Score will need extra work because the entity types are
hard-coded\non some parts of the code.\n\n### How to test it\n1\n* Start
kabana with security solution data \n * You can use the document
generator with `yarn start entity-store`\n* Enable Entity Store and Risk
engine\n* Test the EA features, and they should work normally\n\n\n2\n*
Start kabana with security solution data \n * You can use the document
generator with `yarn start entity-store`\n* Enable the
`serviceEntityStoreEnabled` flag\n* Enable Entity Store and Risk
engine\n* Test the EA features, and you should see a new type of Entity
called\n'service'\n* Service Entity should work with all Entity
analytics features\n\n\n3 \n* Start kabana with security solution data
\n * You can use the document generator with `yarn start
entity-store`\n* Enable the `assetInventoryStoreEnabled` flag\n* Enable
Entity Store and Risk engine\n* Test the EA features, and you should not
see universal/generic entity\nexcept for the entity store status
pages\n\n\n\n\n### Checklist\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- [ ] [Flaky
Test\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\nused on any tests changed\n- [x] The PR description includes the
appropriate Release Notes section,\nand the correct `release_note:*`
label is applied per
the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n\n##
Release note\nAdd the \"service\" type to Security Entity Analytics -
Entity Store. It\nwill find services by the `service.name` field,
calculate risk score,\nand allow asset criticality
assignment.\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"6c31cf73ccae9f917038c51b4ad9e5c896f4bd28"}},{"branch":"8.x","label":"v8.18.0","branchLabelMappingKey":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Pablo Machado 2025-01-17 13:42:26 +01:00 committed by GitHub
parent 06063bba37
commit e6b7b5c9e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
216 changed files with 2538 additions and 1771 deletions

View file

@ -7723,7 +7723,7 @@ paths:
schema:
type: string
- in: query
name: entities_types
name: entity_types
required: true
schema:
items:
@ -45955,6 +45955,7 @@ components:
oneOf:
- $ref: '#/components/schemas/Security_Entity_Analytics_API_UserEntity'
- $ref: '#/components/schemas/Security_Entity_Analytics_API_HostEntity'
- $ref: '#/components/schemas/Security_Entity_Analytics_API_ServiceEntity'
Security_Entity_Analytics_API_EntityRiskLevels:
enum:
- Unknown
@ -46179,6 +46180,42 @@ components:
- index
- description
- category
Security_Entity_Analytics_API_ServiceEntity:
type: object
properties:
'@timestamp':
format: date-time
type: string
asset:
type: object
properties:
criticality:
$ref: '#/components/schemas/Security_Entity_Analytics_API_AssetCriticalityLevel'
required:
- criticality
entity:
type: object
properties:
name:
type: string
source:
type: string
required:
- name
- source
service:
type: object
properties:
name:
type: string
risk:
$ref: '#/components/schemas/Security_Entity_Analytics_API_EntityRiskScoreRecord'
required:
- name
required:
- '@timestamp'
- service
- entity
Security_Entity_Analytics_API_StoreStatus:
enum:
- not_installed

View file

@ -13296,7 +13296,7 @@ paths:
schema:
type: string
- in: query
name: entities_types
name: entity_types
required: true
schema:
items:
@ -35103,6 +35103,7 @@ components:
oneOf:
- $ref: '#/components/schemas/Security_Entity_Analytics_API_UserEntity'
- $ref: '#/components/schemas/Security_Entity_Analytics_API_HostEntity'
- $ref: '#/components/schemas/Security_Entity_Analytics_API_ServiceEntity'
Security_Entity_Analytics_API_EntityRiskLevels:
enum:
- Unknown
@ -35327,6 +35328,42 @@ components:
- index
- description
- category
Security_Entity_Analytics_API_ServiceEntity:
type: object
properties:
'@timestamp':
format: date-time
type: string
asset:
type: object
properties:
criticality:
$ref: '#/components/schemas/Security_Entity_Analytics_API_AssetCriticalityLevel'
required:
- criticality
entity:
type: object
properties:
name:
type: string
source:
type: string
required:
- name
- source
service:
type: object
properties:
name:
type: string
risk:
$ref: '#/components/schemas/Security_Entity_Analytics_API_EntityRiskScoreRecord'
required:
- name
required:
- '@timestamp'
- service
- entity
Security_Entity_Analytics_API_StoreStatus:
enum:
- not_installed

View file

@ -36745,7 +36745,6 @@
"xpack.securitySolution.alertCountByRuleByStatus.ruleName": "kibana.alert.rule.name",
"xpack.securitySolution.alertCountByRuleByStatus.status": "Statut",
"xpack.securitySolution.alertCountByRuleByStatus.tooltipTitle": "Nom de règle",
"xpack.securitySolution.alertDetails.overview.hostRiskDataTitle": "Données de risque de {riskEntity}",
"xpack.securitySolution.alertDetails.summary.readLess": "Lire moins",
"xpack.securitySolution.alertDetails.summary.readMore": "En savoir plus",
"xpack.securitySolution.alerts.badge.readOnly.tooltip": "Impossible de mettre à jour les alertes",
@ -39209,7 +39208,6 @@
"xpack.securitySolution.enableRiskScore.enableRiskScore": "Activer le score de risque de {riskEntity}",
"xpack.securitySolution.enableRiskScore.enableRiskScoreDescription": "Une fois que vous avez activé cette fonctionnalité, vous pouvez obtenir un accès rapide aux scores de risque de {riskEntity} dans cette section. Les données pourront prendre jusqu'à une heure pour être générées après l'activation du module.",
"xpack.securitySolution.enableRiskScore.enableRiskScorePopoverTitle": "Les alertes doivent être disponibles avant d'activer le module",
"xpack.securitySolution.enableRiskScore.upgradeRiskScore": "Mettre à niveau le score de risque de {riskEntity}",
"xpack.securitySolution.endpoint.action.chooseFromTheList": "Choisissez une action dans la liste",
"xpack.securitySolution.endpoint.action.permissionDenied": "Autorisation refusée",
"xpack.securitySolution.endpoint.actions.agentDetails": "Afficher les détails de l'agent",
@ -40104,8 +40102,6 @@
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.acceptedFileFormats": "Formats de fichiers : {formats}",
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.advancedSettingDisabledMessage": "Ils ne disposent pas des privilèges nécessaires pour accéder à la fonctionnalité Criticité des ressources. Contactez votre administrateur si vous avez besoin d'aide.",
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.assetCriticalityLabels": "Niveau de criticité : Spécifiez n'importe laquelle de ces {labels}",
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.assetIdentifierDescription": "Identificateur : Spécifiez le {hostName} ou le {userName} de l'entité.",
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.assetTypeDescription": "Type d'entité : Veuillez indiquer si l'entité est un {host} ou un {user}.",
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.csvFileFormatRequirements": "Formats et taille de fichiers pris en charge",
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.CSVStructureTitle": "Structure de fichiers requise",
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.description": "Assignez en groupe la criticité des ressources en important un fichier CSV, TXT ou TSV exporté depuis vos outils de gestion des ressources. Cela garantit lexactitude des données et réduit les erreurs de saisie manuelle.",
@ -40168,8 +40164,6 @@
"xpack.securitySolution.entityAnalytics.entityStoreManagementPage.title": "Stockage d'entités",
"xpack.securitySolution.entityAnalytics.header.anomalies": "Anomalies",
"xpack.securitySolution.entityAnalytics.header.criticalHosts": "Hôtes critiques",
"xpack.securitySolution.entityAnalytics.header.criticalUsers": "Utilisateurs critiques",
"xpack.securitySolution.entityAnalytics.hostsRiskDashboard.title": "Scores de risque de l'hôte",
"xpack.securitySolution.entityAnalytics.learnMore": "En savoir plus sur la notation des risques des entités",
"xpack.securitySolution.entityAnalytics.riskDashboard.lastUpdatedTitle": "Dernière mise à jour",
"xpack.securitySolution.entityAnalytics.riskDashboard.nameTitle": "Nom de {riskEntity}",
@ -40182,7 +40176,6 @@
"xpack.securitySolution.entityAnalytics.riskScore.chart.totalLabel": "Total",
"xpack.securitySolution.entityAnalytics.riskScore.donut_chart.totalLabel": "Total",
"xpack.securitySolution.entityAnalytics.technicalPreviewLabel": "Version d'évaluation technique",
"xpack.securitySolution.entityAnalytics.usersRiskDashboard.title": "Scores de risque de l'utilisateur",
"xpack.securitySolution.entityDetails.userPanel.error": "Une erreur a été rencontrée lors du calcul du score de risque de {entity}",
"xpack.securitySolution.event.module.linkToElasticEndpointSecurityDescription": "Ouvrir dans Endpoint Security",
"xpack.securitySolution.event.summary.threat_indicator.modal.allMatches": "Toutes les correspondances d'indicateur",
@ -41815,7 +41808,6 @@
"xpack.securitySolution.responseActionsList.list.time": "Heure",
"xpack.securitySolution.responseActionsList.list.user": "Utilisateur",
"xpack.securitySolution.risk_score.toast.viewDashboard": "Afficher le tableau de bord",
"xpack.securitySolution.riskDeprecated.entity.upgradeRiskScoreDescription": "Les données actuelles ne sont plus prises en charge. Veuillez migrer vos données et mettre à niveau le module. Les données pourront prendre jusqu'à une heure pour être générées après l'activation du module.",
"xpack.securitySolution.riskEngine.missingPrivilegesCallOut.messageBody.clusterPrivilegesTitle": "Privilèges de cluster Elasticsearch manquants :",
"xpack.securitySolution.riskEngine.missingPrivilegesCallOut.messageBody.essenceDescription": "Vous avez besoin des privilèges suivants pour accéder totalement à cette fonctionnalité. Contactez votre administrateur si vous avez besoin d'aide. En savoir plus sur {docs}.",
"xpack.securitySolution.riskEngine.missingPrivilegesCallOut.messageBody.indexPrivilegesTitle": "Privilèges d'index Elasticsearch manquants :",
@ -41864,8 +41856,6 @@
"xpack.securitySolution.riskScore.errors.privileges.needToHave": "Vous devez avoir :",
"xpack.securitySolution.riskScore.failSearchDescription": "Impossible de lancer une recherche sur le score de risque",
"xpack.securitySolution.riskScore.hostRiskScoresEnabledTitle": "Scores de risque de l'hôte activés",
"xpack.securitySolution.riskScore.hostsDashboardWarningPanelBody": "Nous navons pas trouvé de données de score de risque de lhôte. Vérifiez si vous avez des filtres globaux dans la barre de recherche KQL globale. Si vous venez dactiver le module de risque de lhôte, le moteur de risque peut mettre une heure à générer les données de score de risque de lhôte et les afficher dans ce panneau.",
"xpack.securitySolution.riskScore.hostsDashboardWarningPanelTitle": "Aucune donnée de score de risque de l'hôte disponible pour l'affichage",
"xpack.securitySolution.riskScore.install.errorMessageTitle": "Erreur d'installation",
"xpack.securitySolution.riskScore.kpi.failSearchDescription": "Impossible de lancer une recherche sur le score de risque",
"xpack.securitySolution.riskScore.maxSpacePanel.message": "Vous pouvez désactiver l'évaluation de l'entité dans l'espace où elle est actuellement activée avant de l'activer dans cet espace",
@ -41892,8 +41882,6 @@
"xpack.securitySolution.riskScore.riskScorePreview.entityRiskScoring": "Score de risque des entités",
"xpack.securitySolution.riskScore.riskScorePreview.errorMessage": "Un problème est survenu lors de la création de l'aperçu. Veuillez réessayer.",
"xpack.securitySolution.riskScore.riskScorePreview.errorTitle": "Erreur de l'aperçu",
"xpack.securitySolution.riskScore.riskScorePreview.hosts.hide": "Masquer les hôtes",
"xpack.securitySolution.riskScore.riskScorePreview.hosts.show": "Afficher les hôtes",
"xpack.securitySolution.riskScore.riskScorePreview.missingPermissionsCallout.description": "L'autorisation de lecture est requise pour le modèle d'index {index} afin de prévisualiser les données. Contactez votre administrateur si vous avez besoin d'aide.",
"xpack.securitySolution.riskScore.riskScorePreview.missingPermissionsCallout.title": "Les privilèges d'index sont insuffisants pour pouvoir afficher un aperçu des données",
"xpack.securitySolution.riskScore.riskScorePreview.preview": "Aperçu",
@ -41935,8 +41923,6 @@
"xpack.securitySolution.riskScore.updatingRiskEngine": "Mise à jour du moteur de risque...",
"xpack.securitySolution.riskScore.userRiskScoresEnabledTitle": "Scores de risque de l'utilisateur activés",
"xpack.securitySolution.riskScore.usersDashboardRestartTooltip": "Le calcul du score de risque pourra prendre un certain temps à se lancer. Cependant, en appuyant sur Redémarrer, vous pouvez le forcer à s'exécuter immédiatement.",
"xpack.securitySolution.riskScore.usersDashboardWarningPanelBody": "Nous navons pas trouvé de données de score de risque de lutilisateur. Vérifiez si vous avez des filtres globaux dans la barre de recherche KQL globale. Si vous venez dactiver le module de risque de lutilisateur, le moteur de risque peut mettre une heure à générer les données de score de risque de lutilisateur et à les afficher dans ce panneau.",
"xpack.securitySolution.riskScore.usersDashboardWarningPanelTitle": "Aucune donnée de score de risque de l'utilisateur disponible pour l'affichage",
"xpack.securitySolution.riskTabBody.scoreOverTimeTitle": "Score de risque de {riskEntity} sur la durée",
"xpack.securitySolution.riskTabBody.viewDashboardButtonLabel": "Afficher le tableau de bord de la source",
"xpack.securitySolution.rowRenderer.executedProcessDescription": "processus exécuté",

View file

@ -36603,7 +36603,6 @@
"xpack.securitySolution.alertCountByRuleByStatus.ruleName": "kibana.alert.rule.name",
"xpack.securitySolution.alertCountByRuleByStatus.status": "ステータス",
"xpack.securitySolution.alertCountByRuleByStatus.tooltipTitle": "ルール名",
"xpack.securitySolution.alertDetails.overview.hostRiskDataTitle": "{riskEntity}リスクデータ",
"xpack.securitySolution.alertDetails.summary.readLess": "表示を減らす",
"xpack.securitySolution.alertDetails.summary.readMore": "続きを読む",
"xpack.securitySolution.alerts.badge.readOnly.tooltip": "アラートを更新できません",
@ -39065,7 +39064,6 @@
"xpack.securitySolution.enableRiskScore.enableRiskScore": "{riskEntity}リスクスコアを有効化",
"xpack.securitySolution.enableRiskScore.enableRiskScoreDescription": "この機能を有効化すると、このセクションで{riskEntity}リスクスコアにすばやくアクセスできます。モジュールを有効化した後、データの生成までに1時間かかる場合があります。",
"xpack.securitySolution.enableRiskScore.enableRiskScorePopoverTitle": "モジュールを有効にする前に、アラートが使用可能でなければなりません",
"xpack.securitySolution.enableRiskScore.upgradeRiskScore": "{riskEntity}リスクスコアをアップグレード",
"xpack.securitySolution.endpoint.action.chooseFromTheList": "リストからアクションを選択",
"xpack.securitySolution.endpoint.action.permissionDenied": "パーミッションが拒否されました",
"xpack.securitySolution.endpoint.actions.agentDetails": "エージェント詳細を表示",
@ -39959,8 +39957,6 @@
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.acceptedFileFormats": "ファイル形式:{formats}",
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.advancedSettingDisabledMessage": "アセット重要度機能にアクセスする権限がありません。サポートについては、管理者にお問い合わせください。",
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.assetCriticalityLabels": "重要度レベル:{labels}のいずれかを指定",
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.assetIdentifierDescription": "識別子:エンティティの{hostName}または{userName}を指定します。",
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.assetTypeDescription": "エンティティタイプ:エンティティが{host}か{user}かを示します。",
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.csvFileFormatRequirements": "サポートされているファイル形式とサイズ",
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.CSVStructureTitle": "必要なファイル構造",
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.description": "アセット管理ツールからエクスポートされたCSV、TXT、TSVファイルをインポートして、アセット重要度を一括で割り当てます。これにより、データの精度が保証され、手作業の入力エラーが減ります。",
@ -40023,8 +40019,6 @@
"xpack.securitySolution.entityAnalytics.entityStoreManagementPage.title": "エンティティストア",
"xpack.securitySolution.entityAnalytics.header.anomalies": "異常",
"xpack.securitySolution.entityAnalytics.header.criticalHosts": "重要なホスト",
"xpack.securitySolution.entityAnalytics.header.criticalUsers": "重要なユーザー",
"xpack.securitySolution.entityAnalytics.hostsRiskDashboard.title": "ホストリスクスコア",
"xpack.securitySolution.entityAnalytics.learnMore": "エンティティリスクスコアの詳細",
"xpack.securitySolution.entityAnalytics.riskDashboard.lastUpdatedTitle": "最終更新",
"xpack.securitySolution.entityAnalytics.riskDashboard.nameTitle": "{riskEntity}名",
@ -40037,7 +40031,6 @@
"xpack.securitySolution.entityAnalytics.riskScore.chart.totalLabel": "合計",
"xpack.securitySolution.entityAnalytics.riskScore.donut_chart.totalLabel": "合計",
"xpack.securitySolution.entityAnalytics.technicalPreviewLabel": "テクニカルプレビュー",
"xpack.securitySolution.entityAnalytics.usersRiskDashboard.title": "ユーザーリスクスコア",
"xpack.securitySolution.entityDetails.userPanel.error": "{entity}リスクスコアの計算中に問題が発生しました",
"xpack.securitySolution.event.module.linkToElasticEndpointSecurityDescription": "Endpoint Securityで開く",
"xpack.securitySolution.event.summary.threat_indicator.modal.allMatches": "すべてのインジケーター一致",
@ -41672,7 +41665,6 @@
"xpack.securitySolution.responseActionsList.list.time": "時間",
"xpack.securitySolution.responseActionsList.list.user": "ユーザー",
"xpack.securitySolution.risk_score.toast.viewDashboard": "ダッシュボードを表示",
"xpack.securitySolution.riskDeprecated.entity.upgradeRiskScoreDescription": "現在のデータはサポートされていません。データを移行し、モジュールをアップグレードしてください。モジュールを有効化した後、データの生成までに1時間かかる場合があります。",
"xpack.securitySolution.riskEngine.missingPrivilegesCallOut.messageBody.clusterPrivilegesTitle": "不足しているElasticsearchクラスター権限",
"xpack.securitySolution.riskEngine.missingPrivilegesCallOut.messageBody.essenceDescription": "この機能のすべてにアクセスするには、次の権限が必要です。サポートについては、管理者にお問い合わせください。{docs}の詳細をお読みください。",
"xpack.securitySolution.riskEngine.missingPrivilegesCallOut.messageBody.indexPrivilegesTitle": "Elasticsearchインデックス権限がありません。",
@ -41721,8 +41713,6 @@
"xpack.securitySolution.riskScore.errors.privileges.needToHave": "次の項目が必要です。",
"xpack.securitySolution.riskScore.failSearchDescription": "リスクスコアで検索を実行できませんでした",
"xpack.securitySolution.riskScore.hostRiskScoresEnabledTitle": "ホストリスクスコア有効",
"xpack.securitySolution.riskScore.hostsDashboardWarningPanelBody": "ホストリスクスコアデータが見つかりません。グローバルKQL検索バーにグローバルフィルターがあるかどうかを確認してください。ホストリスクモジュールを有効にしたばかりの場合は、リスクエンジンがホストリスクスコアデータを生成し、このパネルに表示するまでに1時間かかることがあります。",
"xpack.securitySolution.riskScore.hostsDashboardWarningPanelTitle": "表示するホストリスクスコアデータがありません",
"xpack.securitySolution.riskScore.install.errorMessageTitle": "インストールエラー",
"xpack.securitySolution.riskScore.kpi.failSearchDescription": "リスクスコアで検索を実行できませんでした",
"xpack.securitySolution.riskScore.maxSpacePanel.message": "このスペースで有効化する前に、現在有効なスペースでエンティティリスクスコアリングを無効化できます。",
@ -41749,8 +41739,6 @@
"xpack.securitySolution.riskScore.riskScorePreview.entityRiskScoring": "エンティティリスクスコア",
"xpack.securitySolution.riskScore.riskScorePreview.errorMessage": "プレビューを作成しているときに問題が発生しました。再試行してください。",
"xpack.securitySolution.riskScore.riskScorePreview.errorTitle": "プレビューが失敗しました",
"xpack.securitySolution.riskScore.riskScorePreview.hosts.hide": "ホストを非表示",
"xpack.securitySolution.riskScore.riskScorePreview.hosts.show": "ホストを表示",
"xpack.securitySolution.riskScore.riskScorePreview.missingPermissionsCallout.description": "データをプレビューするには、{index}インデックスパターンの読み取り権限が必要です。サポートについては、管理者にお問い合わせください。",
"xpack.securitySolution.riskScore.riskScorePreview.missingPermissionsCallout.title": "データをプレビューする十分なインデックス権限がありません",
"xpack.securitySolution.riskScore.riskScorePreview.preview": "プレビュー",
@ -41792,8 +41780,6 @@
"xpack.securitySolution.riskScore.updatingRiskEngine": "リスクエンジンを更新中...",
"xpack.securitySolution.riskScore.userRiskScoresEnabledTitle": "ユーザーリスクスコア有効",
"xpack.securitySolution.riskScore.usersDashboardRestartTooltip": "リスクスコア計算の実行には少し時間がかかる場合があります。ただし、再起動を押すと、すぐに強制的に実行できます。",
"xpack.securitySolution.riskScore.usersDashboardWarningPanelBody": "ユーザーリスクスコアデータが見つかりません。グローバルKQL検索バーにグローバルフィルターがあるかどうかを確認してください。ユーザーリスクモジュールを有効にしたばかりの場合は、リスクエンジンがユーザーリスクスコアデータを生成し、このパネルに表示するまでに1時間かかることがあります。",
"xpack.securitySolution.riskScore.usersDashboardWarningPanelTitle": "表示するユーザーリスクスコアデータがありません",
"xpack.securitySolution.riskTabBody.scoreOverTimeTitle": "経時的な{riskEntity}リスクスコア",
"xpack.securitySolution.riskTabBody.viewDashboardButtonLabel": "ソースダッシュボードを表示",
"xpack.securitySolution.rowRenderer.executedProcessDescription": "実行されたプロセス",

View file

@ -36697,7 +36697,6 @@
"xpack.securitySolution.alertCountByRuleByStatus.ruleName": "kibana.alert.rule.name",
"xpack.securitySolution.alertCountByRuleByStatus.status": "状态",
"xpack.securitySolution.alertCountByRuleByStatus.tooltipTitle": "规则名称",
"xpack.securitySolution.alertDetails.overview.hostRiskDataTitle": "{riskEntity}风险数据",
"xpack.securitySolution.alertDetails.summary.readLess": "阅读更少内容",
"xpack.securitySolution.alertDetails.summary.readMore": "阅读更多内容",
"xpack.securitySolution.alerts.badge.readOnly.tooltip": "无法更新告警",
@ -39161,7 +39160,6 @@
"xpack.securitySolution.enableRiskScore.enableRiskScore": "启用{riskEntity}风险分数",
"xpack.securitySolution.enableRiskScore.enableRiskScoreDescription": "一旦启用此功能,您将可以在此部分快速访问{riskEntity}风险分数。启用此模板后,可能需要一小时才能生成数据。",
"xpack.securitySolution.enableRiskScore.enableRiskScorePopoverTitle": "启用模块之前,告警需要处于可用状态",
"xpack.securitySolution.enableRiskScore.upgradeRiskScore": "升级{riskEntity}风险分数",
"xpack.securitySolution.endpoint.action.chooseFromTheList": "从列表中选择操作",
"xpack.securitySolution.endpoint.action.permissionDenied": "权限被拒绝",
"xpack.securitySolution.endpoint.actions.agentDetails": "查看代理详情",
@ -40056,8 +40054,6 @@
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.acceptedFileFormats": "文件格式:{formats}",
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.advancedSettingDisabledMessage": "无权访问资产关键度功能。有关进一步帮助,请联系您的管理员。",
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.assetCriticalityLabels": "关键度级别:指定任意 {labels}",
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.assetIdentifierDescription": "标识符:指定实体的 {hostName} 或 {userName}。",
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.assetTypeDescription": "实体类型:指示实体是 {host} 还是 {user}。",
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.csvFileFormatRequirements": "支持的文件格式和大小",
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.CSVStructureTitle": "所需的文件结构",
"xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.description": "通过导入从资产管理工具中导出的 CSV、TXT 或 TSV 文件来批量分配资产关键度。这确保了数据准确度,并减少了手动输入错误。",
@ -40120,8 +40116,6 @@
"xpack.securitySolution.entityAnalytics.entityStoreManagementPage.title": "实体仓库",
"xpack.securitySolution.entityAnalytics.header.anomalies": "异常",
"xpack.securitySolution.entityAnalytics.header.criticalHosts": "关键主机",
"xpack.securitySolution.entityAnalytics.header.criticalUsers": "关键用户",
"xpack.securitySolution.entityAnalytics.hostsRiskDashboard.title": "主机风险分数",
"xpack.securitySolution.entityAnalytics.learnMore": "详细了解实体风险评分",
"xpack.securitySolution.entityAnalytics.riskDashboard.lastUpdatedTitle": "上次更新时间",
"xpack.securitySolution.entityAnalytics.riskDashboard.nameTitle": "{riskEntity}名称",
@ -40134,7 +40128,6 @@
"xpack.securitySolution.entityAnalytics.riskScore.chart.totalLabel": "合计",
"xpack.securitySolution.entityAnalytics.riskScore.donut_chart.totalLabel": "合计",
"xpack.securitySolution.entityAnalytics.technicalPreviewLabel": "技术预览",
"xpack.securitySolution.entityAnalytics.usersRiskDashboard.title": "用户风险分数",
"xpack.securitySolution.entityDetails.userPanel.error": "计算 {entity} 风险分数时出现问题",
"xpack.securitySolution.event.module.linkToElasticEndpointSecurityDescription": "在 Endpoint Security 中打开",
"xpack.securitySolution.event.summary.threat_indicator.modal.allMatches": "所有指标匹配",
@ -41768,7 +41761,6 @@
"xpack.securitySolution.responseActionsList.list.time": "时间",
"xpack.securitySolution.responseActionsList.list.user": "用户",
"xpack.securitySolution.risk_score.toast.viewDashboard": "查看仪表板",
"xpack.securitySolution.riskDeprecated.entity.upgradeRiskScoreDescription": "当前数据不再受支持。请迁移您的数据并升级该模块。启用此模板后,可能需要一小时才能生成数据。",
"xpack.securitySolution.riskEngine.missingPrivilegesCallOut.messageBody.clusterPrivilegesTitle": "缺少 Elasticsearch 集群权限:",
"xpack.securitySolution.riskEngine.missingPrivilegesCallOut.messageBody.essenceDescription": "您需要以下权限,才能完全使用此功能。有关进一步帮助,请联系您的管理员。阅读有关 {docs} 的更多内容。",
"xpack.securitySolution.riskEngine.missingPrivilegesCallOut.messageBody.indexPrivilegesTitle": "缺少 Elasticsearch 索引权限:",
@ -41817,8 +41809,6 @@
"xpack.securitySolution.riskScore.errors.privileges.needToHave": "您需要具有:",
"xpack.securitySolution.riskScore.failSearchDescription": "无法对风险分数执行搜索",
"xpack.securitySolution.riskScore.hostRiskScoresEnabledTitle": "已启用主机风险分数",
"xpack.securitySolution.riskScore.hostsDashboardWarningPanelBody": "找不到任何主机风险分数数据。检查全局 KQL 搜索栏中是否具有任何全局筛选。如果刚刚启用了主机风险模块,风险引擎可能需要一小时才能生成并在此面板中显示主机风险分数数据。",
"xpack.securitySolution.riskScore.hostsDashboardWarningPanelTitle": "没有可显示的主机风险分数数据",
"xpack.securitySolution.riskScore.install.errorMessageTitle": "安装错误",
"xpack.securitySolution.riskScore.kpi.failSearchDescription": "无法对风险分数执行搜索",
"xpack.securitySolution.riskScore.maxSpacePanel.message": "在此工作区中启用实体风险评分之前,您可以在当前已启用实体风险评分的工作区中将其禁用",
@ -41845,8 +41835,6 @@
"xpack.securitySolution.riskScore.riskScorePreview.entityRiskScoring": "实体风险分数",
"xpack.securitySolution.riskScore.riskScorePreview.errorMessage": "创建预览时出现了问题。请重试。",
"xpack.securitySolution.riskScore.riskScorePreview.errorTitle": "预览失败",
"xpack.securitySolution.riskScore.riskScorePreview.hosts.hide": "隐藏主机",
"xpack.securitySolution.riskScore.riskScorePreview.hosts.show": "显示主机",
"xpack.securitySolution.riskScore.riskScorePreview.missingPermissionsCallout.description": "{index} 索引模式需要读取权限才能预览数据。有关进一步帮助,请联系您的管理员。",
"xpack.securitySolution.riskScore.riskScorePreview.missingPermissionsCallout.title": "索引权限不足,无法预览数据",
"xpack.securitySolution.riskScore.riskScorePreview.preview": "预览",
@ -41888,8 +41876,6 @@
"xpack.securitySolution.riskScore.updatingRiskEngine": "正在更新风险引擎......",
"xpack.securitySolution.riskScore.userRiskScoresEnabledTitle": "已启用用户风险分数",
"xpack.securitySolution.riskScore.usersDashboardRestartTooltip": "风险分数计算可能需要一段时间运行。但是,通过按“重新启动”,您可以立即强制运行该计算。",
"xpack.securitySolution.riskScore.usersDashboardWarningPanelBody": "找不到任何用户风险分数数据。检查全局 KQL 搜索栏中是否具有任何全局筛选。如果刚刚启用了用户风险模块,风险引擎可能需要一小时才能生成并在此面板中显示用户风险分数数据。",
"xpack.securitySolution.riskScore.usersDashboardWarningPanelTitle": "没有可显示的用户风险分数数据",
"xpack.securitySolution.riskTabBody.scoreOverTimeTitle": "一段时间的{riskEntity}风险分数",
"xpack.securitySolution.riskTabBody.viewDashboardButtonLabel": "查看源仪表板",
"xpack.securitySolution.rowRenderer.executedProcessDescription": "已执行进程",

View file

@ -40,6 +40,7 @@ export const AfterKeys = z.object({
host: EntityAfterKey.optional(),
user: EntityAfterKey.optional(),
service: EntityAfterKey.optional(),
universal: EntityAfterKey.optional(),
});
/**
@ -73,7 +74,7 @@ export const DateRange = z.object({
});
export type IdentifierType = z.infer<typeof IdentifierType>;
export const IdentifierType = z.enum(['host', 'user', 'service']);
export const IdentifierType = z.enum(['host', 'user', 'service', 'universal']);
export type IdentifierTypeEnum = typeof IdentifierType.enum;
export const IdentifierTypeEnum = IdentifierType.enum;
@ -176,6 +177,7 @@ export const RiskScoreWeightInternal = z.union([
host: RiskScoreEntityIdentifierWeights,
user: RiskScoreEntityIdentifierWeights.optional(),
service: RiskScoreEntityIdentifierWeights.optional(),
universal: RiskScoreEntityIdentifierWeights.optional(),
})
),
RiskScoreWeightGlobalShared.merge(
@ -183,6 +185,7 @@ export const RiskScoreWeightInternal = z.union([
host: RiskScoreEntityIdentifierWeights.optional(),
user: RiskScoreEntityIdentifierWeights,
service: RiskScoreEntityIdentifierWeights.optional(),
universal: RiskScoreEntityIdentifierWeights.optional(),
})
),
RiskScoreWeightGlobalShared.merge(
@ -190,6 +193,7 @@ export const RiskScoreWeightInternal = z.union([
host: RiskScoreEntityIdentifierWeights.optional(),
user: RiskScoreEntityIdentifierWeights.optional(),
service: RiskScoreEntityIdentifierWeights,
universal: RiskScoreEntityIdentifierWeights.optional(),
})
),
]);

View file

@ -54,6 +54,8 @@ components:
$ref: '#/components/schemas/EntityAfterKey'
service:
$ref: '#/components/schemas/EntityAfterKey'
universal:
$ref: '#/components/schemas/EntityAfterKey'
example:
host:
'host.name': 'example.host'
@ -100,6 +102,7 @@ components:
- host
- user
- service
- universal
RiskScoreInput:
description: A generic representation of a document contributing to a Risk Score.
@ -255,6 +258,8 @@ components:
$ref: '#/components/schemas/RiskScoreEntityIdentifierWeights'
service:
$ref: '#/components/schemas/RiskScoreEntityIdentifierWeights'
universal:
$ref: '#/components/schemas/RiskScoreEntityIdentifierWeights'
- allOf:
- $ref: '#/components/schemas/RiskScoreWeightGlobalShared'
@ -268,6 +273,8 @@ components:
$ref: '#/components/schemas/RiskScoreEntityIdentifierWeights'
service:
$ref: '#/components/schemas/RiskScoreEntityIdentifierWeights'
universal:
$ref: '#/components/schemas/RiskScoreEntityIdentifierWeights'
- allOf:
- $ref: '#/components/schemas/RiskScoreWeightGlobalShared'
- type: object
@ -280,6 +287,8 @@ components:
$ref: '#/components/schemas/RiskScoreEntityIdentifierWeights'
service:
$ref: '#/components/schemas/RiskScoreEntityIdentifierWeights'
universal:
$ref: '#/components/schemas/RiskScoreEntityIdentifierWeights'
RiskScoreWeights:
description: 'A list of weights to be applied to the scoring calculation.'
type: array

View file

@ -68,5 +68,25 @@ export const HostEntity = z.object({
.optional(),
});
export type Entity = z.infer<typeof Entity>;
export const Entity = z.union([UserEntity, HostEntity]);
export type ServiceEntity = z.infer<typeof ServiceEntity>;
export const ServiceEntity = z.object({
'@timestamp': z.string().datetime(),
entity: z.object({
name: z.string(),
source: z.string(),
}),
service: z.object({
name: z.string(),
risk: EntityRiskScoreRecord.optional(),
}),
asset: z
.object({
criticality: AssetCriticalityLevel,
})
.optional(),
});
export const EntityInternal = z.union([UserEntity, HostEntity, ServiceEntity]);
export type Entity = z.infer<typeof EntityInternal>;
export const Entity = EntityInternal as z.ZodType<Entity>;

View file

@ -130,8 +130,44 @@ components:
$ref: '../../asset_criticality/common.schema.yaml#/components/schemas/AssetCriticalityLevel'
required:
- criticality
ServiceEntity:
type: object
required:
- "@timestamp"
- service
- entity
properties:
"@timestamp":
type: string
format: date-time
entity:
type: object
required:
- name
- source
properties:
name:
type: string
source:
type: string
service:
type: object
properties:
name:
type: string
risk:
$ref: '../../common/common.schema.yaml#/components/schemas/EntityRiskScoreRecord'
required:
- name
asset:
type: object
properties:
criticality:
$ref: '../../asset_criticality/common.schema.yaml#/components/schemas/AssetCriticalityLevel'
required:
- criticality
Entity:
oneOf:
- $ref: '#/components/schemas/UserEntity'
- $ref: '#/components/schemas/HostEntity'
- $ref: '#/components/schemas/ServiceEntity'

View file

@ -30,7 +30,7 @@ export const ListEntitiesRequestQuery = z.object({
* An ES query to filter by.
*/
filterQuery: z.string().optional(),
entities_types: ArrayFromString(EntityType),
entity_types: ArrayFromString(EntityType),
});
export type ListEntitiesRequestQueryInput = z.input<typeof ListEntitiesRequestQuery>;

View file

@ -44,7 +44,7 @@ paths:
schema:
type: string
description: An ES query to filter by.
- name: entities_types
- name: entity_types
in: query
required: true
schema:

View file

@ -46,6 +46,10 @@ export const RiskScoresCalculationResponse = z.object({
* A list of service risk scores
*/
service: z.array(EntityRiskScoreRecord).optional(),
/**
* A list of universal risk scores
*/
universal: z.array(EntityRiskScoreRecord).optional(),
/**
* If 'wait_for' the request will wait for the index refresh.
*/

View file

@ -45,6 +45,11 @@ components:
items:
$ref: '../common/common.schema.yaml#/components/schemas/EntityRiskScoreRecord'
description: A list of service risk scores
universal:
type: array
items:
$ref: '../common/common.schema.yaml#/components/schemas/EntityRiskScoreRecord'
description: A list of universal risk scores
refresh:
type: string
enum: [wait_for]

View file

@ -85,6 +85,10 @@ export const RiskScoresPreviewResponse = z.object({
* A list of service risk scores
*/
service: z.array(EntityRiskScoreRecord).optional(),
/**
* A list of universal risk scores
*/
universal: z.array(EntityRiskScoreRecord).optional(),
}),
});

View file

@ -95,3 +95,8 @@ components:
items:
$ref: '../common/common.schema.yaml#/components/schemas/EntityRiskScoreRecord'
description: A list of service risk scores
universal:
type: array
items:
$ref: '../common/common.schema.yaml#/components/schemas/EntityRiskScoreRecord'
description: A list of universal risk scores

View file

@ -6,13 +6,13 @@
*/
import { schema } from '@kbn/config-schema';
import { RiskScoreEntity } from '../../../../search_strategy';
import { EntityType } from '../../../../search_strategy';
export const onboardingRiskScoreRequestBody = {
body: schema.object({
riskScoreEntity: schema.oneOf([
schema.literal(RiskScoreEntity.host),
schema.literal(RiskScoreEntity.user),
schema.literal(EntityType.host),
schema.literal(EntityType.user),
]),
}),
};

View file

@ -35,9 +35,8 @@ import {
} from './related_entities/related_entities';
import {
hostsRiskScoreRequestOptionsSchema,
riskScoreRequestOptionsSchema,
riskScoreKpiRequestOptionsSchema,
usersRiskScoreRequestOptionsSchema,
} from './risk_score/risk_score';
import {
@ -77,8 +76,7 @@ export const searchStrategyRequestSchema = z.discriminatedUnion('factoryQueryTyp
observedUserDetailsSchema,
managedUserDetailsSchema,
userAuthenticationsSchema,
hostsRiskScoreRequestOptionsSchema,
usersRiskScoreRequestOptionsSchema,
riskScoreRequestOptionsSchema,
riskScoreKpiRequestOptionsSchema,
relatedHostsRequestOptionsSchema,
relatedUsersRequestOptionsSchema,

View file

@ -31,10 +31,9 @@ export enum NetworkQueries {
users = 'users',
}
export enum RiskQueries {
hostsRiskScore = 'hostsRiskScore',
usersRiskScore = 'usersRiskScore',
kpiRiskScore = 'kpiRiskScore',
export enum EntityRiskQueries {
list = 'listEntitiesRiskScore',
kpi = 'kpiRiskScore',
}
export enum CtiQueries {
@ -53,7 +52,7 @@ export type FactoryQueryTypes =
| HostsQueries
| UsersQueries
| NetworkQueries
| RiskQueries
| EntityRiskQueries
| CtiQueries
| typeof FirstLastSeenQuery
| RelatedEntitiesQueries;

View file

@ -6,23 +6,14 @@
*/
import { z } from '@kbn/zod';
import { RiskQueries } from '../model/factory_query_type';
import { RiskScoreFields } from '../../../search_strategy/security_solution/risk_score/all';
import { requestBasicOptionsSchema } from '../model/request_basic_options';
import { sort } from '../model/sort';
import { timerange } from '../model/timerange';
import { EntityRiskQueries } from '../model/factory_query_type';
import { riskScoreEntity } from './model/risk_score_entity';
export enum RiskScoreFields {
timestamp = '@timestamp',
hostName = 'host.name',
hostRiskScore = 'host.risk.calculated_score_norm',
hostRisk = 'host.risk.calculated_level',
userName = 'user.name',
userRiskScore = 'user.risk.calculated_score_norm',
userRisk = 'user.risk.calculated_level',
alertsCount = 'alertsCount',
}
const baseRiskScoreRequestOptionsSchema = requestBasicOptionsSchema.extend({
alertsTimerange: timerange.optional(),
riskScoreEntity,
@ -37,33 +28,15 @@ const baseRiskScoreRequestOptionsSchema = requestBasicOptionsSchema.extend({
sort: sort
.removeDefault()
.extend({
field: z.enum([
RiskScoreFields.timestamp,
RiskScoreFields.hostName,
RiskScoreFields.hostRiskScore,
RiskScoreFields.hostRisk,
RiskScoreFields.userName,
RiskScoreFields.userRiskScore,
RiskScoreFields.userRisk,
RiskScoreFields.alertsCount,
]),
field: z.nativeEnum(RiskScoreFields),
})
.optional(),
});
export const hostsRiskScoreRequestOptionsSchema = baseRiskScoreRequestOptionsSchema.extend({
factoryQueryType: z.literal(RiskQueries.hostsRiskScore),
export const riskScoreRequestOptionsSchema = baseRiskScoreRequestOptionsSchema.extend({
factoryQueryType: z.literal(EntityRiskQueries.list),
});
export const usersRiskScoreRequestOptionsSchema = baseRiskScoreRequestOptionsSchema.extend({
factoryQueryType: z.literal(RiskQueries.usersRiskScore),
});
export const riskScoreRequestOptionsSchema = z.union([
hostsRiskScoreRequestOptionsSchema,
usersRiskScoreRequestOptionsSchema,
]);
export type RiskScoreRequestOptionsInput = z.input<typeof riskScoreRequestOptionsSchema>;
export type RiskScoreRequestOptions = z.infer<typeof riskScoreRequestOptionsSchema>;

View file

@ -6,13 +6,14 @@
*/
import { z } from '@kbn/zod';
import { RiskQueries } from '../model/factory_query_type';
import { requestBasicOptionsSchema } from '../model/request_basic_options';
import { riskScoreEntity } from './model/risk_score_entity';
import { EntityRiskQueries } from '../model/factory_query_type';
export const riskScoreKpiRequestOptionsSchema = requestBasicOptionsSchema.extend({
entity: riskScoreEntity,
factoryQueryType: z.literal(RiskQueries.kpiRiskScore),
factoryQueryType: z.literal(EntityRiskQueries.kpi),
});
export type RiskScoreKpiRequestOptionsInput = z.input<typeof riskScoreKpiRequestOptionsSchema>;

View file

@ -6,10 +6,6 @@
*/
import { z } from '@kbn/zod';
import { EntityType } from '../../../../entity_analytics/types';
export enum RiskScoreEntity {
host = 'host',
user = 'user',
}
export const riskScoreEntity = z.enum([RiskScoreEntity.host, RiskScoreEntity.user]);
export const riskScoreEntity = z.nativeEnum(EntityType);

View file

@ -5,11 +5,13 @@
* 2.0.
*/
import { mockGlobalState } from '../../../public/common/mock';
import { parseAssetCriticalityCsvRow } from './parse_asset_criticality_csv_row';
const experimentalFeatures = mockGlobalState.app.enableExperimental;
describe('parseAssetCriticalityCsvRow', () => {
it('should return valid false if the row has no columns', () => {
const result = parseAssetCriticalityCsvRow([]);
const result = parseAssetCriticalityCsvRow([], experimentalFeatures);
expect(result.valid).toBe(false);
// @ts-ignore result can now only be InvalidRecord
@ -17,7 +19,7 @@ describe('parseAssetCriticalityCsvRow', () => {
});
it('should return valid false if the row has 2 columns', () => {
const result = parseAssetCriticalityCsvRow(['host', 'host-1']);
const result = parseAssetCriticalityCsvRow(['host', 'host-1'], experimentalFeatures);
expect(result.valid).toBe(false);
// @ts-ignore result can now only be InvalidRecord
@ -25,7 +27,10 @@ describe('parseAssetCriticalityCsvRow', () => {
});
it('should return valid false if the row has 4 columns', () => {
const result = parseAssetCriticalityCsvRow(['host', 'host-1', 'low_impact', 'extra']);
const result = parseAssetCriticalityCsvRow(
['host', 'host-1', 'low_impact', 'extra'],
experimentalFeatures
);
expect(result.valid).toBe(false);
// @ts-ignore result can now only be InvalidRecord
@ -33,7 +38,7 @@ describe('parseAssetCriticalityCsvRow', () => {
});
it('should return valid false if the entity type is missing', () => {
const result = parseAssetCriticalityCsvRow(['', 'host-1', 'low_impact']);
const result = parseAssetCriticalityCsvRow(['', 'host-1', 'low_impact'], experimentalFeatures);
expect(result.valid).toBe(false);
// @ts-ignore result can now only be InvalidRecord
@ -41,28 +46,34 @@ describe('parseAssetCriticalityCsvRow', () => {
});
it('should return valid false if the entity type is invalid', () => {
const result = parseAssetCriticalityCsvRow(['invalid', 'host-1', 'low_impact']);
const result = parseAssetCriticalityCsvRow(
['invalid', 'host-1', 'low_impact'],
experimentalFeatures
);
expect(result.valid).toBe(false);
// @ts-ignore result can now only be InvalidRecord
expect(result.error).toMatchInlineSnapshot(
`"Invalid entity type \\"invalid\\", expected to be one of: user, host, service, universal"`
`"Invalid entity type \\"invalid\\", expected to be one of: user, host"`
);
});
it('should return valid false if the entity type is invalid and only log 1000 characters', () => {
const invalidEntityType = 'x'.repeat(1001);
const result = parseAssetCriticalityCsvRow([invalidEntityType, 'host-1', 'low_impact']);
const result = parseAssetCriticalityCsvRow(
[invalidEntityType, 'host-1', 'low_impact'],
experimentalFeatures
);
expect(result.valid).toBe(false);
// @ts-ignore result can now only be InvalidRecord
expect(result.error).toMatchInlineSnapshot(
`"Invalid entity type \\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...\\", expected to be one of: user, host, service, universal"`
`"Invalid entity type \\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...\\", expected to be one of: user, host"`
);
});
it('should return valid false if the ID is missing', () => {
const result = parseAssetCriticalityCsvRow(['host', '', 'low_impact']);
const result = parseAssetCriticalityCsvRow(['host', '', 'low_impact'], experimentalFeatures);
expect(result.valid).toBe(false);
// @ts-ignore result can now only be InvalidRecord
@ -70,7 +81,7 @@ describe('parseAssetCriticalityCsvRow', () => {
});
it('should return valid false if the criticality level is missing', () => {
const result = parseAssetCriticalityCsvRow(['host', 'host-1', '']);
const result = parseAssetCriticalityCsvRow(['host', 'host-1', ''], experimentalFeatures);
expect(result.valid).toBe(false);
// @ts-ignore result can now only be InvalidRecord
@ -78,7 +89,7 @@ describe('parseAssetCriticalityCsvRow', () => {
});
it('should return valid false if the criticality level is invalid', () => {
const result = parseAssetCriticalityCsvRow(['host', 'host-1', 'invalid']);
const result = parseAssetCriticalityCsvRow(['host', 'host-1', 'invalid'], experimentalFeatures);
expect(result.valid).toBe(false);
// @ts-ignore result can now only be InvalidRecord
@ -89,7 +100,10 @@ describe('parseAssetCriticalityCsvRow', () => {
it('should return valid false if the criticality level is invalid and only log 1000 characters', () => {
const invalidCriticalityLevel = 'x'.repeat(1001);
const result = parseAssetCriticalityCsvRow(['host', 'host-1', invalidCriticalityLevel]);
const result = parseAssetCriticalityCsvRow(
['host', 'host-1', invalidCriticalityLevel],
experimentalFeatures
);
expect(result.valid).toBe(false);
// @ts-ignore result can now only be InvalidRecord
@ -100,7 +114,10 @@ describe('parseAssetCriticalityCsvRow', () => {
it('should return valid false if the ID is too long', () => {
const idValue = 'x'.repeat(1001);
const result = parseAssetCriticalityCsvRow(['host', idValue, 'low_impact']);
const result = parseAssetCriticalityCsvRow(
['host', idValue, 'low_impact'],
experimentalFeatures
);
expect(result.valid).toBe(false);
// @ts-ignore result can now only be InvalidRecord
@ -110,7 +127,9 @@ describe('parseAssetCriticalityCsvRow', () => {
});
it('should return the parsed row', () => {
expect(parseAssetCriticalityCsvRow(['host', 'host-1', 'low_impact'])).toEqual({
expect(
parseAssetCriticalityCsvRow(['host', 'host-1', 'low_impact'], experimentalFeatures)
).toEqual({
valid: true,
record: {
idField: 'host.name',
@ -121,7 +140,9 @@ describe('parseAssetCriticalityCsvRow', () => {
});
it('should return the parsed row if criticality level is the wrong case', () => {
expect(parseAssetCriticalityCsvRow(['host', 'host-1', 'LOW_IMPACT'])).toEqual({
expect(
parseAssetCriticalityCsvRow(['host', 'host-1', 'LOW_IMPACT'], experimentalFeatures)
).toEqual({
valid: true,
record: {
idField: 'host.name',

View file

@ -8,8 +8,9 @@ import { i18n } from '@kbn/i18n';
import type { CriticalityLevels } from './constants';
import { ValidCriticalityLevels } from './constants';
import { type AssetCriticalityUpsert, type CriticalityLevel } from './types';
import { IDENTITY_FIELD_MAP, getAvailableEntityTypes } from '../entity_store/constants';
import type { EntityType } from '../../api/entity_analytics';
import { EntityTypeToIdentifierField, type EntityType } from '../types';
import { getAssetCriticalityEntityTypes } from './utils';
import type { ExperimentalFeatures } from '../../experimental_features';
const MAX_COLUMN_CHARS = 1000;
@ -39,7 +40,10 @@ const trimColumn = (column: string): string => {
return column.length > MAX_COLUMN_CHARS ? `${column.substring(0, MAX_COLUMN_CHARS)}...` : column;
};
export const parseAssetCriticalityCsvRow = (row: string[]): ReturnType => {
export const parseAssetCriticalityCsvRow = (
row: string[],
experimentalFeatures: ExperimentalFeatures
): ReturnType => {
if (row.length !== 3) {
return validationErrorWithMessage(
i18n.translate('xpack.securitySolution.assetCriticality.csvUpload.expectedColumnsError', {
@ -100,19 +104,20 @@ export const parseAssetCriticalityCsvRow = (row: string[]): ReturnType => {
);
}
if (!getAvailableEntityTypes().includes(entityType as EntityType)) {
const enabledEntityTypes = getAssetCriticalityEntityTypes(experimentalFeatures);
if (!enabledEntityTypes.includes(entityType as EntityType)) {
return validationErrorWithMessage(
i18n.translate('xpack.securitySolution.assetCriticality.csvUpload.invalidEntityTypeError', {
defaultMessage: 'Invalid entity type "{entityType}", expected to be one of: {validTypes}',
values: {
entityType: trimColumn(entityType),
validTypes: getAvailableEntityTypes().join(', '),
validTypes: enabledEntityTypes.join(', '),
},
})
);
}
const idField = IDENTITY_FIELD_MAP[entityType as EntityType];
const idField = EntityTypeToIdentifierField[entityType as EntityType];
return {
valid: true,

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { ExperimentalFeatures } from '../../experimental_features';
import { getAllEntityTypes, getDisabledEntityTypes } from '../utils';
import { EntityType } from '../types';
const ASSET_CRITICALITY_UNAVAILABLE_TYPES = [EntityType.universal];
// TODO delete this function when the universal entity support is added
export const getAssetCriticalityEntityTypes = (experimentalFeatures: ExperimentalFeatures) => {
const allEntityTypes = getAllEntityTypes();
const disabledEntityTypes = getDisabledEntityTypes(experimentalFeatures);
return allEntityTypes.filter(
(value) =>
!disabledEntityTypes.includes(value) && !ASSET_CRITICALITY_UNAVAILABLE_TYPES.includes(value)
);
};

View file

@ -5,9 +5,6 @@
* 2.0.
*/
import type { EntityType, IdField } from '../../api/entity_analytics';
import { EntityTypeEnum } from '../../api/entity_analytics';
/**
* Entity Store routes
*/
@ -26,13 +23,3 @@ export const ENTITY_STORE_REQUIRED_ES_CLUSTER_PRIVILEGES = [
// The index pattern for the entity store has to support '.entities.v1.latest.noop' index
export const ENTITY_STORE_INDEX_PATTERN = '.entities.v1.latest.*';
export const IDENTITY_FIELD_MAP: Record<EntityType, IdField> = {
[EntityTypeEnum.host]: 'host.name',
[EntityTypeEnum.user]: 'user.name',
[EntityTypeEnum.service]: 'service.name',
[EntityTypeEnum.universal]: 'related.entity',
};
export const getAvailableEntityTypes = (): EntityType[] =>
Object.keys(EntityTypeEnum) as EntityType[];

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { ExperimentalFeatures } from '../../experimental_features';
import { EntityType } from '../types';
import { getAllEntityTypes, getDisabledEntityTypes } from '../utils';
const ENTITY_STORE_UNAVAILABLE_TYPES = [EntityType.universal];
// TODO delete this function when the universal entity support is added
export const getEnabledStoreEntityTypes = (experimentalFeatures: ExperimentalFeatures) => {
const allEntityTypes = getAllEntityTypes();
const disabledEntityTypes = getDisabledEntityTypes(experimentalFeatures);
return allEntityTypes.filter(
(value) =>
!disabledEntityTypes.includes(value) && !ENTITY_STORE_UNAVAILABLE_TYPES.includes(value)
);
};

View file

@ -5,8 +5,6 @@
* 2.0.
*/
import * as t from 'io-ts';
import type { EntityType } from '../types';
export const identifierTypeSchema = t.keyof({ user: null, host: null, service: null });
export type IdentifierTypeSchema = t.TypeOf<typeof identifierTypeSchema>;
export type IdentifierType = IdentifierTypeSchema;
export type IdentifierType = EntityType;

View file

@ -6,7 +6,6 @@
*/
export * from './risk_weights';
export * from './identifier_types';
export * from './range';
export * from './risk_levels';
export * from './types';

View file

@ -7,14 +7,6 @@
import type { EntityRiskScoreRecord, RiskScoreInput } from '../../api/entity_analytics/common';
export enum RiskScoreEntity {
host = 'host',
user = 'user',
// TODO Add service when FE is updated https://github.com/elastic/security-team/issues/11326
}
// TODO: Remove this when FE is updated https://github.com/elastic/security-team/issues/11326
export const SERVICE_RISK_SCORE_ENTITY = 'service';
export interface InitRiskEngineResult {
legacyRiskEngineDisabled: boolean;
riskEngineResourcesInstalled: boolean;

View file

@ -6,6 +6,10 @@
*/
import * as t from 'io-ts';
import { EntityType } from '../types';
import { getAllEntityTypes, getDisabledEntityTypes } from '../utils';
import type { ExperimentalFeatures } from '../../experimental_features';
/*
* This utility function can be used to turn a TypeScript enum into a io-ts codec.
*/
@ -23,3 +27,16 @@ export function fromEnum<EnumType extends string>(
t.identity
);
}
const RISK_ENGINE_UNAVAILABLE_TYPES = [EntityType.universal];
// TODO delete this function when the universal entity support is added
export const getRiskEngineEntityTypes = (experimentalFeatures: ExperimentalFeatures) => {
const allEntityTypes = getAllEntityTypes();
const disabledEntityTypes = getDisabledEntityTypes(experimentalFeatures);
return allEntityTypes.filter(
(value) =>
!disabledEntityTypes.includes(value) && !RISK_ENGINE_UNAVAILABLE_TYPES.includes(value)
);
};

View file

@ -0,0 +1,33 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
// Use exclusively for the legacy risk score module
export enum LegacyEntityType {
host = 'host',
user = 'user',
}
export enum EntityType {
user = 'user',
host = 'host',
service = 'service',
universal = 'universal',
}
export enum EntityIdentifierFields {
hostName = 'host.name',
userName = 'user.name',
serviceName = 'service.name',
universal = 'related.entity',
}
export const EntityTypeToIdentifierField: Record<EntityType, EntityIdentifierFields> = {
[EntityType.host]: EntityIdentifierFields.hostName,
[EntityType.user]: EntityIdentifierFields.userName,
[EntityType.service]: EntityIdentifierFields.serviceName,
[EntityType.universal]: EntityIdentifierFields.universal,
};

View file

@ -0,0 +1,64 @@
/*
* 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 { getAllEntityTypes, getDisabledEntityTypes } from './utils';
import { EntityType } from './types';
import type { ExperimentalFeatures } from '../experimental_features';
import { mockGlobalState } from '../../public/common/mock';
const mockedExperimentalFeatures = mockGlobalState.app.enableExperimental;
describe('utils', () => {
describe('getAllEntityTypes', () => {
it('should return all entity types', () => {
const entityTypes = getAllEntityTypes();
expect(entityTypes).toEqual(Object.values(EntityType));
});
});
describe('getDisabledEntityTypes', () => {
it('should return disabled entity types when serviceEntityStoreEnabled is false', () => {
const experimentalFeatures: ExperimentalFeatures = {
...mockedExperimentalFeatures,
serviceEntityStoreEnabled: false,
assetInventoryStoreEnabled: true,
};
const disabledEntityTypes = getDisabledEntityTypes(experimentalFeatures);
expect(disabledEntityTypes).toEqual([EntityType.service]);
});
it('should return disabled entity types when assetInventoryStoreEnabled is false', () => {
const experimentalFeatures: ExperimentalFeatures = {
...mockedExperimentalFeatures,
serviceEntityStoreEnabled: true,
assetInventoryStoreEnabled: false,
};
const disabledEntityTypes = getDisabledEntityTypes(experimentalFeatures);
expect(disabledEntityTypes).toEqual([EntityType.universal]);
});
it('should return both disabled entity types when both features are false', () => {
const experimentalFeatures: ExperimentalFeatures = {
...mockedExperimentalFeatures,
serviceEntityStoreEnabled: false,
assetInventoryStoreEnabled: false,
};
const disabledEntityTypes = getDisabledEntityTypes(experimentalFeatures);
expect(disabledEntityTypes).toEqual([EntityType.service, EntityType.universal]);
});
it('should return no disabled entity types when both features are true', () => {
const experimentalFeatures: ExperimentalFeatures = {
...mockedExperimentalFeatures,
serviceEntityStoreEnabled: true,
assetInventoryStoreEnabled: true,
};
const disabledEntityTypes = getDisabledEntityTypes(experimentalFeatures);
expect(disabledEntityTypes).toEqual([]);
});
});
});

View file

@ -0,0 +1,28 @@
/*
* 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 { ExperimentalFeatures } from '../experimental_features';
import { EntityType } from './types';
export const getAllEntityTypes = (): EntityType[] => Object.values(EntityType);
export const getDisabledEntityTypes = (
experimentalFeatures: ExperimentalFeatures
): EntityType[] => {
const disabledEntityTypes: EntityType[] = [];
const isServiceEntityStoreEnabled = experimentalFeatures.serviceEntityStoreEnabled;
const isUniversalEntityStoreEnabled = experimentalFeatures.assetInventoryStoreEnabled;
if (!isServiceEntityStoreEnabled) {
disabledEntityTypes.push(EntityType.service);
}
if (!isUniversalEntityStoreEnabled) {
disabledEntityTypes.push(EntityType.universal);
}
return disabledEntityTypes;
};

View file

@ -30,12 +30,6 @@ import type {
CtiDataSourceStrategyResponse,
} from './cti';
import type {
RiskQueries,
KpiRiskScoreStrategyResponse,
HostsRiskScoreStrategyResponse,
UsersRiskScoreStrategyResponse,
} from './risk_score';
import type { UsersQueries } from './users';
import type { ObservedUserDetailsStrategyResponse } from './users/observed_details';
@ -48,6 +42,7 @@ import type { UsersRelatedHostsStrategyResponse } from './related_entities/relat
import type { HostsRelatedUsersStrategyResponse } from './related_entities/related_users';
import type {
EntityRiskQueries,
EventEnrichmentRequestOptions,
EventEnrichmentRequestOptionsInput,
FirstLastSeenRequestOptions,
@ -97,6 +92,11 @@ import type {
UsersRequestOptions,
UsersRequestOptionsInput,
} from '../../api/search_strategy';
import type {
KpiRiskScoreStrategyResponse,
EntityType,
RiskScoreStrategyResponse,
} from './risk_score';
export * from './cti';
export * from './hosts';
@ -110,7 +110,7 @@ export type FactoryQueryTypes =
| HostsQueries
| UsersQueries
| NetworkQueries
| RiskQueries
| EntityRiskQueries
| CtiQueries
| typeof FirstLastSeenQuery
| RelatedEntitiesQueries;
@ -155,11 +155,9 @@ export type StrategyResponseType<T extends FactoryQueryTypes> = T extends HostsQ
? CtiEventEnrichmentStrategyResponse
: T extends CtiQueries.dataSource
? CtiDataSourceStrategyResponse
: T extends RiskQueries.hostsRiskScore
? HostsRiskScoreStrategyResponse
: T extends RiskQueries.usersRiskScore
? UsersRiskScoreStrategyResponse
: T extends RiskQueries.kpiRiskScore
: T extends EntityRiskQueries.list
? RiskScoreStrategyResponse<EntityType>
: T extends EntityRiskQueries.kpi
? KpiRiskScoreStrategyResponse
: T extends RelatedEntitiesQueries.relatedUsers
? HostsRelatedUsersStrategyResponse
@ -207,11 +205,9 @@ export type StrategyRequestInputType<T extends FactoryQueryTypes> = T extends Ho
? EventEnrichmentRequestOptionsInput
: T extends CtiQueries.dataSource
? ThreatIntelSourceRequestOptionsInput
: T extends RiskQueries.hostsRiskScore
: T extends EntityRiskQueries.list
? RiskScoreRequestOptionsInput
: T extends RiskQueries.usersRiskScore
? RiskScoreRequestOptionsInput
: T extends RiskQueries.kpiRiskScore
: T extends EntityRiskQueries.kpi
? RiskScoreKpiRequestOptionsInput
: T extends RelatedEntitiesQueries.relatedHosts
? RelatedHostsRequestOptionsInput
@ -259,11 +255,9 @@ export type StrategyRequestType<T extends FactoryQueryTypes> = T extends HostsQu
? EventEnrichmentRequestOptions
: T extends CtiQueries.dataSource
? ThreatIntelSourceRequestOptions
: T extends RiskQueries.hostsRiskScore
: T extends EntityRiskQueries.list
? RiskScoreRequestOptions
: T extends RiskQueries.usersRiskScore
? RiskScoreRequestOptions
: T extends RiskQueries.kpiRiskScore
: T extends EntityRiskQueries.kpi
? RiskScoreKpiRequestOptions
: T extends RelatedEntitiesQueries.relatedHosts
? RelatedHostsRequestOptions

View file

@ -7,20 +7,15 @@
import type { IEsSearchResponse } from '@kbn/search-types';
import { EntityIdentifierFields, EntityType } from '../../../../entity_analytics/types';
import { EntityRiskLevels, EntityRiskLevelsEnum } from '../../../../api/entity_analytics/common';
import type { EntityRiskScoreRecord } from '../../../../api/entity_analytics/common';
import type { Inspect, Maybe, SortField } from '../../../common';
export interface HostsRiskScoreStrategyResponse extends IEsSearchResponse {
export interface RiskScoreStrategyResponse<T extends EntityType> extends IEsSearchResponse {
inspect?: Maybe<Inspect>;
totalCount: number;
data: HostRiskScore[] | undefined;
}
export interface UsersRiskScoreStrategyResponse extends IEsSearchResponse {
inspect?: Maybe<Inspect>;
totalCount: number;
data: UserRiskScore[] | undefined;
data: Array<EntityRiskScore<T>> | undefined;
}
export interface RiskStats extends EntityRiskScoreRecord {
@ -31,25 +26,15 @@ export interface RiskStats extends EntityRiskScoreRecord {
export const RiskSeverity = EntityRiskLevels.enum;
export type RiskSeverity = EntityRiskLevels;
export interface HostRiskScore {
export type EntityRiskScore<T extends EntityType> = {
'@timestamp': string;
host: {
name: string;
risk: RiskStats;
};
alertsCount?: number;
oldestAlertTimestamp?: string;
}
} & Record<T, { name: string; risk: RiskStats }>;
export interface UserRiskScore {
'@timestamp': string;
user: {
name: string;
risk: RiskStats;
};
alertsCount?: number;
oldestAlertTimestamp?: string;
}
export type HostRiskScore = EntityRiskScore<EntityType.host>;
export type UserRiskScore = EntityRiskScore<EntityType.user>;
export type ServiceRiskScore = EntityRiskScore<EntityType.service>;
export interface RuleRisk {
rule_name: string;
@ -61,34 +46,38 @@ export type RiskScoreSortField = SortField<RiskScoreFields>;
export enum RiskScoreFields {
timestamp = '@timestamp',
hostName = 'host.name',
hostName = EntityIdentifierFields.hostName,
hostRiskScore = 'host.risk.calculated_score_norm',
hostRisk = 'host.risk.calculated_level',
userName = 'user.name',
userName = EntityIdentifierFields.userName,
userRiskScore = 'user.risk.calculated_score_norm',
userRisk = 'user.risk.calculated_level',
serviceName = EntityIdentifierFields.serviceName,
serviceRiskScore = 'service.risk.calculated_score_norm',
serviceRisk = 'service.risk.calculated_level',
alertsCount = 'alertsCount',
unsupported = 'unsupported', // Temporary value used while we don't support the universal entity
}
export interface RiskScoreItem {
_id?: Maybe<string>;
[RiskScoreFields.hostName]: Maybe<string>;
[RiskScoreFields.userName]: Maybe<string>;
[RiskScoreFields.serviceName]: Maybe<string>;
[RiskScoreFields.timestamp]: Maybe<string>;
[RiskScoreFields.hostRisk]: Maybe<EntityRiskLevels>;
[RiskScoreFields.userRisk]: Maybe<EntityRiskLevels>;
[RiskScoreFields.serviceRisk]: Maybe<EntityRiskLevels>;
[RiskScoreFields.hostRiskScore]: Maybe<number>;
[RiskScoreFields.userRiskScore]: Maybe<number>;
[RiskScoreFields.serviceRiskScore]: Maybe<number>;
[RiskScoreFields.alertsCount]: Maybe<number>;
}
export const isUserRiskScore = (risk: HostRiskScore | UserRiskScore): risk is UserRiskScore =>
'user' in risk;
export const EMPTY_SEVERITY_COUNT = {
[EntityRiskLevelsEnum.Critical]: 0,
[EntityRiskLevelsEnum.High]: 0,
@ -96,3 +85,17 @@ export const EMPTY_SEVERITY_COUNT = {
[EntityRiskLevelsEnum.Moderate]: 0,
[EntityRiskLevelsEnum.Unknown]: 0,
};
export const EntityTypeToLevelField: Record<EntityType, RiskScoreFields> = {
[EntityType.host]: RiskScoreFields.hostRisk,
[EntityType.user]: RiskScoreFields.userRisk,
[EntityType.service]: RiskScoreFields.serviceRisk,
[EntityType.universal]: RiskScoreFields.unsupported, // We don't calculate risk for the universal entity
};
export const EntityTypeToScoreField: Record<EntityType, RiskScoreFields> = {
[EntityType.host]: RiskScoreFields.hostRiskScore,
[EntityType.user]: RiskScoreFields.userRiskScore,
[EntityType.service]: RiskScoreFields.serviceRiskScore,
[EntityType.universal]: RiskScoreFields.unsupported, // We don't calculate risk for the universal entity
};

View file

@ -5,14 +5,13 @@
* 2.0.
*/
import { EntityTypeToIdentifierField, EntityType } from '../../../../entity_analytics/types';
import type { ESQuery } from '../../../../typed_json';
import { RISKY_HOSTS_INDEX_PREFIX, RISKY_USERS_INDEX_PREFIX } from '../../../../constants';
import {
RiskScoreEntity,
getRiskScoreLatestIndex,
getRiskScoreTimeSeriesIndex,
} from '../../../../entity_analytics/risk_engine';
export { RiskQueries } from '../../../../api/search_strategy';
/**
* Make sure this aligns with the index in step 6, 9 in
@ -24,7 +23,7 @@ export const getHostRiskIndex = (
isNewRiskScoreModuleInstalled: boolean
): string => {
if (isNewRiskScoreModuleInstalled) {
return onlyLatest ? getRiskScoreLatestIndex(spaceId) : getRiskScoreTimeSeriesIndex(spaceId);
return getRiskIndex(spaceId, onlyLatest);
} else {
return `${RISKY_HOSTS_INDEX_PREFIX}${onlyLatest ? 'latest_' : ''}${spaceId}`;
}
@ -36,27 +35,29 @@ export const getUserRiskIndex = (
isNewRiskScoreModuleInstalled: boolean
): string => {
if (isNewRiskScoreModuleInstalled) {
return onlyLatest ? getRiskScoreLatestIndex(spaceId) : getRiskScoreTimeSeriesIndex(spaceId);
return getRiskIndex(spaceId, onlyLatest);
} else {
return `${RISKY_USERS_INDEX_PREFIX}${onlyLatest ? 'latest_' : ''}${spaceId}`;
}
};
/**
* This implementation doesn't support the deprecated risk score module.
*/
export const getRiskIndex = (spaceId: string, onlyLatest: boolean = true): string => {
return onlyLatest ? getRiskScoreLatestIndex(spaceId) : getRiskScoreTimeSeriesIndex(spaceId);
};
export const buildHostNamesFilter = (hostNames: string[]) => {
return { terms: { 'host.name': hostNames } };
return buildEntityNameFilter(EntityType.host, hostNames);
};
export const buildUserNamesFilter = (userNames: string[]) => {
return { terms: { 'user.name': userNames } };
return buildEntityNameFilter(EntityType.user, userNames);
};
export const buildEntityNameFilter = (
entityNames: string[],
riskEntity: RiskScoreEntity
): ESQuery => {
return riskEntity === RiskScoreEntity.host
? { terms: { 'host.name': entityNames } }
: { terms: { 'user.name': entityNames } };
export const buildEntityNameFilter = (riskEntity: EntityType, entityNames: string[]): ESQuery => {
return { terms: { [EntityTypeToIdentifierField[riskEntity]]: entityNames } };
};
export { RiskScoreEntity };
export { EntityType };

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { RiskScoreEntity } from '../search_strategy';
import { EntityType } from '../search_strategy';
import {
getCreateLatestTransformOptions,
getCreateMLHostPivotTransformOptions,
@ -37,7 +37,7 @@ import {
const mockSpaceId = 'customSpaceId';
describe.each([[RiskScoreEntity.host], [RiskScoreEntity.user]])('Risk Score Modules', (entity) => {
describe.each([[EntityType.host], [EntityType.user]])('Risk Score Modules', (entity) => {
test(`getRiskScorePivotTransformId - ${entity}`, () => {
const id = getRiskScorePivotTransformId(entity, mockSpaceId);
expect(id).toMatchInlineSnapshot(`"ml_${entity}riskscore_pivot_transform_customSpaceId"`);
@ -119,7 +119,7 @@ describe.each([[RiskScoreEntity.host], [RiskScoreEntity.user]])('Risk Score Modu
entity.charAt(0).toUpperCase() + entity.slice(1)
}PivotTransformOptions`, () => {
const fn =
entity === RiskScoreEntity.host
entity === EntityType.host
? getCreateMLHostPivotTransformOptions
: getCreateMLUserPivotTransformOptions;
const options = fn({
@ -129,7 +129,7 @@ describe.each([[RiskScoreEntity.host], [RiskScoreEntity.user]])('Risk Score Modu
});
test(`getRisk${entity.charAt(0).toUpperCase() + entity.slice(1)}CreateLevelScriptOptions`, () => {
const fn =
entity === RiskScoreEntity.host
entity === EntityType.host
? getRiskHostCreateLevelScriptOptions
: getRiskUserCreateLevelScriptOptions;
const options = fn();
@ -137,7 +137,7 @@ describe.each([[RiskScoreEntity.host], [RiskScoreEntity.user]])('Risk Score Modu
});
test(`getRisk${entity.charAt(0).toUpperCase() + entity.slice(1)}CreateMapScriptOptions`, () => {
const fn =
entity === RiskScoreEntity.host
entity === EntityType.host
? getRiskHostCreateMapScriptOptions
: getRiskUserCreateMapScriptOptions;
const options = fn();
@ -147,7 +147,7 @@ describe.each([[RiskScoreEntity.host], [RiskScoreEntity.user]])('Risk Score Modu
entity.charAt(0).toUpperCase() + entity.slice(1)
}CreateReduceScriptOptions`, () => {
const fn =
entity === RiskScoreEntity.host
entity === EntityType.host
? getRiskHostCreateReduceScriptOptions
: getRiskUserCreateReduceScriptOptions;
const options = fn();
@ -157,7 +157,7 @@ describe.each([[RiskScoreEntity.host], [RiskScoreEntity.user]])('Risk Score Modu
/**
* User risk score doesn't have init script, so we only check for host
*/
if (entity === RiskScoreEntity.host) {
if (entity === EntityType.host) {
test(`getRiskHostCreateInitScriptOptions`, () => {
const options = getRiskHostCreateInitScriptOptions();
expect(options).toMatchSnapshot();

View file

@ -6,7 +6,7 @@
*/
import { DEFAULT_ALERTS_INDEX } from '../constants';
import { RiskScoreEntity, RiskScoreFields } from '../search_strategy';
import { EntityType, RiskScoreFields } from '../search_strategy';
import type { Pipeline, Processor } from '../types/risk_scores';
/**
@ -14,34 +14,30 @@ import type { Pipeline, Processor } from '../types/risk_scores';
* and ingest pipelines (and dashboard saved objects) are created with spaceId
* so they won't affect each other across different spaces.
*/
export const getRiskScorePivotTransformId = (
riskScoreEntity: RiskScoreEntity,
spaceId = 'default'
) => `ml_${riskScoreEntity}riskscore_pivot_transform_${spaceId}`;
export const getRiskScorePivotTransformId = (riskScoreEntity: EntityType, spaceId = 'default') =>
`ml_${riskScoreEntity}riskscore_pivot_transform_${spaceId}`;
export const getRiskScoreLatestTransformId = (
riskScoreEntity: RiskScoreEntity,
spaceId = 'default'
) => `ml_${riskScoreEntity}riskscore_latest_transform_${spaceId}`;
export const getRiskScoreLatestTransformId = (riskScoreEntity: EntityType, spaceId = 'default') =>
`ml_${riskScoreEntity}riskscore_latest_transform_${spaceId}`;
export const getIngestPipelineName = (riskScoreEntity: RiskScoreEntity, spaceId = 'default') =>
export const getIngestPipelineName = (riskScoreEntity: EntityType, spaceId = 'default') =>
`ml_${riskScoreEntity}riskscore_ingest_pipeline_${spaceId}`;
export const getPivotTransformIndex = (riskScoreEntity: RiskScoreEntity, spaceId = 'default') =>
export const getPivotTransformIndex = (riskScoreEntity: EntityType, spaceId = 'default') =>
`ml_${riskScoreEntity}_risk_score_${spaceId}`;
export const getLatestTransformIndex = (riskScoreEntity: RiskScoreEntity, spaceId = 'default') =>
export const getLatestTransformIndex = (riskScoreEntity: EntityType, spaceId = 'default') =>
`ml_${riskScoreEntity}_risk_score_latest_${spaceId}`;
export const getAlertsIndex = (spaceId = 'default') => `${DEFAULT_ALERTS_INDEX}-${spaceId}`;
export const getRiskScoreLevelScriptId = (riskScoreEntity: RiskScoreEntity, spaceId = 'default') =>
export const getRiskScoreLevelScriptId = (riskScoreEntity: EntityType, spaceId = 'default') =>
`ml_${riskScoreEntity}riskscore_levels_script_${spaceId}`;
export const getRiskScoreInitScriptId = (riskScoreEntity: RiskScoreEntity, spaceId = 'default') =>
export const getRiskScoreInitScriptId = (riskScoreEntity: EntityType, spaceId = 'default') =>
`ml_${riskScoreEntity}riskscore_init_script_${spaceId}`;
export const getRiskScoreMapScriptId = (riskScoreEntity: RiskScoreEntity, spaceId = 'default') =>
export const getRiskScoreMapScriptId = (riskScoreEntity: EntityType, spaceId = 'default') =>
`ml_${riskScoreEntity}riskscore_map_script_${spaceId}`;
export const getRiskScoreReduceScriptId = (riskScoreEntity: RiskScoreEntity, spaceId = 'default') =>
export const getRiskScoreReduceScriptId = (riskScoreEntity: EntityType, spaceId = 'default') =>
`ml_${riskScoreEntity}riskscore_reduce_script_${spaceId}`;
/**
@ -51,15 +47,15 @@ export const getRiskScoreReduceScriptId = (riskScoreEntity: RiskScoreEntity, spa
* are Deprecated.
* But We still need to keep track of the old ids, so we can delete them during upgrade.
*/
export const getLegacyIngestPipelineName = (riskScoreEntity: RiskScoreEntity) =>
export const getLegacyIngestPipelineName = (riskScoreEntity: EntityType) =>
`ml_${riskScoreEntity}riskscore_ingest_pipeline`;
export const getLegacyRiskScoreLevelScriptId = (riskScoreEntity: RiskScoreEntity) =>
export const getLegacyRiskScoreLevelScriptId = (riskScoreEntity: EntityType) =>
`ml_${riskScoreEntity}riskscore_levels_script`;
export const getLegacyRiskScoreInitScriptId = (riskScoreEntity: RiskScoreEntity) =>
export const getLegacyRiskScoreInitScriptId = (riskScoreEntity: EntityType) =>
`ml_${riskScoreEntity}riskscore_init_script`;
export const getLegacyRiskScoreMapScriptId = (riskScoreEntity: RiskScoreEntity) =>
export const getLegacyRiskScoreMapScriptId = (riskScoreEntity: EntityType) =>
`ml_${riskScoreEntity}riskscore_map_script`;
export const getLegacyRiskScoreReduceScriptId = (riskScoreEntity: RiskScoreEntity) =>
export const getLegacyRiskScoreReduceScriptId = (riskScoreEntity: EntityType) =>
`ml_${riskScoreEntity}riskscore_reduce_script`;
/**
@ -73,7 +69,7 @@ export const getRiskHostCreateLevelScriptOptions = (
const source =
"double risk_score = (def)ctx.getByPath(params.risk_score);\nif (risk_score < 20) {\n ctx['host']['risk']['calculated_level'] = 'Unknown'\n}\nelse if (risk_score >= 20 && risk_score < 40) {\n ctx['host']['risk']['calculated_level'] = 'Low'\n}\nelse if (risk_score >= 40 && risk_score < 70) {\n ctx['host']['risk']['calculated_level'] = 'Moderate'\n}\nelse if (risk_score >= 70 && risk_score < 90) {\n ctx['host']['risk']['calculated_level'] = 'High'\n}\nelse if (risk_score >= 90) {\n ctx['host']['risk']['calculated_level'] = 'Critical'\n}";
return {
id: getRiskScoreLevelScriptId(RiskScoreEntity.host, spaceId),
id: getRiskScoreLevelScriptId(EntityType.host, spaceId),
script: {
lang: 'painless',
source: stringifyScript ? JSON.stringify(source) : source,
@ -92,7 +88,7 @@ export const getRiskHostCreateInitScriptOptions = (
const source =
'state.rule_risk_stats = new HashMap();\nstate.host_variant_set = false;\nstate.host_variant = new String();\nstate.tactic_ids = new HashSet();';
return {
id: getRiskScoreInitScriptId(RiskScoreEntity.host, spaceId),
id: getRiskScoreInitScriptId(EntityType.host, spaceId),
script: {
lang: 'painless',
source: stringifyScript ? JSON.stringify(source) : source,
@ -111,7 +107,7 @@ export const getRiskHostCreateMapScriptOptions = (
const source =
'// Get the host variant\nif (state.host_variant_set == false) {\n if (doc.containsKey("host.os.full") && doc["host.os.full"].size() != 0) {\n state.host_variant = doc["host.os.full"].value;\n state.host_variant_set = true;\n }\n}\n// Aggregate all the tactics seen on the host\nif (doc.containsKey("signal.rule.threat.tactic.id") && doc["signal.rule.threat.tactic.id"].size() != 0) {\n state.tactic_ids.add(doc["signal.rule.threat.tactic.id"].value);\n}\n// Get running sum of time-decayed risk score per rule name per shard\nString rule_name = doc["signal.rule.name"].value;\ndef stats = state.rule_risk_stats.getOrDefault(rule_name, [0.0,"",false]);\nint time_diff = (int)((System.currentTimeMillis() - doc["@timestamp"].value.toInstant().toEpochMilli()) / (1000.0 * 60.0 * 60.0));\ndouble risk_derate = Math.min(1, Math.exp((params.lookback_time - time_diff) / params.time_decay_constant));\nstats[0] = Math.max(stats[0], doc["signal.rule.risk_score"].value * risk_derate);\nif (stats[2] == false) {\n stats[1] = doc["kibana.alert.rule.uuid"].value;\n stats[2] = true;\n}\nstate.rule_risk_stats.put(rule_name, stats);';
return {
id: getRiskScoreMapScriptId(RiskScoreEntity.host, spaceId),
id: getRiskScoreMapScriptId(EntityType.host, spaceId),
script: {
lang: 'painless',
source: stringifyScript ? JSON.stringify(source) : source,
@ -130,7 +126,7 @@ export const getRiskHostCreateReduceScriptOptions = (
const source =
'// Consolidating time decayed risks and tactics from across all shards\nMap total_risk_stats = new HashMap();\nString host_variant = new String();\ndef tactic_ids = new HashSet();\nfor (state in states) {\n for (key in state.rule_risk_stats.keySet()) {\n def rule_stats = state.rule_risk_stats.get(key);\n def stats = total_risk_stats.getOrDefault(key, [0.0,"",false]);\n stats[0] = Math.max(stats[0], rule_stats[0]);\n if (stats[2] == false) {\n stats[1] = rule_stats[1];\n stats[2] = true;\n } \n total_risk_stats.put(key, stats);\n }\n if (host_variant.length() == 0) {\n host_variant = state.host_variant;\n }\n tactic_ids.addAll(state.tactic_ids);\n}\n// Consolidating individual rule risks and arranging them in decreasing order\nList risks = new ArrayList();\nfor (key in total_risk_stats.keySet()) {\n risks.add(total_risk_stats[key][0])\n}\nCollections.sort(risks, Collections.reverseOrder());\n// Calculating total host risk score\ndouble total_risk = 0.0;\ndouble risk_cap = params.max_risk * params.zeta_constant;\nfor (int i=0;i<risks.length;i++) {\n total_risk += risks[i] / Math.pow((1+i), params.p);\n}\n// Normalizing the host risk score\ndouble total_norm_risk = 100 * total_risk / risk_cap;\nif (total_norm_risk < 40) {\n total_norm_risk = 2.125 * total_norm_risk;\n}\nelse if (total_norm_risk >= 40 && total_norm_risk < 50) {\n total_norm_risk = 85 + (total_norm_risk - 40);\n}\nelse {\n total_norm_risk = 95 + (total_norm_risk - 50) / 10;\n}\n// Calculating multipliers to the host risk score\ndouble risk_multiplier = 1.0;\nList multipliers = new ArrayList();\n// Add a multiplier if host is a server\nif (host_variant.toLowerCase().contains("server")) {\n risk_multiplier *= params.server_multiplier;\n multipliers.add("Host is a server");\n}\n// Add multipliers based on number and diversity of tactics seen on the host\nfor (String tactic : tactic_ids) {\n multipliers.add("Tactic "+tactic);\n risk_multiplier *= 1 + params.tactic_base_multiplier * params.tactic_weights.getOrDefault(tactic, 0);\n}\n// Calculating final risk\ndouble final_risk = total_norm_risk;\nif (risk_multiplier > 1.0) {\n double prior_odds = (total_norm_risk) / (100 - total_norm_risk);\n double updated_odds = prior_odds * risk_multiplier; \n final_risk = 100 * updated_odds / (1 + updated_odds);\n}\n// Adding additional metadata\nList rule_stats = new ArrayList();\nfor (key in total_risk_stats.keySet()) {\n Map temp = new HashMap();\n temp["rule_name"] = key;\n temp["rule_risk"] = total_risk_stats[key][0];\n temp["rule_id"] = total_risk_stats[key][1];\n rule_stats.add(temp);\n}\n\nreturn ["calculated_score_norm": final_risk, "rule_risks": rule_stats, "multipliers": multipliers];';
return {
id: getRiskScoreReduceScriptId(RiskScoreEntity.host, spaceId),
id: getRiskScoreReduceScriptId(EntityType.host, spaceId),
script: {
lang: 'painless',
source: stringifyScript ? JSON.stringify(source) : source,
@ -149,7 +145,7 @@ export const getRiskUserCreateLevelScriptOptions = (
const source =
"double risk_score = (def)ctx.getByPath(params.risk_score);\nif (risk_score < 20) {\n ctx['user']['risk']['calculated_level'] = 'Unknown'\n}\nelse if (risk_score >= 20 && risk_score < 40) {\n ctx['user']['risk']['calculated_level'] = 'Low'\n}\nelse if (risk_score >= 40 && risk_score < 70) {\n ctx['user']['risk']['calculated_level'] = 'Moderate'\n}\nelse if (risk_score >= 70 && risk_score < 90) {\n ctx['user']['risk']['calculated_level'] = 'High'\n}\nelse if (risk_score >= 90) {\n ctx['user']['risk']['calculated_level'] = 'Critical'\n}";
return {
id: getRiskScoreLevelScriptId(RiskScoreEntity.user, spaceId),
id: getRiskScoreLevelScriptId(EntityType.user, spaceId),
script: {
lang: 'painless',
source: stringifyScript ? JSON.stringify(source) : source,
@ -168,7 +164,7 @@ export const getRiskUserCreateMapScriptOptions = (
const source =
'// Get running sum of risk score per rule name per shard\\\\\nString rule_name = doc["signal.rule.name"].value;\ndef stats = state.rule_risk_stats.getOrDefault(rule_name, 0.0);\nstats = doc["signal.rule.risk_score"].value;\nstate.rule_risk_stats.put(rule_name, stats);';
return {
id: getRiskScoreMapScriptId(RiskScoreEntity.user, spaceId),
id: getRiskScoreMapScriptId(EntityType.user, spaceId),
script: {
lang: 'painless',
source: stringifyScript ? JSON.stringify(source) : source,
@ -187,7 +183,7 @@ export const getRiskUserCreateReduceScriptOptions = (
const source =
'// Consolidating time decayed risks from across all shards\nMap total_risk_stats = new HashMap();\nfor (state in states) {\n for (key in state.rule_risk_stats.keySet()) {\n def rule_stats = state.rule_risk_stats.get(key);\n def stats = total_risk_stats.getOrDefault(key, 0.0);\n stats = rule_stats;\n total_risk_stats.put(key, stats);\n }\n}\n// Consolidating individual rule risks and arranging them in decreasing order\nList risks = new ArrayList();\nfor (key in total_risk_stats.keySet()) {\n risks.add(total_risk_stats[key])\n}\nCollections.sort(risks, Collections.reverseOrder());\n// Calculating total risk and normalizing it to a range\ndouble total_risk = 0.0;\ndouble risk_cap = params.max_risk * params.zeta_constant;\nfor (int i=0;i<risks.length;i++) {\n total_risk += risks[i] / Math.pow((1+i), params.p);\n}\ndouble total_norm_risk = 100 * total_risk / risk_cap;\nif (total_norm_risk < 40) {\n total_norm_risk = 2.125 * total_norm_risk;\n}\nelse if (total_norm_risk >= 40 && total_norm_risk < 50) {\n total_norm_risk = 85 + (total_norm_risk - 40);\n}\nelse {\n total_norm_risk = 95 + (total_norm_risk - 50) / 10;\n}\n\nList rule_stats = new ArrayList();\nfor (key in total_risk_stats.keySet()) {\n Map temp = new HashMap();\n temp["rule_name"] = key;\n temp["rule_risk"] = total_risk_stats[key];\n rule_stats.add(temp);\n}\n\nreturn ["calculated_score_norm": total_norm_risk, "rule_risks": rule_stats];';
return {
id: getRiskScoreReduceScriptId(RiskScoreEntity.user, spaceId),
id: getRiskScoreReduceScriptId(EntityType.user, spaceId),
script: {
lang: 'painless',
source: stringifyScript ? JSON.stringify(source) : source,
@ -201,7 +197,7 @@ export const getRiskUserCreateReduceScriptOptions = (
* console_templates/enable_host_risk_score.console step 5
*/
export const getRiskScoreIngestPipelineOptions = (
riskScoreEntity: RiskScoreEntity,
riskScoreEntity: EntityType,
spaceId = 'default',
stringifyScript?: boolean
): Pipeline => {
@ -245,7 +241,7 @@ export const getCreateRiskScoreIndicesOptions = ({
stringifyScript,
}: {
spaceId?: string;
riskScoreEntity: RiskScoreEntity;
riskScoreEntity: EntityType;
stringifyScript?: boolean;
}) => {
const mappings = {
@ -313,7 +309,7 @@ export const getCreateRiskScoreLatestIndicesOptions = ({
stringifyScript,
}: {
spaceId?: string;
riskScoreEntity: RiskScoreEntity;
riskScoreEntity: EntityType;
stringifyScript?: boolean;
}) => {
const mappings = {
@ -383,8 +379,8 @@ export const getCreateMLHostPivotTransformOptions = ({
}) => {
const options = {
dest: {
index: getPivotTransformIndex(RiskScoreEntity.host, spaceId),
pipeline: getIngestPipelineName(RiskScoreEntity.host, spaceId),
index: getPivotTransformIndex(EntityType.host, spaceId),
pipeline: getIngestPipelineName(EntityType.host, spaceId),
},
frequency: '1h',
pivot: {
@ -398,10 +394,10 @@ export const getCreateMLHostPivotTransformOptions = ({
scripted_metric: {
combine_script: 'return state',
init_script: {
id: getRiskScoreInitScriptId(RiskScoreEntity.host, spaceId),
id: getRiskScoreInitScriptId(EntityType.host, spaceId),
},
map_script: {
id: getRiskScoreMapScriptId(RiskScoreEntity.host, spaceId),
id: getRiskScoreMapScriptId(EntityType.host, spaceId),
},
params: {
lookback_time: 72,
@ -429,7 +425,7 @@ export const getCreateMLHostPivotTransformOptions = ({
zeta_constant: 2.612,
},
reduce_script: {
id: getRiskScoreReduceScriptId(RiskScoreEntity.host, spaceId),
id: getRiskScoreReduceScriptId(EntityType.host, spaceId),
},
},
},
@ -482,8 +478,8 @@ export const getCreateMLUserPivotTransformOptions = ({
}) => {
const options = {
dest: {
index: getPivotTransformIndex(RiskScoreEntity.user, spaceId),
pipeline: getIngestPipelineName(RiskScoreEntity.user, spaceId),
index: getPivotTransformIndex(EntityType.user, spaceId),
pipeline: getIngestPipelineName(EntityType.user, spaceId),
},
frequency: '1h',
pivot: {
@ -498,7 +494,7 @@ export const getCreateMLUserPivotTransformOptions = ({
combine_script: 'return state',
init_script: 'state.rule_risk_stats = new HashMap();',
map_script: {
id: getRiskScoreMapScriptId(RiskScoreEntity.user, spaceId),
id: getRiskScoreMapScriptId(EntityType.user, spaceId),
},
params: {
max_risk: 100,
@ -506,7 +502,7 @@ export const getCreateMLUserPivotTransformOptions = ({
zeta_constant: 2.612,
},
reduce_script: {
id: getRiskScoreReduceScriptId(RiskScoreEntity.user, spaceId),
id: getRiskScoreReduceScriptId(EntityType.user, spaceId),
},
},
},
@ -561,7 +557,7 @@ export const getCreateLatestTransformOptions = ({
stringifyScript,
}: {
spaceId?: string;
riskScoreEntity: RiskScoreEntity;
riskScoreEntity: EntityType;
stringifyScript?: boolean;
}) => {
const options = {

View file

@ -570,7 +570,7 @@ paths:
schema:
type: string
- in: query
name: entities_types
name: entity_types
required: true
schema:
items:
@ -950,6 +950,7 @@ components:
oneOf:
- $ref: '#/components/schemas/UserEntity'
- $ref: '#/components/schemas/HostEntity'
- $ref: '#/components/schemas/ServiceEntity'
EntityRiskLevels:
enum:
- Unknown
@ -1186,6 +1187,42 @@ components:
- index
- description
- category
ServiceEntity:
type: object
properties:
'@timestamp':
format: date-time
type: string
asset:
type: object
properties:
criticality:
$ref: '#/components/schemas/AssetCriticalityLevel'
required:
- criticality
entity:
type: object
properties:
name:
type: string
source:
type: string
required:
- name
- source
service:
type: object
properties:
name:
type: string
risk:
$ref: '#/components/schemas/EntityRiskScoreRecord'
required:
- name
required:
- '@timestamp'
- service
- entity
StoreStatus:
enum:
- not_installed

View file

@ -570,7 +570,7 @@ paths:
schema:
type: string
- in: query
name: entities_types
name: entity_types
required: true
schema:
items:
@ -950,6 +950,7 @@ components:
oneOf:
- $ref: '#/components/schemas/UserEntity'
- $ref: '#/components/schemas/HostEntity'
- $ref: '#/components/schemas/ServiceEntity'
EntityRiskLevels:
enum:
- Unknown
@ -1186,6 +1187,42 @@ components:
- index
- description
- category
ServiceEntity:
type: object
properties:
'@timestamp':
format: date-time
type: string
asset:
type: object
properties:
criticality:
$ref: '#/components/schemas/AssetCriticalityLevel'
required:
- criticality
entity:
type: object
properties:
name:
type: string
source:
type: string
required:
- name
- source
service:
type: object
properties:
name:
type: string
risk:
$ref: '#/components/schemas/EntityRiskScoreRecord'
required:
- name
required:
- '@timestamp'
- service
- entity
StoreStatus:
enum:
- not_installed

View file

@ -7,20 +7,7 @@
import type { FlyoutPanelProps } from '@kbn/expandable-flyout';
import { TableId } from '@kbn/securitysolution-data-table';
const UserPanelKey: UserPanelExpandableFlyoutProps['key'] = 'user-panel';
interface UserPanelProps extends Record<string, unknown> {
contextID: string;
scopeId: string;
userName: string;
isDraggable?: boolean;
}
interface UserPanelExpandableFlyoutProps extends FlyoutPanelProps {
key: 'user-panel';
params: UserPanelProps;
}
import { UserPanelKey } from '../../../../../flyout/entity_details/shared/constants';
export const isUserName = (fieldName: string) => fieldName === 'user.name';

View file

@ -7,7 +7,6 @@
import { useMemo } from 'react';
import {
RiskScoreEntity,
type HostRiskScore,
type UserRiskScore,
buildHostNamesFilter,
@ -15,6 +14,7 @@ import {
} from '../../../common/search_strategy';
import { useRiskScore } from '../../entity_analytics/api/hooks/use_risk_score';
import { FIRST_RECORD_PAGINATION } from '../../entity_analytics/common';
import { EntityType } from '../../../common/entity_analytics/types';
export const useHasRiskScore = ({
field,
@ -29,7 +29,7 @@ export const useHasRiskScore = ({
[isHostNameField, value]
);
const { data } = useRiskScore({
riskEntity: isHostNameField ? RiskScoreEntity.host : RiskScoreEntity.user,
riskEntity: isHostNameField ? EntityType.host : EntityType.user,
filterQuery: buildFilterQuery,
onlyLatest: false,
pagination: FIRST_RECORD_PAGINATION,

View file

@ -11,6 +11,7 @@ import type { SyntheticEvent, MouseEvent } from 'react';
import React, { useMemo, useCallback } from 'react';
import { isArray, isNil } from 'lodash/fp';
import type { NavigateToAppOptions } from '@kbn/core-application-browser';
import { EntityType } from '../../../../common/entity_analytics/types';
import { IP_REPUTATION_LINKS_SETTING, APP_UI_ID } from '../../../../common/constants';
import { encodeIpv6 } from '../../lib/helpers';
import {
@ -95,7 +96,7 @@ const UserDetailsLinkComponent: React.FC<{
const onClick = useCallback(
(e: SyntheticEvent) => {
telemetry.reportEvent(EntityEventTypes.EntityDetailsClicked, { entity: 'user' });
telemetry.reportEvent(EntityEventTypes.EntityDetailsClicked, { entity: EntityType.user });
const callback = onClickParam ?? goToUsersDetails;
callback(e);
},
@ -172,7 +173,7 @@ const HostDetailsLinkComponent: React.FC<HostDetailsLinkProps> = ({
const onClick = useCallback(
(e: SyntheticEvent) => {
telemetry.reportEvent(EntityEventTypes.EntityDetailsClicked, { entity: 'host' });
telemetry.reportEvent(EntityEventTypes.EntityDetailsClicked, { entity: EntityType.host });
const callback = onClickParam ?? goToHostDetails;
callback(e);
@ -200,6 +201,32 @@ const HostDetailsLinkComponent: React.FC<HostDetailsLinkProps> = ({
export const HostDetailsLink = React.memo(HostDetailsLinkComponent);
export interface EntityDetailsLinkProps {
children?: React.ReactNode;
/** `Component` is only used with `EuiDataGrid`; the grid keeps a reference to `Component` for show / hide functionality */
Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon;
entityName: string;
isButton?: boolean;
onClick?: (e: SyntheticEvent) => void;
tab?: HostsTableType | UsersTableType;
title?: string;
entityType: EntityType;
}
export const EntityDetailsLink = ({
entityType,
tab,
entityName,
...props
}: EntityDetailsLinkProps) => {
if (entityType === EntityType.host) {
return <HostDetailsLink {...props} hostTab={tab as HostsTableType} hostName={entityName} />;
} else if (entityType === EntityType.user) {
return <UserDetailsLink {...props} userTab={tab as UsersTableType} userName={entityName} />;
}
return entityName;
};
const allowedUrlSchemes = ['http://', 'https://'];
export const ExternalLink = React.memo<{
url: string;

View file

@ -6,7 +6,7 @@
*/
import type { RootSchema } from '@kbn/core/public';
import type { RiskSeverity } from '../../../../../../common/search_strategy';
import type { EntityType, RiskSeverity } from '../../../../../../common/search_strategy';
export enum EntityEventTypes {
EntityDetailsClicked = 'Entity Details Clicked',
@ -33,7 +33,7 @@ export enum ML_JOB_TELEMETRY_STATUS {
installationError = 'installationError',
}
interface EntityParam {
entity: 'host' | 'user';
entity: EntityType;
}
type ReportEntityDetailsClickedParams = EntityParam;

View file

@ -28,7 +28,7 @@ import type {
AssetCriticalityRecord,
EntityAnalyticsPrivileges,
} from '../../../common/api/entity_analytics';
import type { RiskScoreEntity } from '../../../common/search_strategy';
import type { EntityType } from '../../../common/search_strategy';
import {
RISK_ENGINE_STATUS_URL,
RISK_SCORE_PREVIEW_URL,
@ -90,7 +90,7 @@ export const useEntityAnalyticsRoutes = () => {
version: API_VERSIONS.public.v1,
method: 'GET',
query: {
entities_types: params.entitiesTypes,
entity_types: params.entityTypes,
sort_field: params.sortField,
sort_order: params.sortOrder,
page: params.page,
@ -264,7 +264,7 @@ export const useEntityAnalyticsRoutes = () => {
}: {
query: {
indexName: string;
entity: RiskScoreEntity;
entity: EntityType;
};
signal?: AbortSignal;
}): Promise<{

View file

@ -6,7 +6,7 @@
*/
import { TestProviders } from '../../../common/mock';
import { RiskScoreEntity } from '../../../../common/search_strategy';
import { EntityType } from '../../../../common/entity_analytics/types';
import { useCalculateEntityRiskScore } from './use_calculate_entity_risk_score';
import { waitFor, renderHook, act } from '@testing-library/react';
import { RiskEngineStatusEnum } from '../../../../common/api/entity_analytics/risk_engine/engine_status_route.gen';
@ -37,7 +37,7 @@ jest.mock('../../../common/hooks/use_app_toasts', () => ({
}),
}));
const identifierType = RiskScoreEntity.user;
const identifierType = EntityType.user;
const identifier = 'test-user';
const options = {
onSuccess: jest.fn(),

View file

@ -9,14 +9,14 @@ import { useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { useMutation } from '@tanstack/react-query';
import type { EntityType } from '../../../../common/entity_analytics/types';
import { RiskEngineStatusEnum } from '../../../../common/api/entity_analytics/risk_engine/engine_status_route.gen';
import { useEntityAnalyticsRoutes } from '../api';
import { useAppToasts } from '../../../common/hooks/use_app_toasts';
import { useRiskEngineStatus } from './use_risk_engine_status';
import { RiskScoreEntity } from '../../../../common/entity_analytics/risk_engine';
export const useCalculateEntityRiskScore = (
identifierType: RiskScoreEntity,
identifierType: EntityType,
identifier: string,
{ onSuccess }: { onSuccess: () => void }
) => {
@ -29,7 +29,7 @@ export const useCalculateEntityRiskScore = (
addError(error, {
title: i18n.translate('xpack.securitySolution.entityDetails.userPanel.error', {
defaultMessage: 'There was a problem calculating the {entity} risk score',
values: { entity: identifierType === RiskScoreEntity.host ? "host's" : "user's" },
values: { entity: `${identifierType}'s` },
}),
});
},

View file

@ -14,7 +14,9 @@ import { useAppToasts } from '../../../common/hooks/use_app_toasts';
import { useAppToastsMock } from '../../../common/hooks/use_app_toasts.mock';
import { useRiskScoreFeatureStatus } from './use_risk_score_feature_status';
import { useIsNewRiskScoreModuleInstalled } from './use_risk_engine_status';
import { RiskScoreEntity } from '../../../../common/search_strategy';
import { EntityType } from '../../../../common/search_strategy';
import { EntityRiskQueries } from '../../../../common/api/search_strategy';
jest.mock('../../../common/containers/use_search_strategy', () => ({
useSearchStrategy: jest.fn(),
}));
@ -67,169 +69,166 @@ const defaultSearchResponse = {
inspect: {},
error: undefined,
};
describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
'useRiskScore entityType: %s',
(riskEntity) => {
beforeEach(() => {
jest.clearAllMocks();
appToastsMock = useAppToastsMock.create();
(useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
mockUseRiskScoreFeatureStatus.mockReturnValue(defaultFeatureStatus);
mockUseSearchStrategy.mockReturnValue(defaultSearchResponse);
mockUseIsNewRiskScoreModuleInstalled.mockReturnValue(defaultRiskScoreModuleStatus);
describe.each([EntityType.host, EntityType.user])('useRiskScore entityType: %s', (riskEntity) => {
beforeEach(() => {
jest.clearAllMocks();
appToastsMock = useAppToastsMock.create();
(useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
mockUseRiskScoreFeatureStatus.mockReturnValue(defaultFeatureStatus);
mockUseSearchStrategy.mockReturnValue(defaultSearchResponse);
mockUseIsNewRiskScoreModuleInstalled.mockReturnValue(defaultRiskScoreModuleStatus);
});
test('does not search if license is not valid', () => {
mockUseRiskScoreFeatureStatus.mockReturnValue({
...defaultFeatureStatus,
isAuthorized: false,
});
const { result } = renderHook(() => useRiskScore({ riskEntity }), {
wrapper: TestProviders,
});
expect(mockSearch).not.toHaveBeenCalled();
expect(result.current).toEqual({
loading: false,
...defaultRisk,
isAuthorized: false,
refetch: result.current.refetch,
});
});
test('does not search if feature is not enabled', () => {
mockUseRiskScoreFeatureStatus.mockReturnValue({
...defaultFeatureStatus,
isEnabled: false,
});
test('does not search if license is not valid', () => {
mockUseRiskScoreFeatureStatus.mockReturnValue({
...defaultFeatureStatus,
isAuthorized: false,
});
const { result } = renderHook(() => useRiskScore({ riskEntity }), {
wrapper: TestProviders,
});
expect(mockSearch).not.toHaveBeenCalled();
expect(result.current).toEqual({
loading: false,
...defaultRisk,
isAuthorized: false,
refetch: result.current.refetch,
});
const { result } = renderHook(() => useRiskScore({ riskEntity }), {
wrapper: TestProviders,
});
test('does not search if feature is not enabled', () => {
mockUseRiskScoreFeatureStatus.mockReturnValue({
...defaultFeatureStatus,
isEnabled: false,
});
const { result } = renderHook(() => useRiskScore({ riskEntity }), {
wrapper: TestProviders,
});
expect(mockSearch).not.toHaveBeenCalled();
expect(result.current).toEqual({
loading: false,
...defaultRisk,
isModuleEnabled: false,
refetch: result.current.refetch,
});
expect(mockSearch).not.toHaveBeenCalled();
expect(result.current).toEqual({
loading: false,
...defaultRisk,
isModuleEnabled: false,
refetch: result.current.refetch,
});
});
test('does not search if index is deprecated ', () => {
mockUseRiskScoreFeatureStatus.mockReturnValue({
...defaultFeatureStatus,
isDeprecated: true,
});
const { result } = renderHook(() => useRiskScore({ riskEntity, skip: true }), {
wrapper: TestProviders,
});
expect(mockSearch).not.toHaveBeenCalled();
expect(result.current).toEqual({
loading: false,
...defaultRisk,
isDeprecated: true,
refetch: result.current.refetch,
});
test('does not search if index is deprecated ', () => {
mockUseRiskScoreFeatureStatus.mockReturnValue({
...defaultFeatureStatus,
isDeprecated: true,
});
const { result } = renderHook(() => useRiskScore({ riskEntity, skip: true }), {
wrapper: TestProviders,
});
expect(mockSearch).not.toHaveBeenCalled();
expect(result.current).toEqual({
loading: false,
...defaultRisk,
isDeprecated: true,
refetch: result.current.refetch,
});
});
test('handle index not found error', () => {
mockUseRiskScoreFeatureStatus.mockReturnValue({
...defaultFeatureStatus,
isDeprecated: false,
isEnabled: false,
});
mockUseSearchStrategy.mockReturnValue({
...defaultSearchResponse,
error: {
attributes: {
caused_by: {
type: 'index_not_found_exception',
},
test('handle index not found error', () => {
mockUseRiskScoreFeatureStatus.mockReturnValue({
...defaultFeatureStatus,
isDeprecated: false,
isEnabled: false,
});
mockUseSearchStrategy.mockReturnValue({
...defaultSearchResponse,
error: {
attributes: {
caused_by: {
type: 'index_not_found_exception',
},
},
});
const { result } = renderHook(() => useRiskScore({ riskEntity }), {
wrapper: TestProviders,
});
},
});
const { result } = renderHook(() => useRiskScore({ riskEntity }), {
wrapper: TestProviders,
});
expect(result.current).toEqual({
loading: false,
...defaultRisk,
isModuleEnabled: false,
refetch: result.current.refetch,
error: {
attributes: {
caused_by: {
type: 'index_not_found_exception',
},
},
},
});
});
test('show error toast', () => {
const error = new Error();
mockUseSearchStrategy.mockReturnValue({
...defaultSearchResponse,
error,
});
renderHook(() => useRiskScore({ riskEntity }), {
wrapper: TestProviders,
});
expect(appToastsMock.addError).toHaveBeenCalledWith(error, {
title: 'Failed to run search on risk score',
});
});
test('runs search if feature is enabled and not deprecated', () => {
renderHook(() => useRiskScore({ riskEntity }), {
wrapper: TestProviders,
});
expect(mockSearch).toHaveBeenCalledTimes(1);
expect(mockSearch).toHaveBeenCalledWith({
defaultIndex: [`ml_${riskEntity}_risk_score_latest_default`],
factoryQueryType: EntityRiskQueries.list,
riskScoreEntity: riskEntity,
includeAlertsCount: false,
});
});
test('runs search with new index if feature is enabled and not deprecated and new module installed', () => {
mockUseIsNewRiskScoreModuleInstalled.mockReturnValue({
...defaultRiskScoreModuleStatus,
installed: true,
});
renderHook(() => useRiskScore({ riskEntity }), {
wrapper: TestProviders,
});
expect(mockSearch).toHaveBeenCalledTimes(1);
expect(mockSearch).toHaveBeenCalledWith({
defaultIndex: ['risk-score.risk-score-latest-default'],
factoryQueryType: EntityRiskQueries.list,
riskScoreEntity: riskEntity,
includeAlertsCount: false,
});
});
test('return result', async () => {
mockUseSearchStrategy.mockReturnValue({
...defaultSearchResponse,
result: {
data: [],
totalCount: 0,
},
});
const { result } = renderHook(() => useRiskScore({ riskEntity }), {
wrapper: TestProviders,
});
await waitFor(() => {
expect(result.current).toEqual({
loading: false,
...defaultRisk,
isModuleEnabled: false,
data: [],
refetch: result.current.refetch,
error: {
attributes: {
caused_by: {
type: 'index_not_found_exception',
},
},
},
});
});
test('show error toast', () => {
const error = new Error();
mockUseSearchStrategy.mockReturnValue({
...defaultSearchResponse,
error,
});
renderHook(() => useRiskScore({ riskEntity }), {
wrapper: TestProviders,
});
expect(appToastsMock.addError).toHaveBeenCalledWith(error, {
title: 'Failed to run search on risk score',
});
});
test('runs search if feature is enabled and not deprecated', () => {
renderHook(() => useRiskScore({ riskEntity }), {
wrapper: TestProviders,
});
expect(mockSearch).toHaveBeenCalledTimes(1);
expect(mockSearch).toHaveBeenCalledWith({
defaultIndex: [`ml_${riskEntity}_risk_score_latest_default`],
factoryQueryType: `${riskEntity}sRiskScore`,
riskScoreEntity: riskEntity,
includeAlertsCount: false,
});
});
test('runs search with new index if feature is enabled and not deprecated and new module installed', () => {
mockUseIsNewRiskScoreModuleInstalled.mockReturnValue({
...defaultRiskScoreModuleStatus,
installed: true,
});
renderHook(() => useRiskScore({ riskEntity }), {
wrapper: TestProviders,
});
expect(mockSearch).toHaveBeenCalledTimes(1);
expect(mockSearch).toHaveBeenCalledWith({
defaultIndex: ['risk-score.risk-score-latest-default'],
factoryQueryType: `${riskEntity}sRiskScore`,
riskScoreEntity: riskEntity,
includeAlertsCount: false,
});
});
test('return result', async () => {
mockUseSearchStrategy.mockReturnValue({
...defaultSearchResponse,
result: {
data: [],
totalCount: 0,
},
});
const { result } = renderHook(() => useRiskScore({ riskEntity }), {
wrapper: TestProviders,
});
await waitFor(() => {
expect(result.current).toEqual({
loading: false,
...defaultRisk,
data: [],
refetch: result.current.refetch,
});
});
});
}
);
});
});

View file

@ -8,17 +8,16 @@
import { useCallback, useEffect, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EntityRiskQueries } from '../../../../common/api/search_strategy';
import { useRiskScoreFeatureStatus } from './use_risk_score_feature_status';
import { createFilter } from '../../../common/containers/helpers';
import type { RiskScoreSortField, StrategyResponseType } from '../../../../common/search_strategy';
import {
RiskQueries,
getUserRiskIndex,
RiskScoreEntity,
getHostRiskIndex,
import type {
RiskScoreSortField,
RiskScoreStrategyResponse,
StrategyResponseType,
} from '../../../../common/search_strategy';
import { getHostRiskIndex, getUserRiskIndex, EntityType } from '../../../../common/search_strategy';
import type { ESQuery } from '../../../../common/typed_json';
import type { InspectResponse } from '../../../types';
import { useAppToasts } from '../../../common/hooks/use_app_toasts';
import { isIndexNotFoundError } from '../../../common/utils/exceptions';
@ -27,12 +26,8 @@ import { useSpaceId } from '../../../common/hooks/use_space_id';
import { useSearchStrategy } from '../../../common/containers/use_search_strategy';
import { useIsNewRiskScoreModuleInstalled } from './use_risk_engine_status';
export interface RiskScoreState<T extends RiskScoreEntity.host | RiskScoreEntity.user> {
data:
| undefined
| StrategyResponseType<
T extends RiskScoreEntity.host ? RiskQueries.hostsRiskScore : RiskQueries.usersRiskScore
>['data'];
export interface RiskScoreState<T extends EntityType> {
data: RiskScoreStrategyResponse<T>['data'];
inspect: InspectResponse;
isInspected: boolean;
refetch: inputsModel.Refetch;
@ -63,15 +58,12 @@ interface UseRiskScore<T> extends UseRiskScoreParams {
riskEntity: T;
}
export const initialResult: Omit<
StrategyResponseType<RiskQueries.hostsRiskScore | RiskQueries.usersRiskScore>,
'rawResponse'
> = {
export const initialResult: Omit<StrategyResponseType<EntityRiskQueries.list>, 'rawResponse'> = {
totalCount: 0,
data: undefined,
};
export const useRiskScore = <T extends RiskScoreEntity.host | RiskScoreEntity.user>({
export const useRiskScore = <T extends EntityType>({
timerange,
onlyLatest = true,
filterQuery,
@ -86,13 +78,11 @@ export const useRiskScore = <T extends RiskScoreEntity.host | RiskScoreEntity.us
useIsNewRiskScoreModuleInstalled();
const defaultIndex =
spaceId && !riskScoreStatusLoading && isNewRiskScoreModuleInstalled !== undefined
? riskEntity === RiskScoreEntity.host
? riskEntity === EntityType.host
? getHostRiskIndex(spaceId, onlyLatest, isNewRiskScoreModuleInstalled)
: getUserRiskIndex(spaceId, onlyLatest, isNewRiskScoreModuleInstalled)
: undefined;
const factoryQueryType =
riskEntity === RiskScoreEntity.host ? RiskQueries.hostsRiskScore : RiskQueries.usersRiskScore;
const factoryQueryType = EntityRiskQueries.list;
const { querySize, cursorStart } = pagination || {};
const { addError } = useAppToasts();
@ -112,7 +102,7 @@ export const useRiskScore = <T extends RiskScoreEntity.host | RiskScoreEntity.us
refetch,
inspect,
error,
} = useSearchStrategy<RiskQueries.hostsRiskScore | RiskQueries.usersRiskScore>({
} = useSearchStrategy<EntityRiskQueries.list>({
factoryQueryType,
initialResult,
abort: skip,

View file

@ -9,7 +9,7 @@ import { renderHook, act } from '@testing-library/react';
import { TestProviders } from '../../../common/mock';
import { useRiskScoreFeatureStatus } from './use_risk_score_feature_status';
import { RiskScoreEntity } from '../../../../common/search_strategy';
import { EntityType } from '../../../../common/search_strategy';
import { useFetch } from '../../../common/hooks/use_fetch';
import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities';
import { useHasSecurityCapability } from '../../../helper_hooks';
@ -49,7 +49,7 @@ describe(`risk score feature status`, () => {
test('does not search if license is not valid, and initial isDeprecated state is false', () => {
mockUseMlCapabilities.mockReturnValue({ isPlatinumOrTrialLicense: false });
const { result } = renderHook(
() => useRiskScoreFeatureStatus(RiskScoreEntity.host, 'the_right_one'),
() => useRiskScoreFeatureStatus(EntityType.host, 'the_right_one'),
{
wrapper: TestProviders,
}
@ -67,7 +67,7 @@ describe(`risk score feature status`, () => {
test("does not search if the user doesn't has entity analytics capability", () => {
mockUseHasSecurityCapability.mockReturnValue(false);
const { result } = renderHook(
() => useRiskScoreFeatureStatus(RiskScoreEntity.host, 'the_right_one'),
() => useRiskScoreFeatureStatus(EntityType.host, 'the_right_one'),
{
wrapper: TestProviders,
}
@ -84,13 +84,13 @@ describe(`risk score feature status`, () => {
test('runs search if feature is enabled, and initial isDeprecated state is true', () => {
const { result } = renderHook(
() => useRiskScoreFeatureStatus(RiskScoreEntity.host, 'the_right_one'),
() => useRiskScoreFeatureStatus(EntityType.host, 'the_right_one'),
{
wrapper: TestProviders,
}
);
expect(mockFetch).toHaveBeenCalledWith({
query: { entity: RiskScoreEntity.host, indexName: 'the_right_one' },
query: { entity: EntityType.host, indexName: 'the_right_one' },
});
expect(result.current).toEqual({
...defaultResult,
@ -100,7 +100,7 @@ describe(`risk score feature status`, () => {
test('updates state after search returns isDeprecated = false', () => {
const { result, rerender } = renderHook(
() => useRiskScoreFeatureStatus(RiskScoreEntity.host, 'the_right_one'),
() => useRiskScoreFeatureStatus(EntityType.host, 'the_right_one'),
{
wrapper: TestProviders,
}

View file

@ -8,7 +8,7 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities';
import { REQUEST_NAMES, useFetch } from '../../../common/hooks/use_fetch';
import type { RiskScoreEntity } from '../../../../common/search_strategy';
import type { EntityType } from '../../../../common/search_strategy';
import { useHasSecurityCapability } from '../../../helper_hooks';
import { useEntityAnalyticsRoutes } from '../api';
@ -25,7 +25,7 @@ interface RiskScoresFeatureStatus {
}
export const useRiskScoreFeatureStatus = (
riskEntity: RiskScoreEntity.host | RiskScoreEntity.user,
riskEntity: EntityType,
defaultIndex?: string
): RiskScoresFeatureStatus => {
const { isPlatinumOrTrialLicense, capabilitiesFetched } = useMlCapabilities();

View file

@ -8,14 +8,15 @@
import { useCallback, useEffect, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EntityRiskQueries } from '../../../../common/api/search_strategy';
import {
EntityType,
getHostRiskIndex,
getUserRiskIndex,
RiskQueries,
RiskSeverity,
RiskScoreEntity,
EMPTY_SEVERITY_COUNT,
} from '../../../../common/search_strategy';
import { isIndexNotFoundError } from '../../../common/utils/exceptions';
import type { ESQuery } from '../../../../common/typed_json';
import type { SeverityCount } from '../../components/severity/types';
@ -40,7 +41,7 @@ interface RiskScoreKpi {
interface UseRiskScoreKpiProps {
filterQuery?: string | ESQuery;
skip?: boolean;
riskEntity: RiskScoreEntity;
riskEntity: EntityType;
timerange?: { to: string; from: string };
}
@ -54,9 +55,10 @@ export const useRiskScoreKpi = ({
const spaceId = useSpaceId();
const { installed: isNewRiskScoreModuleInstalled, isLoading: riskScoreStatusLoading } =
useIsNewRiskScoreModuleInstalled();
const defaultIndex =
spaceId && !riskScoreStatusLoading && isNewRiskScoreModuleInstalled !== undefined
? riskEntity === RiskScoreEntity.host
? riskEntity === EntityType.host
? getHostRiskIndex(spaceId, true, isNewRiskScoreModuleInstalled)
: getUserRiskIndex(spaceId, true, isNewRiskScoreModuleInstalled)
: undefined;
@ -70,8 +72,8 @@ export const useRiskScoreKpi = ({
} = useRiskScoreFeatureStatus(riskEntity, defaultIndex);
const { loading, result, search, refetch, inspect, error } =
useSearchStrategy<RiskQueries.kpiRiskScore>({
factoryQueryType: RiskQueries.kpiRiskScore,
useSearchStrategy<EntityRiskQueries.kpi>({
factoryQueryType: EntityRiskQueries.kpi,
initialResult: {
kpiRiskScore: EMPTY_SEVERITY_COUNT,
},

View file

@ -17,9 +17,11 @@ import {
} from '@elastic/eui';
import { css } from '@emotion/css';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { FormattedMessage, useI18n } from '@kbn/i18n-react';
import { euiThemeVars } from '@kbn/ui-theme';
import { useAssetCriticalityEntityTypes } from '../../../hooks/use_enabled_entity_types';
import { EntityTypeToIdentifierField } from '../../../../../common/entity_analytics/types';
import {
CRITICALITY_CSV_MAX_SIZE_BYTES,
ValidCriticalityLevels,
@ -44,8 +46,18 @@ const listStyle = css`
export const AssetCriticalityFilePickerStep: React.FC<AssetCriticalityFilePickerStepProps> =
React.memo(({ onFileChange, errorMessage, isLoading }) => {
const i18n = useI18n();
const formatBytes = useFormatBytes();
const { euiTheme } = useEuiTheme();
const entityTypes = useAssetCriticalityEntityTypes();
const i18nOrList = (items: string[]) =>
i18n
.formatListToParts(items, {
type: 'disjunction',
})
.map(({ type, value }) => (type === 'element' ? <b>{value}</b> : value)); // bolded list items
return (
<>
<EuiSpacer size="m" />
@ -94,22 +106,22 @@ export const AssetCriticalityFilePickerStep: React.FC<AssetCriticalityFilePicker
<ul className={listStyle}>
<li>
<FormattedMessage
defaultMessage="Entity type: Indicate whether the entity is a {host} or a {user}."
defaultMessage="Entity type: Indicate whether the entity is a {entityTypes}"
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.assetTypeDescription"
values={{
host: <b>{'host'}</b>,
user: <b>{'user'}</b>,
entityTypes: i18nOrList(entityTypes),
}}
/>
</li>
<li>
{
<FormattedMessage
defaultMessage="Identifier: Specify the entity's {hostName} or {userName}."
defaultMessage="Identifier: Specify the entity's {fieldsName}"
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.assetIdentifierDescription"
values={{
hostName: <b>{'host.name'}</b>,
userName: <b>{'user.name'}</b>,
fieldsName: i18nOrList(
entityTypes.map((type) => EntityTypeToIdentifierField[type])
),
}}
/>
}

View file

@ -10,10 +10,16 @@ import { TestProviders } from '@kbn/timelines-plugin/public/mock';
import { waitFor, renderHook } from '@testing-library/react';
import { useFileValidation } from './hooks';
import { useKibana as mockUseKibana } from '../../../common/lib/kibana/__mocks__';
import { mockGlobalState } from '../../../common/mock';
const mockedExperimentalFeatures = mockGlobalState.app.enableExperimental;
const mockedUseKibana = mockUseKibana();
const mockedTelemetry = createTelemetryServiceMock();
jest.mock('../../../common/hooks/use_experimental_features', () => ({
useEnableExperimental: () => ({ ...mockedExperimentalFeatures, serviceEntityStoreEnabled: true }),
}));
jest.mock('../../../common/lib/kibana', () => {
const original = jest.requireActual('../../../common/lib/kibana');

View file

@ -11,6 +11,7 @@ import { unparse, parse } from 'papaparse';
import { useCallback, useMemo } from 'react';
import type { EuiStepHorizontalProps } from '@elastic/eui/src/components/steps/step_horizontal';
import { noop } from 'lodash/fp';
import { useEnableExperimental } from '../../../common/hooks/use_experimental_features';
import { useFormatBytes } from '../../../common/components/formatted_bytes';
import { validateParsedContent, validateFile } from './validations';
import { useKibana } from '../../../common/lib/kibana';
@ -27,6 +28,7 @@ interface UseFileChangeCbParams {
export const useFileValidation = ({ onError, onComplete }: UseFileChangeCbParams) => {
const formatBytes = useFormatBytes();
const { telemetry } = useKibana().services;
const experimentalFeatures = useEnableExperimental();
const onErrorWrapper = useCallback(
(
@ -87,7 +89,10 @@ export const useFileValidation = ({ onError, onComplete }: UseFileChangeCbParams
return;
}
const { invalid, valid, errors } = validateParsedContent(parsedFile.data);
const { invalid, valid, errors } = validateParsedContent(
parsedFile.data,
experimentalFeatures
);
const validLinesAsText = unparse(valid);
const invalidLinesAsText = unparse(invalid);
const processingEndTime = Date.now();
@ -118,7 +123,7 @@ export const useFileValidation = ({ onError, onComplete }: UseFileChangeCbParams
parse(file, parserConfig);
},
[formatBytes, telemetry, onErrorWrapper, onComplete]
[formatBytes, telemetry, onErrorWrapper, experimentalFeatures, onComplete]
);
};

View file

@ -5,13 +5,16 @@
* 2.0.
*/
import { mockGlobalState } from '../../../common/mock';
import { validateParsedContent, validateFile } from './validations';
const formatBytes = (bytes: number) => bytes.toString();
const experimentalFeatures = mockGlobalState.app.enableExperimental;
describe('validateParsedContent', () => {
it('should return empty arrays when data is empty', () => {
const result = validateParsedContent([]);
const result = validateParsedContent([], experimentalFeatures);
expect(result).toEqual({
valid: [],
@ -27,7 +30,7 @@ describe('validateParsedContent', () => {
['host', 'host-1', 'low_impact'], // valid
];
const result = validateParsedContent(data);
const result = validateParsedContent(data, experimentalFeatures);
expect(result).toEqual({
valid: [data[2]],

View file

@ -6,6 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
import type { ExperimentalFeatures } from '../../../../common';
import {
CRITICALITY_CSV_MAX_SIZE_BYTES,
CRITICALITY_CSV_MAX_SIZE_BYTES_WITH_TOLERANCE,
@ -19,7 +20,8 @@ export interface RowValidationErrors {
}
export const validateParsedContent = (
data: string[][]
data: string[][],
experimentalFeatures: ExperimentalFeatures
): { valid: string[][]; invalid: string[][]; errors: RowValidationErrors[] } => {
if (data.length === 0) {
return { valid: [], invalid: [], errors: [] };
@ -32,7 +34,7 @@ export const validateParsedContent = (
errors: RowValidationErrors[];
}>(
(acc, row) => {
const parsedRow = parseAssetCriticalityCsvRow(row);
const parsedRow = parseAssetCriticalityCsvRow(row, experimentalFeatures);
if (parsedRow.valid) {
acc.valid.push(row);
} else {

View file

@ -6,7 +6,7 @@
*/
import { EuiEmptyPrompt, EuiPanel, EuiToolTip } from '@elastic/eui';
import React from 'react';
import type { RiskScoreEntity } from '../../../../common/search_strategy';
import type { EntityType } from '../../../../common/search_strategy';
import { useCheckSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_check_signal_index';
import type { inputsModel } from '../../../common/store';
import { RiskScoreHeaderTitle } from '../risk_score_onboarding/risk_score_header_title';
@ -24,7 +24,7 @@ const EnableRiskScoreComponent = ({
}: {
isDeprecated: boolean;
isDisabled: boolean;
entityType: RiskScoreEntity;
entityType: EntityType;
refetch: inputsModel.Refetch;
timerange: {
from: string;

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import type { RiskScoreEntity } from '../../../../common/search_strategy';
import type { EntityType } from '../../../../common/entity_analytics/types';
import { getRiskEntityTranslation } from '../risk_score/translations';
export const ENABLE_RISK_SCORE_POPOVER = i18n.translate(
@ -15,7 +15,24 @@ export const ENABLE_RISK_SCORE_POPOVER = i18n.translate(
}
);
export const UPGRADE_RISK_SCORE = (riskEntity: RiskScoreEntity) =>
export const ENABLE_RISK_SCORE = (riskEntity: EntityType) =>
i18n.translate('xpack.securitySolution.enableRiskScore.enableRiskScore', {
defaultMessage: 'Enable {riskEntity} Risk Score',
values: {
riskEntity: getRiskEntityTranslation(riskEntity),
},
});
export const ENABLE_RISK_SCORE_DESCRIPTION = (riskEntity: EntityType) =>
i18n.translate('xpack.securitySolution.enableRiskScore.enableRiskScoreDescription', {
defaultMessage:
'Once you have enabled this feature you can get quick access to the {riskEntity} risk scores in this section. The data might need an hour to be generated after enabling the module.',
values: {
riskEntity: getRiskEntityTranslation(riskEntity, true),
},
});
export const UPGRADE_RISK_SCORE = (riskEntity: EntityType) =>
i18n.translate('xpack.securitySolution.enableRiskScore.upgradeRiskScore', {
defaultMessage: 'Upgrade {riskEntity} Risk Score',
values: {
@ -30,20 +47,3 @@ export const UPGRADE_RISK_SCORE_DESCRIPTION = i18n.translate(
'Current data is no longer supported. Please migrate your data and upgrade the module. The data might need an hour to be generated after enabling the module.',
}
);
export const ENABLE_RISK_SCORE = (riskEntity: RiskScoreEntity) =>
i18n.translate('xpack.securitySolution.enableRiskScore.enableRiskScore', {
defaultMessage: 'Enable {riskEntity} Risk Score',
values: {
riskEntity: getRiskEntityTranslation(riskEntity),
},
});
export const ENABLE_RISK_SCORE_DESCRIPTION = (riskEntity: RiskScoreEntity) =>
i18n.translate('xpack.securitySolution.enableRiskScore.enableRiskScoreDescription', {
defaultMessage:
'Once you have enabled this feature you can get quick access to the {riskEntity} risk scores in this section. The data might need an hour to be generated after enabling the module.',
values: {
riskEntity: getRiskEntityTranslation(riskEntity, true),
},
});

View file

@ -8,12 +8,14 @@ import React, { useMemo, useCallback } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle, EuiLink } from '@elastic/eui';
import styled from 'styled-components';
import { useDispatch } from 'react-redux';
import { sumBy } from 'lodash/fp';
import { capitalize, sumBy } from 'lodash/fp';
import { FormattedMessage } from '@kbn/i18n-react';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
import { SEVERITY_COLOR } from '../../../overview/components/detection_response/utils';
import { LinkAnchor, useGetSecuritySolutionLinkProps } from '../../../common/components/links';
import {
Direction,
RiskScoreEntity,
EntityType,
RiskScoreFields,
RiskSeverity,
} from '../../../../common/search_strategy';
@ -34,13 +36,16 @@ import { isJobStarted } from '../../../../common/machine_learning/helpers';
import { FormattedCount } from '../../../common/components/formatted_number';
import { useGlobalFilterQuery } from '../../../common/hooks/use_global_filter_query';
import { useRiskScoreKpi } from '../../api/hooks/use_risk_score_kpi';
import type { SeverityCount } from '../severity/types';
const StyledEuiTitle = styled(EuiTitle)`
color: ${SEVERITY_COLOR.critical};
`;
// This is not used by the inspect feature but required by the refresh button
const HOST_RISK_QUERY_ID = 'hostRiskScoreKpiQuery';
const USER_RISK_QUERY_ID = 'userRiskScoreKpiQuery';
const SERVICE_RISK_QUERY_ID = 'serviceRiskScoreKpiQuery';
export const EntityAnalyticsHeader = () => {
const { from, to } = useGlobalTime();
@ -52,6 +57,7 @@ export const EntityAnalyticsHeader = () => {
}),
[from, to]
);
const isServiceEntityStoreEnabled = useIsExperimentalFeatureEnabled('serviceEntityStoreEnabled');
const {
severityCount: hostsSeverityCount,
@ -60,7 +66,7 @@ export const EntityAnalyticsHeader = () => {
refetch: refetchHostRiskScore,
} = useRiskScoreKpi({
timerange,
riskEntity: RiskScoreEntity.host,
riskEntity: EntityType.host,
filterQuery,
});
@ -72,7 +78,18 @@ export const EntityAnalyticsHeader = () => {
} = useRiskScoreKpi({
filterQuery,
timerange,
riskEntity: RiskScoreEntity.user,
riskEntity: EntityType.user,
});
const {
severityCount: servicesSeverityCount,
loading: serviceRiskLoading,
refetch: refetchServiceRiskScore,
inspect: inspectServiceRiskScore,
} = useRiskScoreKpi({
filterQuery,
timerange,
riskEntity: EntityType.service,
});
const { data } = useAggregatedAnomaliesByJob({ skip: false, from, to });
@ -146,6 +163,15 @@ export const EntityAnalyticsHeader = () => {
inspect: inspectHostRiskScore,
});
useQueryInspector({
queryId: SERVICE_RISK_QUERY_ID,
loading: serviceRiskLoading,
refetch: refetchServiceRiskScore,
setQuery,
deleteQuery,
inspect: inspectServiceRiskScore,
});
// Anomaly jobs are enabled if at least one job is started or has data
const areJobsEnabled = useMemo(
() =>
@ -173,56 +199,33 @@ export const EntityAnalyticsHeader = () => {
<EuiPanel hasBorder paddingSize="l">
<EuiFlexGroup justifyContent="spaceAround" responsive={false}>
{isPlatinumOrTrialLicense && (
<EuiFlexItem grow={false}>
<EuiFlexGroup direction="column" gutterSize="s" responsive={false}>
<EuiFlexItem className="eui-textCenter">
<StyledEuiTitle data-test-subj="critical_hosts_quantity" size="l">
<span>
{hostsSeverityCount ? (
<FormattedCount count={hostsSeverityCount[RiskSeverity.Critical]} />
) : (
'-'
)}
</span>
</StyledEuiTitle>
<>
<EuiFlexItem grow={false}>
<CriticalEntitiesCount
entityType={EntityType.host}
severityCount={hostsSeverityCount}
onClick={goToHostRiskTabFilteredByCritical}
href={hostRiskTabUrl}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<CriticalEntitiesCount
entityType={EntityType.user}
severityCount={usersSeverityCount}
onClick={goToUserRiskTabFilteredByCritical}
href={userRiskTabUrl}
/>
</EuiFlexItem>
{isServiceEntityStoreEnabled && (
<EuiFlexItem grow={false}>
<CriticalEntitiesCount
entityType={EntityType.service}
severityCount={servicesSeverityCount}
/>
</EuiFlexItem>
<EuiFlexItem>
<LinkAnchor
onClick={goToHostRiskTabFilteredByCritical}
href={hostRiskTabUrl}
data-test-subj="critical_hosts_link"
>
{i18n.CRITICAL_HOSTS}
</LinkAnchor>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
)}
{isPlatinumOrTrialLicense && (
<EuiFlexItem grow={false}>
<EuiFlexGroup direction="column" gutterSize="s" responsive={false}>
<EuiFlexItem className="eui-textCenter">
<StyledEuiTitle data-test-subj="critical_users_quantity" size="l">
<span>
{usersSeverityCount ? (
<FormattedCount count={usersSeverityCount[RiskSeverity.Critical]} />
) : (
'-'
)}
</span>
</StyledEuiTitle>
</EuiFlexItem>
<EuiFlexItem>
<LinkAnchor
onClick={goToUserRiskTabFilteredByCritical}
href={userRiskTabUrl}
data-test-subj="critical_users_link"
>
{i18n.CRITICAL_USERS}
</LinkAnchor>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
)}
</>
)}
<EuiFlexItem grow={false}>
@ -243,3 +246,44 @@ export const EntityAnalyticsHeader = () => {
</EuiPanel>
);
};
const CriticalEntitiesCount = ({
entityType,
severityCount,
href,
onClick,
}: {
severityCount?: SeverityCount;
href?: string;
onClick?: React.MouseEventHandler;
entityType: EntityType;
}) => {
const CriticalEntitiesText = (
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.header.criticalEntities"
defaultMessage="Critical {entityType}"
values={{ entityType: capitalize(entityType) }}
/>
);
return (
<EuiFlexGroup direction="column" gutterSize="s" responsive={false}>
<EuiFlexItem className="eui-textCenter">
<StyledEuiTitle data-test-subj={`critical_${entityType}s_quantity`} size="l">
<span>
{severityCount ? <FormattedCount count={severityCount[RiskSeverity.Critical]} /> : '-'}
</span>
</StyledEuiTitle>
</EuiFlexItem>
<EuiFlexItem>
{href || onClick ? (
<LinkAnchor onClick={onClick} href={href} data-test-subj={`critical_${entityType}s_link`}>
{CriticalEntitiesText}
</LinkAnchor>
) : (
CriticalEntitiesText
)}
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -14,13 +14,6 @@ export const CRITICAL_HOSTS = i18n.translate(
}
);
export const CRITICAL_USERS = i18n.translate(
'xpack.securitySolution.entityAnalytics.header.criticalUsers',
{
defaultMessage: 'Critical Users',
}
);
export const ANOMALIES = i18n.translate('xpack.securitySolution.entityAnalytics.header.anomalies', {
defaultMessage: 'Anomalies',
});

View file

@ -7,7 +7,7 @@
import { render } from '@testing-library/react';
import React from 'react';
import { RiskScoreEntity, RiskSeverity } from '../../../../common/search_strategy';
import { EntityType } from '../../../../common/entity_analytics/types';
import { VisualizationEmbeddable } from '../../../common/components/visualization_actions/visualization_embeddable';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
import { useSpaceId } from '../../../common/hooks/use_space_id';
@ -15,6 +15,7 @@ import { TestProviders } from '../../../common/mock';
import { generateSeverityFilter } from '../../../explore/hosts/store/helpers';
import { ChartContent } from './chart_content';
import { mockSeverityCount } from './__mocks__';
import { RiskSeverity } from '../../../../common/search_strategy';
jest.mock('../../../common/components/visualization_actions/visualization_embeddable');
jest.mock('../../../common/hooks/use_experimental_features', () => ({
@ -27,7 +28,7 @@ describe('ChartContent', () => {
const props = {
dataExists: true,
kpiQueryId: 'mockQueryId',
riskEntity: RiskScoreEntity.host,
riskEntity: EntityType.host,
severityCount: undefined,
timerange: { from: '2022-04-05T12:00:00.000Z', to: '2022-04-08T12:00:00.000Z' },
selectedSeverity: [RiskSeverity.Unknown],

View file

@ -7,7 +7,8 @@
import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import type { RiskScoreEntity, RiskSeverity } from '../../../../common/search_strategy';
import type { EntityType } from '../../../../common/entity_analytics/types';
import type { RiskSeverity } from '../../../../common/search_strategy';
import { EMPTY_SEVERITY_COUNT } from '../../../../common/search_strategy';
import { VisualizationEmbeddable } from '../../../common/components/visualization_actions/visualization_embeddable';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
@ -28,7 +29,7 @@ const ChartContentComponent = ({
}: {
dataExists?: boolean;
kpiQueryId: string;
riskEntity: RiskScoreEntity;
riskEntity: EntityType;
severityCount: SeverityCount | undefined;
timerange: {
from: string;

View file

@ -10,17 +10,24 @@ import React from 'react';
import type { EuiBasicTableColumn } from '@elastic/eui';
import { EuiLink } from '@elastic/eui';
import styled from 'styled-components';
import { get } from 'lodash/fp';
import { EntityTypeToIdentifierField } from '../../../../common/entity_analytics/types';
import { getEmptyTagValue } from '../../../common/components/empty_value';
import { HostDetailsLink, UserDetailsLink } from '../../../common/components/links';
import { EntityDetailsLink } from '../../../common/components/links';
import { RiskScoreLevel } from '../severity/common';
import { CELL_ACTIONS_TELEMETRY } from '../risk_score/constants';
import type {
HostRiskScore,
EntityRiskScore,
Maybe,
RiskSeverity,
UserRiskScore,
EntityType,
} from '../../../../common/search_strategy';
import {
EntityTypeToLevelField,
EntityTypeToScoreField,
RiskScoreFields,
} from '../../../../common/search_strategy';
import { RiskScoreEntity, RiskScoreFields } from '../../../../common/search_strategy';
import * as i18n from './translations';
import { FormattedCount } from '../../../common/components/formatted_number';
import {
@ -32,8 +39,6 @@ import {
import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date';
import { formatRiskScore } from '../../common';
type HostRiskScoreColumns = Array<EuiBasicTableColumn<HostRiskScore & UserRiskScore>>;
const StyledCellActions = styled(SecurityCellActions)`
padding-left: ${({ theme }) => theme.eui.euiSizeS};
`;
@ -41,147 +46,137 @@ const StyledCellActions = styled(SecurityCellActions)`
type OpenEntityOnAlertsPage = (entityName: string) => void;
type OpenEntityOnExpandableFlyout = (entityName: string) => void;
export const getRiskScoreColumns = (
riskEntity: RiskScoreEntity,
export const getRiskScoreColumns = <E extends EntityType>(
entityType: E,
openEntityOnAlertsPage: OpenEntityOnAlertsPage,
openEntityOnExpandableFlyout: OpenEntityOnExpandableFlyout
): HostRiskScoreColumns => [
{
field: riskEntity === RiskScoreEntity.host ? 'host.name' : 'user.name',
name: i18n.ENTITY_NAME(riskEntity),
truncateText: false,
mobileOptions: { show: true },
className: 'inline-actions-table-cell',
render: (entityName: string) => {
const onEntityDetailsLinkClick = (e: SyntheticEvent) => {
e.preventDefault();
openEntityOnExpandableFlyout(entityName);
};
): Array<EuiBasicTableColumn<EntityRiskScore<E>>> => {
const fieldName = EntityTypeToIdentifierField[entityType];
const getEntityName = get(fieldName);
const getEntityDetailsLinkComponent = (entityName: string) => {
const onEntityDetailsLinkClick: (e: SyntheticEvent) => void = (e) => {
e.preventDefault();
openEntityOnExpandableFlyout(entityName);
};
if (entityName != null && entityName.length > 0) {
return riskEntity === RiskScoreEntity.host ? (
<>
<HostDetailsLink hostName={entityName} onClick={onEntityDetailsLinkClick} />
<StyledCellActions
data={{
value: entityName,
field: 'host.name',
}}
triggerId={SecurityCellActionsTrigger.DEFAULT}
mode={CellActionsMode.INLINE}
visibleCellActions={2}
disabledActionTypes={[
SecurityCellActionType.FILTER,
SecurityCellActionType.SHOW_TOP_N,
]}
metadata={{
telemetry: CELL_ACTIONS_TELEMETRY,
}}
/>
</>
) : (
<>
<UserDetailsLink userName={entityName} onClick={onEntityDetailsLinkClick} />
return (
<EntityDetailsLink
entityType={entityType}
entityName={entityName}
onClick={onEntityDetailsLinkClick}
/>
);
};
<StyledCellActions
data={{
value: entityName,
field: 'user.name',
}}
triggerId={SecurityCellActionsTrigger.DEFAULT}
mode={CellActionsMode.INLINE}
disabledActionTypes={[
SecurityCellActionType.FILTER,
SecurityCellActionType.SHOW_TOP_N,
]}
/>
</>
);
}
return getEmptyTagValue();
},
},
return [
{
field: fieldName,
name: i18n.ENTITY_NAME(entityType),
truncateText: false,
mobileOptions: { show: true },
className: 'inline-actions-table-cell',
render: (entityName: string) => {
if (entityName != null && entityName.length > 0) {
return (
<>
{getEntityDetailsLinkComponent(entityName)}
{
field: RiskScoreFields.timestamp,
name: i18n.LAST_UPDATED,
truncateText: false,
mobileOptions: { show: true },
sortable: true,
width: '20%',
render: (lastSeen: Maybe<string>) => {
if (lastSeen != null) {
return <FormattedRelativePreferenceDate value={lastSeen} />;
}
return getEmptyTagValue();
<StyledCellActions
data={{
value: entityName,
field: fieldName,
}}
triggerId={SecurityCellActionsTrigger.DEFAULT}
mode={CellActionsMode.INLINE}
visibleCellActions={2}
disabledActionTypes={[
SecurityCellActionType.FILTER,
SecurityCellActionType.SHOW_TOP_N,
]}
metadata={{
telemetry: CELL_ACTIONS_TELEMETRY,
}}
/>
</>
);
}
return getEmptyTagValue();
},
},
},
{
field:
riskEntity === RiskScoreEntity.host
? RiskScoreFields.hostRiskScore
: RiskScoreFields.userRiskScore,
width: '15%',
name: i18n.RISK_SCORE_TITLE(riskEntity),
truncateText: true,
mobileOptions: { show: true },
render: (riskScore: number) => {
if (riskScore != null) {
return (
<span data-test-subj="risk-score-truncate" title={`${riskScore}`}>
{formatRiskScore(riskScore)}
</span>
);
}
return getEmptyTagValue();
{
field: RiskScoreFields.timestamp,
name: i18n.LAST_UPDATED,
truncateText: false,
mobileOptions: { show: true },
sortable: true,
width: '20%',
render: (lastSeen: Maybe<string>) => {
if (lastSeen != null) {
return <FormattedRelativePreferenceDate value={lastSeen} />;
}
return getEmptyTagValue();
},
},
},
{
field:
riskEntity === RiskScoreEntity.host ? RiskScoreFields.hostRisk : RiskScoreFields.userRisk,
width: '25%',
name: i18n.ENTITY_RISK_LEVEL(riskEntity),
truncateText: false,
mobileOptions: { show: true },
render: (risk: RiskSeverity) => {
if (risk != null) {
return <RiskScoreLevel severity={risk} />;
}
return getEmptyTagValue();
{
field: EntityTypeToScoreField[entityType],
width: '15%',
name: i18n.RISK_SCORE_TITLE(entityType),
truncateText: true,
mobileOptions: { show: true },
render: (riskScore: number) => {
if (riskScore != null) {
return (
<span data-test-subj="risk-score-truncate" title={`${riskScore}`}>
{formatRiskScore(riskScore)}
</span>
);
}
return getEmptyTagValue();
},
},
},
{
field: RiskScoreFields.alertsCount,
width: '10%',
name: i18n.ALERTS,
truncateText: false,
mobileOptions: { show: true },
className: 'inline-actions-table-cell',
render: (alertCount: number, risk) => (
<>
<EuiLink
data-test-subj="risk-score-alerts"
disabled={alertCount === 0}
onClick={() =>
openEntityOnAlertsPage(
riskEntity === RiskScoreEntity.host ? risk.host.name : risk.user.name
)
}
>
<FormattedCount count={alertCount} />
</EuiLink>
<StyledCellActions
data={{
value: riskEntity === RiskScoreEntity.host ? risk.host.name : risk.user.name,
field: riskEntity === RiskScoreEntity.host ? 'host.name' : 'user.name',
}}
mode={CellActionsMode.INLINE}
triggerId={SecurityCellActionsTrigger.ALERTS_COUNT}
metadata={{
andFilters: [{ field: 'kibana.alert.workflow_status', value: 'open' }],
}}
/>
</>
),
},
];
{
field: EntityTypeToLevelField[entityType],
width: '25%',
name: i18n.ENTITY_RISK_LEVEL(entityType),
truncateText: false,
mobileOptions: { show: true },
render: (risk: RiskSeverity) => {
if (risk != null) {
return <RiskScoreLevel severity={risk} />;
}
return getEmptyTagValue();
},
},
{
field: RiskScoreFields.alertsCount,
width: '10%',
name: i18n.ALERTS,
truncateText: false,
mobileOptions: { show: true },
className: 'inline-actions-table-cell',
render: (alertCount: number, risk) => (
<>
<EuiLink
data-test-subj="risk-score-alerts"
disabled={alertCount === 0}
onClick={() => openEntityOnAlertsPage(getEntityName(risk))}
>
<FormattedCount count={alertCount} />
</EuiLink>
<StyledCellActions
data={{
value: getEntityName(risk),
field: fieldName,
}}
mode={CellActionsMode.INLINE}
triggerId={SecurityCellActionsTrigger.ALERTS_COUNT}
metadata={{
andFilters: [{ field: 'kibana.alert.workflow_status', value: 'open' }],
}}
/>
</>
),
},
];
};

View file

@ -4,11 +4,10 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { RenderResult } from '@testing-library/react';
import { render } from '@testing-library/react';
import React from 'react';
import { SecurityPageName } from '../../../../common/constants';
import { RiskScoreEntity } from '../../../../common/search_strategy';
import { EntityType } from '../../../../common/entity_analytics/types';
import { useGetSecuritySolutionLinkProps } from '../../../common/components/links';
import { RiskScoreHeaderContent } from './header_content';
@ -21,58 +20,57 @@ jest.mock('../../../common/components/links', () => {
});
const mockGetSecuritySolutionLinkProps = jest
.fn()
.mockReturnValue({ onClick: jest.fn(), href: '' });
.mockReturnValue({ onClick: jest.fn(), href: '/test' });
const defaultProps = {
entityLinkProps: {
deepLinkId: SecurityPageName.users,
onClick: jest.fn(),
path: '/userRisk',
},
onSelectSeverityFilter: jest.fn(),
riskEntity: EntityType.user,
selectedSeverity: [],
toggleStatus: true,
};
describe('RiskScoreHeaderContent', () => {
let res: RenderResult;
jest.clearAllMocks();
(useGetSecuritySolutionLinkProps as jest.Mock).mockReturnValue(mockGetSecuritySolutionLinkProps);
beforeEach(() => {
res = render(
<RiskScoreHeaderContent
entityLinkProps={{
deepLinkId: SecurityPageName.users,
onClick: jest.fn(),
path: '/userRisk',
}}
onSelectSeverityFilter={jest.fn()}
riskEntity={RiskScoreEntity.user}
selectedSeverity={[]}
toggleStatus={true}
/>
jest.clearAllMocks();
(useGetSecuritySolutionLinkProps as jest.Mock).mockReturnValue(
mockGetSecuritySolutionLinkProps
);
});
it('should render when toggleStatus = true', () => {
const res = render(<RiskScoreHeaderContent {...defaultProps} />);
expect(res.getByTestId(`user-risk-score-header-content`)).toBeInTheDocument();
});
it('should render learn more button', () => {
const res = render(<RiskScoreHeaderContent {...defaultProps} />);
expect(res.getByText(`How is risk score calculated?`)).toBeInTheDocument();
});
it('should render severity filter group', () => {
const res = render(<RiskScoreHeaderContent {...defaultProps} />);
expect(res.getByTestId(`risk-filter`)).toBeInTheDocument();
});
it('should render view all button', () => {
const res = render(<RiskScoreHeaderContent {...defaultProps} />);
expect(res.getByTestId(`view-all-button`)).toBeInTheDocument();
});
it('should not render if toggleStatus = false', () => {
res = render(
<RiskScoreHeaderContent
entityLinkProps={{
deepLinkId: SecurityPageName.users,
onClick: jest.fn(),
path: '/userRisk',
}}
onSelectSeverityFilter={jest.fn()}
riskEntity={RiskScoreEntity.user}
selectedSeverity={[]}
toggleStatus={false}
/>
const res = render(<RiskScoreHeaderContent {...defaultProps} toggleStatus={false} />);
expect(res.queryByTestId(`view-all-button`)).not.toBeInTheDocument();
});
it('should render when entity type is service', () => {
const res = render(
<RiskScoreHeaderContent {...defaultProps} riskEntity={EntityType.service} />
);
expect(res.getByTestId(`view-all-button`)).toBeInTheDocument();
expect(res.getByTestId(`service-risk-score-header-content`)).toBeInTheDocument();
});
});

View file

@ -4,9 +4,11 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { MouseEventHandler } from 'react';
import React, { useMemo } from 'react';
import { EuiFilterGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import type { RiskSeverity, RiskScoreEntity } from '../../../../common/search_strategy';
import type { RiskSeverity } from '../../../../common/search_strategy';
import type { EntityType } from '../../../../common/entity_analytics/types';
import { SeverityFilter } from '../severity/severity_filter';
import { LinkButton, useGetSecuritySolutionLinkProps } from '../../../common/components/links';
import type { SecurityPageName } from '../../../../common/constants';
@ -20,22 +22,24 @@ const RiskScoreHeaderContentComponent = ({
selectedSeverity,
toggleStatus,
}: {
entityLinkProps: {
entityLinkProps?: {
deepLinkId: SecurityPageName;
path: string;
onClick: () => void;
};
onSelectSeverityFilter: (newSelection: RiskSeverity[]) => void;
riskEntity: RiskScoreEntity;
riskEntity: EntityType;
selectedSeverity: RiskSeverity[];
toggleStatus: boolean;
}) => {
const getSecuritySolutionLinkProps = useGetSecuritySolutionLinkProps();
const [goToEntityRiskTab, entityRiskTabUrl] = useMemo(() => {
const { onClick, href } = getSecuritySolutionLinkProps(entityLinkProps);
const { onClick, href }: { onClick?: MouseEventHandler<HTMLAnchorElement>; href?: string } =
entityLinkProps ? getSecuritySolutionLinkProps(entityLinkProps) : {};
return [onClick, href];
}, [entityLinkProps, getSecuritySolutionLinkProps]);
return toggleStatus ? (
<EuiFlexGroup
alignItems="center"
@ -55,13 +59,15 @@ const RiskScoreHeaderContentComponent = ({
</EuiFilterGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<LinkButton
data-test-subj="view-all-button"
onClick={goToEntityRiskTab}
href={entityRiskTabUrl}
>
{i18n.VIEW_ALL}
</LinkButton>
{entityRiskTabUrl && (
<LinkButton
data-test-subj="view-all-button"
onClick={goToEntityRiskTab}
href={entityRiskTabUrl}
>
{i18n.VIEW_ALL}
</LinkButton>
)}
</EuiFlexItem>
</EuiFlexGroup>
) : null;

View file

@ -9,13 +9,15 @@ import { render, fireEvent, waitFor } from '@testing-library/react';
import React from 'react';
import { TestProviders } from '../../../common/mock';
import { EntityAnalyticsRiskScores } from '.';
import { RiskScoreEntity, RiskSeverity } from '../../../../common/search_strategy';
import { EntityType, EntityTypeToIdentifierField } from '../../../../common/entity_analytics/types';
import type { SeverityCount } from '../severity/types';
import { useKibana as mockUseKibana } from '../../../common/lib/kibana/__mocks__';
import { createTelemetryServiceMock } from '../../../common/lib/telemetry/telemetry_service.mock';
import { useRiskScore } from '../../api/hooks/use_risk_score';
import { useRiskScoreKpi } from '../../api/hooks/use_risk_score_kpi';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { RiskSeverity } from '../../../../common/search_strategy';
import { capitalize } from 'lodash/fp';
const mockedTelemetry = createTelemetryServiceMock();
const mockedUseKibana = mockUseKibana();
@ -73,7 +75,7 @@ jest.mock('../../../common/hooks/use_navigate_to_alerts_page_with_filters', () =
const mockOpenRightPanel = jest.fn();
jest.mock('@kbn/expandable-flyout');
describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
describe.each([EntityType.host, EntityType.user, EntityType.service])(
'EntityAnalyticsRiskScores entityType: %s',
(riskEntity) => {
beforeEach(() => {
@ -222,64 +224,68 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
await waitFor(() => {
expect(mockOpenAlertsPageWithFilters.mock.calls[0][0]).toEqual([
{
title: riskEntity === RiskScoreEntity.host ? 'Host' : 'User',
fieldName: riskEntity === RiskScoreEntity.host ? 'host.name' : 'user.name',
title: capitalize(riskEntity),
fieldName: EntityTypeToIdentifierField[riskEntity],
selectedOptions: [name],
},
]);
});
});
it('opens the expandable flyout when entity name is clicked', async () => {
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() });
mockUseRiskScoreKpi.mockReturnValue({
severityCount: mockSeverityCount,
loading: false,
});
const name = 'testName';
const data = [
{
'@timestamp': '1234567899',
[riskEntity]: {
name,
risk: {
rule_risks: [],
calculated_level: RiskSeverity.High,
calculated_score_norm: 75,
multipliers: [],
},
},
alertsCount: 0,
},
];
mockUseRiskScore.mockReturnValue({ ...defaultProps, data });
const { getByTestId, queryByTestId } = render(
<TestProviders>
<EntityAnalyticsRiskScores riskEntity={riskEntity} />
</TestProviders>
);
await waitFor(() => {
expect(queryByTestId('loadingPanelRiskScore')).not.toBeInTheDocument();
});
const detailsButton = getByTestId(
riskEntity === RiskScoreEntity.host ? `host-details-button` : `users-link-anchor`
);
fireEvent.click(detailsButton);
await waitFor(() => {
expect(mockOpenRightPanel).toHaveBeenCalledWith({
id: `${riskEntity}-panel`,
params: {
[riskEntity === RiskScoreEntity.host ? `hostName` : `userName`]: 'testName',
contextID: 'entity-risk-score-table',
scopeId: 'entity-risk-score-table',
},
// Skip service entity test while it doesn't has a flyout
(riskEntity === EntityType.service ? it.skip : it)(
'opens the expandable flyout when entity name is clicked',
async () => {
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() });
mockUseRiskScoreKpi.mockReturnValue({
severityCount: mockSeverityCount,
loading: false,
});
});
});
const name = 'testName';
const data = [
{
'@timestamp': '1234567899',
[riskEntity]: {
name,
risk: {
rule_risks: [],
calculated_level: RiskSeverity.High,
calculated_score_norm: 75,
multipliers: [],
},
},
alertsCount: 0,
},
];
mockUseRiskScore.mockReturnValue({ ...defaultProps, data });
const { getByTestId, queryByTestId } = render(
<TestProviders>
<EntityAnalyticsRiskScores riskEntity={riskEntity} />
</TestProviders>
);
await waitFor(() => {
expect(queryByTestId('loadingPanelRiskScore')).not.toBeInTheDocument();
});
const detailsButton = getByTestId(
riskEntity === EntityType.host ? `host-details-button` : `users-link-anchor`
);
fireEvent.click(detailsButton);
await waitFor(() => {
expect(mockOpenRightPanel).toHaveBeenCalledWith({
id: `${riskEntity}-panel`,
params: {
[riskEntity === EntityType.host ? `hostName` : `userName`]: 'testName',
contextID: 'entity-risk-score-table',
scopeId: 'entity-risk-score-table',
},
});
});
}
);
}
);

View file

@ -8,15 +8,21 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { HostPanelKey } from '../../../flyout/entity_details/host_right';
import type { RiskSeverity } from '../../../../common/search_strategy';
import { useQueryInspector } from '../../../common/components/page/manage_query';
import {
EntityPanelKeyByType,
EntityPanelParamByType,
} from '../../../flyout/entity_details/shared/constants';
import { EnableRiskScore } from '../enable_risk_score';
import { getRiskScoreColumns } from './columns';
import { LastUpdatedAt } from '../../../common/components/last_updated_at';
import { HeaderSection } from '../../../common/components/header_section';
import type { RiskSeverity } from '../../../../common/search_strategy';
import { RiskScoreEntity } from '../../../../common/search_strategy';
import {
type EntityType,
EntityTypeToIdentifierField,
} from '../../../../common/entity_analytics/types';
import { generateSeverityFilter } from '../../../explore/hosts/store/helpers';
import { useQueryInspector } from '../../../common/components/page/manage_query';
import { useGlobalTime } from '../../../common/containers/use_global_time';
import { InspectButtonContainer } from '../../../common/components/inspect';
import { useQueryToggle } from '../../../common/containers/query_toggle';
@ -35,20 +41,24 @@ import { useKibana } from '../../../common/lib/kibana';
import { useGlobalFilterQuery } from '../../../common/hooks/use_global_filter_query';
import { useRiskScoreKpi } from '../../api/hooks/use_risk_score_kpi';
import { useRiskScore } from '../../api/hooks/use_risk_score';
import { UserPanelKey } from '../../../flyout/entity_details/user_right';
import { RiskEnginePrivilegesCallOut } from '../risk_engine_privileges_callout';
import { useMissingRiskEnginePrivileges } from '../../hooks/use_missing_risk_engine_privileges';
import { EntityEventTypes } from '../../../common/lib/telemetry';
export const ENTITY_RISK_SCORE_TABLE_ID = 'entity-risk-score-table';
const EntityAnalyticsRiskScoresComponent = ({ riskEntity }: { riskEntity: RiskScoreEntity }) => {
const EntityAnalyticsRiskScoresComponent = <T extends EntityType>({
riskEntity,
}: {
riskEntity: T;
}) => {
const { deleteQuery, setQuery, from, to } = useGlobalTime();
const [updatedAt, setUpdatedAt] = useState<number>(Date.now());
const entity = useEntityInfo(riskEntity);
const openAlertsPageWithFilters = useNavigateToAlertsPageWithFilters();
const { telemetry } = useKibana().services;
const { openRightPanel } = useExpandableFlyoutApi();
const entityNameField = EntityTypeToIdentifierField[riskEntity];
const openEntityOnAlertsPage = useCallback(
(entityName: string) => {
@ -57,23 +67,27 @@ const EntityAnalyticsRiskScoresComponent = ({ riskEntity }: { riskEntity: RiskSc
{
title: getRiskEntityTranslation(riskEntity),
selectedOptions: [entityName],
fieldName: riskEntity === RiskScoreEntity.host ? 'host.name' : 'user.name',
fieldName: entityNameField,
},
]);
},
[telemetry, riskEntity, openAlertsPageWithFilters]
[telemetry, riskEntity, openAlertsPageWithFilters, entityNameField]
);
const openEntityOnExpandableFlyout = useCallback(
(entityName: string) => {
openRightPanel({
id: riskEntity === RiskScoreEntity.host ? HostPanelKey : UserPanelKey,
params: {
[riskEntity === RiskScoreEntity.host ? 'hostName' : 'userName']: entityName,
contextID: ENTITY_RISK_SCORE_TABLE_ID,
scopeId: ENTITY_RISK_SCORE_TABLE_ID,
},
});
const panelKey = EntityPanelKeyByType[riskEntity];
const panelParam = EntityPanelParamByType[riskEntity];
if (panelKey && panelParam) {
openRightPanel({
id: panelKey,
params: {
[panelParam]: entityName,
contextID: ENTITY_RISK_SCORE_TABLE_ID,
scopeId: ENTITY_RISK_SCORE_TABLE_ID,
},
});
}
},
[openRightPanel, riskEntity]
);
@ -126,6 +140,7 @@ const EntityAnalyticsRiskScoresComponent = ({ riskEntity }: { riskEntity: RiskSc
deleteQuery,
inspect: inspectKpi,
});
const {
data,
loading: isTableLoading,
@ -221,7 +236,7 @@ const EntityAnalyticsRiskScoresComponent = ({ riskEntity }: { riskEntity: RiskSc
<EuiFlexItem grow={false}>
<ChartContent
dataExists={data && data.length > 0}
kpiQueryId={entity.kpiQueryId}
kpiQueryId={entity.kpiQueryId ?? ''}
riskEntity={riskEntity}
severityCount={severityCount}
timerange={timerange}

View file

@ -7,10 +7,10 @@
import { i18n } from '@kbn/i18n';
import { getRiskEntityTranslation } from '../risk_score/translations';
import type { RiskScoreEntity } from '../../../../common/search_strategy';
import type { EntityType } from '../../../../common/entity_analytics/types';
export * from '../risk_score/translations';
export const ENTITY_NAME = (riskEntity: RiskScoreEntity) =>
export const ENTITY_NAME = (riskEntity: EntityType) =>
i18n.translate('xpack.securitySolution.entityAnalytics.riskDashboard.nameTitle', {
defaultMessage: '{riskEntity} Name',
values: {

View file

@ -6,8 +6,8 @@
*/
import { renderHook } from '@testing-library/react';
import { RiskScoreEntity } from '../../../../common/search_strategy/security_solution/risk_score';
import { useEntityInfo } from './use_entity';
import { EntityType } from '../../../../common/entity_analytics/types';
jest.mock('react-redux', () => {
const actual = jest.requireActual('react-redux');
@ -19,10 +19,10 @@ jest.mock('react-redux', () => {
describe('useEntityInfo', () => {
it('should return host entity info', () => {
const { result } = renderHook(() => useEntityInfo(RiskScoreEntity.host));
const { result } = renderHook(() => useEntityInfo(EntityType.host));
expect(result?.current).toMatchInlineSnapshot(`
Object {
"kpiQueryId": "headerHostRiskScoreKpiQuery",
"kpiQueryId": "hostHeaderRiskScoreKpiQuery",
"linkProps": Object {
"deepLinkId": "hosts",
"onClick": [Function],
@ -33,10 +33,10 @@ describe('useEntityInfo', () => {
`);
});
it('should return user entity info', () => {
const { result } = renderHook(() => useEntityInfo(RiskScoreEntity.user));
const { result } = renderHook(() => useEntityInfo(EntityType.user));
expect(result?.current).toMatchInlineSnapshot(`
Object {
"kpiQueryId": "headerUserRiskScoreKpiQuery",
"kpiQueryId": "userHeaderRiskScoreKpiQuery",
"linkProps": Object {
"deepLinkId": "users",
"onClick": [Function],
@ -46,4 +46,15 @@ describe('useEntityInfo', () => {
}
`);
});
it('should return service entity info', () => {
const { result } = renderHook(() => useEntityInfo(EntityType.service));
expect(result?.current).toMatchInlineSnapshot(`
Object {
"kpiQueryId": "serviceHeaderRiskScoreKpiQuery",
"linkProps": undefined,
"tableQueryId": "serviceRiskDashboardTable",
}
`);
});
});

View file

@ -5,55 +5,58 @@
* 2.0.
*/
import { useDispatch } from 'react-redux';
import { EntityType } from '../../../../common/entity_analytics/types';
import { getTabsOnUsersUrl } from '../../../common/components/link_to/redirect_to_users';
import { UsersTableType } from '../../../explore/users/store/model';
import { getTabsOnHostsUrl } from '../../../common/components/link_to/redirect_to_hosts';
import { HostsTableType, HostsType } from '../../../explore/hosts/store/model';
import { RiskScoreEntity } from '../../../../common/search_strategy/security_solution/risk_score';
import { usersActions } from '../../../explore/users/store';
import { hostsActions } from '../../../explore/hosts/store';
import { SecurityPageName } from '../../../app/types';
const HOST_RISK_TABLE_QUERY_ID = 'hostRiskDashboardTable';
const HOST_RISK_KPI_QUERY_ID = 'headerHostRiskScoreKpiQuery';
const USER_RISK_TABLE_QUERY_ID = 'userRiskDashboardTable';
const USER_RISK_KPI_QUERY_ID = 'headerUserRiskScoreKpiQuery';
export const useEntityInfo = (riskEntity: RiskScoreEntity) => {
export const useEntityInfo = (riskEntity: EntityType) => {
const dispatch = useDispatch();
return riskEntity === RiskScoreEntity.host
? {
linkProps: {
deepLinkId: SecurityPageName.hosts,
path: getTabsOnHostsUrl(HostsTableType.risk),
onClick: () => {
dispatch(
hostsActions.updateHostRiskScoreSeverityFilter({
severitySelection: [],
hostsType: HostsType.page,
})
);
},
const tableQueryIds = {
tableQueryId: `${riskEntity}RiskDashboardTable`,
kpiQueryId: `${riskEntity}HeaderRiskScoreKpiQuery`,
};
if (riskEntity === EntityType.host) {
return {
linkProps: {
deepLinkId: SecurityPageName.hosts,
path: getTabsOnHostsUrl(HostsTableType.risk),
onClick: () => {
dispatch(
hostsActions.updateHostRiskScoreSeverityFilter({
severitySelection: [],
hostsType: HostsType.page,
})
);
},
tableQueryId: HOST_RISK_TABLE_QUERY_ID,
kpiQueryId: HOST_RISK_KPI_QUERY_ID,
}
: {
linkProps: {
deepLinkId: SecurityPageName.users,
path: getTabsOnUsersUrl(UsersTableType.risk),
onClick: () => {
dispatch(
usersActions.updateUserRiskScoreSeverityFilter({
severitySelection: [],
})
);
},
},
...tableQueryIds,
};
} else if (riskEntity === EntityType.user) {
return {
linkProps: {
deepLinkId: SecurityPageName.users,
path: getTabsOnUsersUrl(UsersTableType.risk),
onClick: () => {
dispatch(
usersActions.updateUserRiskScoreSeverityFilter({
severitySelection: [],
})
);
},
tableQueryId: USER_RISK_TABLE_QUERY_ID,
kpiQueryId: USER_RISK_KPI_QUERY_ID,
};
},
...tableQueryIds,
};
}
return {
linkProps: undefined,
...tableQueryIds,
};
};

View file

@ -7,6 +7,7 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import type { EntityType } from '../../../../common/search_strategy';
import { EntityDetailsLeftPanelTab } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header';
import { PREFIX } from '../../../flyout/shared/test_ids';
import type { RiskInputsTabProps } from './tabs/risk_inputs/risk_inputs_tab';
@ -16,7 +17,11 @@ import { InsightsTabCsp } from '../../../cloud_security_posture/components/csp_d
export const RISK_INPUTS_TAB_TEST_ID = `${PREFIX}RiskInputsTab` as const;
export const INSIGHTS_TAB_TEST_ID = `${PREFIX}InsightInputsTab` as const;
export const getRiskInputTab = ({ entityType, entityName, scopeId }: RiskInputsTabProps) => ({
export const getRiskInputTab = <T extends EntityType>({
entityType,
entityName,
scopeId,
}: RiskInputsTabProps<T>) => ({
id: EntityDetailsLeftPanelTab.RISK_INPUTS,
'data-test-subj': RISK_INPUTS_TAB_TEST_ID,
name: (

View file

@ -12,7 +12,7 @@ import { times } from 'lodash/fp';
import { EXPAND_ALERT_TEST_ID, RiskInputsTab } from './risk_inputs_tab';
import { alertInputDataMock } from '../../mocks';
import { RiskSeverity } from '../../../../../../common/search_strategy';
import { RiskScoreEntity } from '../../../../../../common/entity_analytics/risk_engine';
import { EntityType } from '../../../../../../common/entity_analytics/types';
const mockUseRiskContributingAlerts = jest.fn().mockReturnValue({ loading: false, data: [] });
@ -74,7 +74,7 @@ describe('RiskInputsTab', () => {
const { getByTestId, queryByTestId } = render(
<TestProviders>
<RiskInputsTab entityType={RiskScoreEntity.user} entityName="elastic" scopeId={'scopeId'} />
<RiskInputsTab entityType={EntityType.user} entityName="elastic" scopeId={'scopeId'} />
</TestProviders>
);
@ -87,7 +87,7 @@ describe('RiskInputsTab', () => {
const { queryByTestId } = render(
<TestProviders>
<RiskInputsTab entityType={RiskScoreEntity.user} entityName="elastic" scopeId={'scopeId'} />
<RiskInputsTab entityType={EntityType.user} entityName="elastic" scopeId={'scopeId'} />
</TestProviders>
);
@ -116,7 +116,7 @@ describe('RiskInputsTab', () => {
const { queryByTestId } = render(
<TestProviders>
<RiskInputsTab entityType={RiskScoreEntity.user} entityName="elastic" scopeId={'scopeId'} />
<RiskInputsTab entityType={EntityType.user} entityName="elastic" scopeId={'scopeId'} />
</TestProviders>
);
@ -137,7 +137,7 @@ describe('RiskInputsTab', () => {
const { getByTestId } = render(
<TestProviders>
<RiskInputsTab entityType={RiskScoreEntity.user} entityName="elastic" scopeId={'scopeId'} />
<RiskInputsTab entityType={EntityType.user} entityName="elastic" scopeId={'scopeId'} />
</TestProviders>
);
@ -155,7 +155,7 @@ describe('RiskInputsTab', () => {
const { getByTestId } = render(
<TestProviders>
<RiskInputsTab entityType={RiskScoreEntity.user} entityName="elastic" scopeId={'scopeId'} />
<RiskInputsTab entityType={EntityType.user} entityName="elastic" scopeId={'scopeId'} />
</TestProviders>
);
const contextsTable = getByTestId('risk-input-contexts-table');
@ -174,7 +174,7 @@ describe('RiskInputsTab', () => {
const { getByTestId } = render(
<TestProviders>
<RiskInputsTab entityType={RiskScoreEntity.user} entityName="elastic" scopeId={'scopeId'} />
<RiskInputsTab entityType={EntityType.user} entityName="elastic" scopeId={'scopeId'} />
</TestProviders>
);
const contextsTable = getByTestId('risk-input-contexts-table');
@ -193,7 +193,7 @@ describe('RiskInputsTab', () => {
const { getByTestId } = render(
<TestProviders>
<RiskInputsTab entityType={RiskScoreEntity.user} entityName="elastic" scopeId={'scopeId'} />
<RiskInputsTab entityType={EntityType.user} entityName="elastic" scopeId={'scopeId'} />
</TestProviders>
);
@ -222,7 +222,7 @@ describe('RiskInputsTab', () => {
const { queryByTestId } = render(
<TestProviders>
<RiskInputsTab entityType={RiskScoreEntity.user} entityName="elastic" scopeId={'scopeId'} />
<RiskInputsTab entityType={EntityType.user} entityName="elastic" scopeId={'scopeId'} />
</TestProviders>
);

View file

@ -25,19 +25,18 @@ import { useRiskContributingAlerts } from '../../../../hooks/use_risk_contributi
import { PreferenceFormattedDate } from '../../../../../common/components/formatted_date';
import { useRiskScore } from '../../../../api/hooks/use_risk_score';
import type { HostRiskScore, UserRiskScore } from '../../../../../../common/search_strategy';
import type { EntityRiskScore } from '../../../../../../common/search_strategy';
import {
buildHostNamesFilter,
buildUserNamesFilter,
isUserRiskScore,
EntityType,
} from '../../../../../../common/search_strategy';
import { RiskScoreEntity } from '../../../../../../common/entity_analytics/risk_engine';
import { AssetCriticalityBadge } from '../../../asset_criticality';
import { RiskInputsUtilityBar } from '../../components/utility_bar';
import { ActionColumn } from '../../components/action_column';
export interface RiskInputsTabProps extends Record<string, unknown> {
entityType: RiskScoreEntity;
export interface RiskInputsTabProps<T extends EntityType> {
entityType: T;
entityName: string;
scopeId: string;
}
@ -50,14 +49,19 @@ const FIRST_RECORD_PAGINATION = {
export const EXPAND_ALERT_TEST_ID = 'risk-input-alert-preview-button';
export const RISK_INPUTS_TAB_QUERY_ID = 'RiskInputsTabQuery';
export const RiskInputsTab = ({ entityType, entityName, scopeId }: RiskInputsTabProps) => {
export const RiskInputsTab = <T extends EntityType>({
entityType,
entityName,
scopeId,
}: RiskInputsTabProps<T>) => {
const { setQuery, deleteQuery } = useGlobalTime();
const [selectedItems, setSelectedItems] = useState<InputAlert[]>([]);
const nameFilterQuery = useMemo(() => {
if (entityType === RiskScoreEntity.host) {
// TODO Add support for services on a follow-up PR
if (entityType === EntityType.host) {
return buildHostNamesFilter([entityName]);
} else if (entityType === RiskScoreEntity.user) {
} else if (entityType === EntityType.user) {
return buildUserNamesFilter([entityName]);
}
}, [entityName, entityType]);
@ -68,7 +72,7 @@ export const RiskInputsTab = ({ entityType, entityName, scopeId }: RiskInputsTab
loading: loadingRiskScore,
inspect: inspectRiskScore,
refetch,
} = useRiskScore({
} = useRiskScore<T>({
riskEntity: entityType,
filterQuery: nameFilterQuery,
onlyLatest: false,
@ -87,7 +91,7 @@ export const RiskInputsTab = ({ entityType, entityName, scopeId }: RiskInputsTab
const riskScore = riskScoreData && riskScoreData.length > 0 ? riskScoreData[0] : undefined;
const alerts = useRiskContributingAlerts({ riskScore });
const alerts = useRiskContributingAlerts<T>({ riskScore, entityType });
const euiTableSelectionProps = useMemo(
() => ({
@ -212,13 +216,17 @@ export const RiskInputsTab = ({ entityType, entityName, scopeId }: RiskInputsTab
itemId="_id"
/>
<EuiSpacer size="s" />
<ExtraAlertsMessage riskScore={riskScore} alerts={alerts} />
<ExtraAlertsMessage<T> riskScore={riskScore} alerts={alerts} entityType={entityType} />
</>
);
return (
<>
<ContextsSection loading={loadingRiskScore} riskScore={riskScore} />
<ContextsSection<T>
loading={loadingRiskScore}
riskScore={riskScore}
entityType={entityType}
/>
<EuiSpacer size="m" />
{riskInputsAlertSection}
</>
@ -227,27 +235,27 @@ export const RiskInputsTab = ({ entityType, entityName, scopeId }: RiskInputsTab
RiskInputsTab.displayName = 'RiskInputsTab';
const ContextsSection: React.FC<{
riskScore?: UserRiskScore | HostRiskScore;
interface ContextsSectionProps<T extends EntityType> {
riskScore?: EntityRiskScore<T>;
entityType: T;
loading: boolean;
}> = ({ riskScore, loading }) => {
}
const ContextsSection = <T extends EntityType>({
riskScore,
loading,
entityType,
}: ContextsSectionProps<T>) => {
const criticality = useMemo(() => {
if (!riskScore) {
return undefined;
}
if (isUserRiskScore(riskScore)) {
return {
level: riskScore.user.risk.criticality_level,
contribution: riskScore.user.risk.category_2_score,
};
}
return {
level: riskScore.host.risk.criticality_level,
contribution: riskScore.host.risk.category_2_score,
level: riskScore[entityType].risk.criticality_level,
contribution: riskScore[entityType].risk.category_2_score,
};
}, [riskScore]);
}, [entityType, riskScore]);
if (loading || criticality === undefined) {
return null;
@ -334,16 +342,23 @@ const contextColumns: Array<EuiBasicTableColumn<ContextRow>> = [
},
];
interface ExtraAlertsMessageProps {
riskScore?: UserRiskScore | HostRiskScore;
interface ExtraAlertsMessageProps<T extends EntityType> {
riskScore?: EntityRiskScore<T>;
alerts: UseRiskContributingAlertsResult;
entityType: T;
}
const ExtraAlertsMessage: React.FC<ExtraAlertsMessageProps> = ({ riskScore, alerts }) => {
const ExtraAlertsMessage = <T extends EntityType>({
riskScore,
alerts,
entityType,
}: ExtraAlertsMessageProps<T>) => {
const totals = !riskScore
? { count: 0, score: 0 }
: isUserRiskScore(riskScore)
? { count: riskScore.user.risk.category_1_count, score: riskScore.user.risk.category_1_score }
: { count: riskScore.host.risk.category_1_count, score: riskScore.host.risk.category_1_score };
: {
count: riskScore[entityType].risk.category_1_count,
score: riskScore[entityType].risk.category_1_score,
};
const displayed = {
count: alerts.data?.length || 0,

View file

@ -15,8 +15,8 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { useStoreEntityTypes } from '../../../hooks/use_enabled_entity_types';
import { RiskEngineStatusEnum } from '../../../../../common/api/entity_analytics';
import { RiskScoreEntity } from '../../../../../common/search_strategy';
import { EntitiesList } from '../entities_list';
import { useEntityStoreStatus } from '../hooks/use_entity_store';
import { EntityAnalyticsRiskScores } from '../../entity_analytics_risk_score';
@ -27,6 +27,7 @@ import { EnablementPanel } from './dashboard_enablement_panel';
const EntityStoreDashboardPanelsComponent = () => {
const riskEngineStatus = useRiskEngineStatus();
const storeStatusQuery = useEntityStoreStatus({});
const entityTypes = useStoreEntityTypes();
const callouts = (storeStatusQuery.data?.engines ?? [])
.filter((engine) => engine.status === 'error')
@ -73,12 +74,11 @@ const EntityStoreDashboardPanelsComponent = () => {
{riskEngineStatus.data?.risk_engine_status !== RiskEngineStatusEnum.NOT_INSTALLED && (
<>
<EuiFlexItem>
<EntityAnalyticsRiskScores riskEntity={RiskScoreEntity.user} />
</EuiFlexItem>
<EuiFlexItem>
<EntityAnalyticsRiskScores riskEntity={RiskScoreEntity.host} />
</EuiFlexItem>
{entityTypes.map((entityType) => (
<EuiFlexItem key={entityType}>
<EntityAnalyticsRiskScores riskEntity={entityType} />
</EuiFlexItem>
))}
</>
)}
{storeStatusQuery.data?.status !== 'not_installed' &&

View file

@ -178,7 +178,7 @@ const getResourcePath = (id: string, resource: EngineComponentResource) => {
}
if (resource === EngineComponentResourceEnum.transform) {
return `data/transform/enrich_policies?_a=(transform:(queryText:'${id}'))`;
return `data/transform?_a=(transform:(queryText:'${id}'))`;
}
return null;
};

View file

@ -11,12 +11,11 @@ import { noop } from 'lodash/fp';
import { i18n } from '@kbn/i18n';
import { useErrorToast } from '../../../common/hooks/use_error_toast';
import type { CriticalityLevels } from '../../../../common/constants';
import type { RiskSeverity } from '../../../../common/search_strategy';
import { type RiskSeverity } from '../../../../common/search_strategy';
import { useQueryInspector } from '../../../common/components/page/manage_query';
import { Direction } from '../../../../common/search_strategy/common';
import { useGlobalTime } from '../../../common/containers/use_global_time';
import { useQueryToggle } from '../../../common/containers/query_toggle';
import { EntityType } from '../../../../common/api/entity_analytics/entity_store/common.gen';
import type { Criteria } from '../../../explore/components/paginated_table';
import { PaginatedTable } from '../../../explore/components/paginated_table';
import { SeverityFilter } from '../severity/severity_filter';
@ -27,6 +26,7 @@ import { useEntitiesListQuery } from './hooks/use_entities_list_query';
import { ENTITIES_LIST_TABLE_ID, rowItems } from './constants';
import { useEntitiesListColumns } from './hooks/use_entities_list_columns';
import type { EntitySourceTag } from './types';
import { useStoreEntityTypes } from '../../hooks/use_enabled_entity_types';
export const EntitiesList: React.FC = () => {
const { deleteQuery, setQuery, isInitializing, from, to } = useGlobalTime();
@ -37,7 +37,7 @@ export const EntitiesList: React.FC = () => {
field: '@timestamp',
direction: Direction.desc,
});
const entityTypes = useStoreEntityTypes();
const [selectedSeverities, setSelectedSeverities] = useState<RiskSeverity[]>([]);
const [selectedCriticalities, setSelectedCriticalities] = useState<CriticalityLevels[]>([]);
const [selectedSources, setSelectedSources] = useState<EntitySourceTag[]>([]);
@ -69,7 +69,7 @@ export const EntitiesList: React.FC = () => {
const searchParams = useMemo(
() => ({
entitiesTypes: [EntityType.Enum.user, EntityType.Enum.host],
entityTypes,
page: activePage + 1,
perPage: limit,
sortField: sorting.field,
@ -81,7 +81,7 @@ export const EntitiesList: React.FC = () => {
},
}),
}),
[activePage, limit, querySkip, sorting, filter]
[entityTypes, activePage, limit, sorting.field, sorting.direction, querySkip, filter]
);
const { data, isLoading, isRefetching, refetch, error } = useEntitiesListQuery(searchParams);

View file

@ -5,17 +5,19 @@
* 2.0.
*/
import { isUserEntity, sourceFieldToText } from './helpers';
import type {
Entity,
UserEntity,
} from '../../../../common/api/entity_analytics/entity_store/entities/common.gen';
import { getEntityType, sourceFieldToText } from './helpers';
import { render } from '@testing-library/react';
import { TestProviders } from '@kbn/timelines-plugin/public/mock';
import type {
Entity,
HostEntity,
ServiceEntity,
UserEntity,
} from '../../../../common/api/entity_analytics';
describe('helpers', () => {
describe('isUserEntity', () => {
it('should return true if the record is a UserEntity', () => {
describe('getEntityType', () => {
it('should return "user" if the record is a UserEntity', () => {
const userEntity: UserEntity = {
'@timestamp': '2021-08-02T14:00:00.000Z',
user: {
@ -27,11 +29,11 @@ describe('helpers', () => {
},
};
expect(isUserEntity(userEntity)).toBe(true);
expect(getEntityType(userEntity)).toBe('user');
});
it('should return false if the record is not a UserEntity', () => {
const nonUserEntity: Entity = {
it('should return "host" if the record is a HostEntity', () => {
const hostEntity: HostEntity = {
'@timestamp': '2021-08-02T14:00:00.000Z',
host: {
name: 'test_host',
@ -42,7 +44,35 @@ describe('helpers', () => {
},
};
expect(isUserEntity(nonUserEntity)).toBe(false);
expect(getEntityType(hostEntity)).toBe('host');
});
it('should return "service" if the record is a ServiceEntity', () => {
const serviceEntity: ServiceEntity = {
'@timestamp': '2021-08-02T14:00:00.000Z',
service: {
name: 'test_service',
},
entity: {
name: 'test_service',
source: 'logs-test',
},
};
expect(getEntityType(serviceEntity)).toBe('service');
});
it('should throw an error if the record does not match any entity type', () => {
const unknownEntity = {
'@timestamp': '2021-08-02T14:00:00.000Z',
entity: {
name: 'unknown_entity',
source: 'logs-test',
},
} as unknown as Entity;
expect(() => getEntityType(unknownEntity)).toThrow(
'Unexpected entity: {"@timestamp":"2021-08-02T14:00:00.000Z","entity":{"name":"unknown_entity","source":"logs-test"}}'
);
});
});

View file

@ -6,17 +6,37 @@
*/
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import type { IconType } from '@elastic/eui';
import { get } from 'lodash/fp';
import { getAllEntityTypes } from '../../../../common/entity_analytics/utils';
import { EntityType } from '../../../../common/entity_analytics/types';
import {
ASSET_CRITICALITY_INDEX_PATTERN,
RISK_SCORE_INDEX_PATTERN,
} from '../../../../common/constants';
import type {
Entity,
UserEntity,
} from '../../../../common/api/entity_analytics/entity_store/entities/common.gen';
import type { Entity } from '../../../../common/api/entity_analytics/entity_store/entities/common.gen';
export const isUserEntity = (record: Entity): record is UserEntity =>
!!(record as UserEntity)?.user;
export const getEntityType = (record: Entity): EntityType => {
const allEntityTypes = getAllEntityTypes();
const entityType = allEntityTypes.find((type) => {
return get(type, record) !== undefined;
});
if (!entityType) {
throw new Error(`Unexpected entity: ${JSON.stringify(record)}`);
}
return entityType;
};
export const EntityIconByType: Record<EntityType, IconType> = {
[EntityType.user]: 'user',
[EntityType.host]: 'storage',
[EntityType.service]: 'gear',
[EntityType.universal]: 'globe', // random value since we don't support universal entity type
};
export const sourceFieldToText = (source: string) => {
if (source.match(`^${RISK_SCORE_INDEX_PATTERN}`)) {

View file

@ -10,8 +10,15 @@ import { EuiButtonIcon, EuiIcon, useEuiTheme } from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { UserPanelKey } from '../../../../flyout/entity_details/user_right';
import { HostPanelKey } from '../../../../flyout/entity_details/host_right';
import { get } from 'lodash/fp';
import {
EntityTypeToLevelField,
EntityTypeToScoreField,
} from '../../../../../common/search_strategy';
import {
EntityPanelKeyByType,
EntityPanelParamByType,
} from '../../../../flyout/entity_details/shared/constants';
import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date';
import { RiskScoreLevel } from '../../severity/common';
import { getEmptyTagValue } from '../../../../common/components/empty_value';
@ -19,7 +26,7 @@ import type { Columns } from '../../../../explore/components/paginated_table';
import type { Entity } from '../../../../../common/api/entity_analytics/entity_store/entities/common.gen';
import { type CriticalityLevels } from '../../../../../common/constants';
import { ENTITIES_LIST_TABLE_ID } from '../constants';
import { isUserEntity, sourceFieldToText } from '../helpers';
import { EntityIconByType, getEntityType, sourceFieldToText } from '../helpers';
import { CRITICALITY_LEVEL_TITLE } from '../../asset_criticality/translations';
import { formatRiskScore } from '../../../common';
@ -47,19 +54,25 @@ export const useEntitiesListColumns = (): EntitiesListColumns => {
),
render: (record: Entity) => {
const entityType = getEntityType(record);
const value = record.entity.name;
const onClick = () => {
const id = isUserEntity(record) ? UserPanelKey : HostPanelKey;
const params = {
[isUserEntity(record) ? 'userName' : 'hostName']: value,
contextID: ENTITIES_LIST_TABLE_ID,
scopeId: ENTITIES_LIST_TABLE_ID,
};
const id = EntityPanelKeyByType[entityType];
openRightPanel({ id, params });
if (id) {
openRightPanel({
id,
params: {
[EntityPanelParamByType[entityType] ?? '']: value,
contextID: ENTITIES_LIST_TABLE_ID,
scopeId: ENTITIES_LIST_TABLE_ID,
},
});
}
};
if (!value) {
if (!value || !EntityPanelKeyByType[entityType]) {
return null;
}
@ -90,9 +103,10 @@ export const useEntitiesListColumns = (): EntitiesListColumns => {
sortable: true,
truncateText: { lines: 2 },
render: (_: string, record: Entity) => {
const entityType = getEntityType(record);
return (
<span>
{isUserEntity(record) ? <EuiIcon type="user" /> : <EuiIcon type="storage" />}
<EuiIcon type={EntityIconByType[entityType]} />
<span css={{ paddingLeft: euiTheme.size.s }}>{record.entity.name}</span>
</span>
);
@ -143,9 +157,8 @@ export const useEntitiesListColumns = (): EntitiesListColumns => {
),
width: '10%',
render: (entity: Entity) => {
const riskScore = isUserEntity(entity)
? entity.user?.risk?.calculated_score_norm
: entity.host?.risk?.calculated_score_norm;
const entityType = getEntityType(entity);
const riskScore = get(EntityTypeToScoreField[entityType], entity);
if (riskScore != null) {
return (
@ -166,9 +179,8 @@ export const useEntitiesListColumns = (): EntitiesListColumns => {
),
width: '10%',
render: (entity: Entity) => {
const riskLevel = isUserEntity(entity)
? entity.user?.risk?.calculated_level
: entity.host?.risk?.calculated_level;
const entityType = getEntityType(entity);
const riskLevel = get(EntityTypeToLevelField[entityType], entity);
if (riskLevel != null) {
return <RiskScoreLevel severity={riskLevel} />;

View file

@ -12,7 +12,12 @@ import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/type
import { CriticalityLevels } from '../../../../../common/constants';
import { RiskSeverity } from '../../../../../common/search_strategy';
import { EntitySourceTag } from '../types';
import { mockGlobalState } from '../../../../common/mock';
const mockedExperimentalFeatures = mockGlobalState.app.enableExperimental;
jest.mock('../../../../common/hooks/use_experimental_features', () => ({
useEnableExperimental: () => ({ ...mockedExperimentalFeatures, serviceEntityStoreEnabled: true }),
}));
jest.mock('../../../../common/hooks/use_global_filter_query');
describe('useEntitiesListFilters', () => {
@ -47,10 +52,21 @@ describe('useEntitiesListFilters', () => {
{
bool: {
should: [
{ term: { 'host.risk.calculated_level': RiskSeverity.Low } },
{ term: { 'user.risk.calculated_level': RiskSeverity.Low } },
{ term: { 'host.risk.calculated_level': RiskSeverity.High } },
{ term: { 'user.risk.calculated_level': RiskSeverity.High } },
{
terms: {
'user.risk.calculated_level': ['Low', 'High'],
},
},
{
terms: {
'host.risk.calculated_level': ['Low', 'High'],
},
},
{
terms: {
'service.risk.calculated_level': ['Low', 'High'],
},
},
],
},
},
@ -173,8 +189,21 @@ describe('useEntitiesListFilters', () => {
{
bool: {
should: [
{ term: { 'host.risk.calculated_level': RiskSeverity.Low } },
{ term: { 'user.risk.calculated_level': RiskSeverity.Low } },
{
terms: {
'user.risk.calculated_level': ['Low'],
},
},
{
terms: {
'host.risk.calculated_level': ['Low'],
},
},
{
terms: {
'service.risk.calculated_level': ['Low'],
},
},
],
},
},

View file

@ -12,9 +12,10 @@ import {
RISK_SCORE_INDEX_PATTERN,
type CriticalityLevels,
} from '../../../../../common/constants';
import type { RiskSeverity } from '../../../../../common/search_strategy';
import { EntityTypeToLevelField, type RiskSeverity } from '../../../../../common/search_strategy';
import { useGlobalFilterQuery } from '../../../../common/hooks/use_global_filter_query';
import { EntitySourceTag } from '../types';
import { useStoreEntityTypes } from '../../../hooks/use_enabled_entity_types';
interface UseEntitiesListFiltersParams {
selectedSeverities: RiskSeverity[];
@ -28,6 +29,7 @@ export const useEntitiesListFilters = ({
selectedSources,
}: UseEntitiesListFiltersParams) => {
const { filterQuery: globalQuery } = useGlobalFilterQuery();
const enabledEntityTypes = useStoreEntityTypes();
return useMemo(() => {
const criticalityFilter: QueryDslQueryContainer[] = selectedCriticalities.length
@ -58,18 +60,11 @@ export const useEntitiesListFilters = ({
? [
{
bool: {
should: selectedSeverities.flatMap((value) => [
{
term: {
'host.risk.calculated_level': value,
},
should: enabledEntityTypes.map((type) => ({
terms: {
[EntityTypeToLevelField[type]]: selectedSeverities,
},
{
term: {
'user.risk.calculated_level': value,
},
},
]),
})),
},
},
]
@ -84,7 +79,7 @@ export const useEntitiesListFilters = ({
filterList.push(globalQuery);
}
return filterList;
}, [globalQuery, selectedCriticalities, selectedSeverities, selectedSources]);
}, [enabledEntityTypes, globalQuery, selectedCriticalities, selectedSeverities, selectedSources]);
};
const getSourceTagFilterQuery = (tag: EntitySourceTag): QueryDslQueryContainer => {

View file

@ -27,7 +27,7 @@ describe('useEntitiesListQuery', () => {
});
it('should call fetchEntitiesList with correct parameters', async () => {
const searchParams = { entitiesTypes: [], page: 7 };
const searchParams = { entityTypes: [], page: 7 };
fetchEntitiesListMock.mockResolvedValueOnce({ data: 'test data' });
@ -42,7 +42,7 @@ describe('useEntitiesListQuery', () => {
});
it('should not call fetchEntitiesList if skip is true', async () => {
const searchParams = { entitiesTypes: [], page: 7 };
const searchParams = { entityTypes: [], page: 7 };
const { result } = renderHook(() => useEntitiesListQuery({ ...searchParams, skip: true }), {
wrapper: TestWrapper,

View file

@ -17,8 +17,8 @@ import { HostDetailsLink } from '../../../common/components/links';
import type { HostRiskScoreColumns } from '.';
import * as i18n from './translations';
import { HostsTableType } from '../../../explore/hosts/store/model';
import type { Maybe, RiskSeverity } from '../../../../common/search_strategy';
import { RiskScoreFields, RiskScoreEntity } from '../../../../common/search_strategy';
import { RiskScoreFields, type Maybe, type RiskSeverity } from '../../../../common/search_strategy';
import { EntityType } from '../../../../common/entity_analytics/types';
import { RiskScoreLevel } from '../severity/common';
import { ENTITY_RISK_LEVEL } from '../risk_score/translations';
import { CELL_ACTIONS_TELEMETRY } from '../risk_score/constants';
@ -92,7 +92,7 @@ export const getHostRiskScoreColumns = ({
},
{
field: RiskScoreFields.hostRisk,
name: ENTITY_RISK_LEVEL(RiskScoreEntity.host),
name: ENTITY_RISK_LEVEL(EntityType.host),
truncateText: false,
mobileOptions: { show: true },
sortable: true,

View file

@ -26,7 +26,7 @@ import type {
RiskSeverity,
RiskScoreFields,
} from '../../../../common/search_strategy';
import { RiskScoreEntity } from '../../../../common/search_strategy';
import { EntityType } from '../../../../common/entity_analytics/types';
import type { State } from '../../../common/store';
import * as i18n from '../../../explore/hosts/components/hosts_table/translations';
import * as i18nHosts from './translations';
@ -182,14 +182,14 @@ const HostRiskScoreTableComponent: React.FC<HostRiskScoreTableProps> = ({
headerFilters={
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<RiskInformationButtonEmpty riskEntity={RiskScoreEntity.host} />
<RiskInformationButtonEmpty riskEntity={EntityType.host} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFilterGroup>
<SeverityFilter
selectedItems={severitySelectionRedux}
onSelect={onSelect}
riskEntity={RiskScoreEntity.host}
riskEntity={EntityType.host}
/>
</EuiFilterGroup>
</EuiFlexItem>

View file

@ -10,7 +10,7 @@ import { render } from '@testing-library/react';
import { TestProviders } from '../../../common/mock';
import { useQueryToggle } from '../../../common/containers/query_toggle';
import { RiskDetailsTabBody } from '.';
import { RiskScoreEntity } from '../../../../common/search_strategy';
import { EntityType } from '../../../../common/search_strategy';
import { HostsType } from '../../../explore/hosts/store/model';
import { UsersType } from '../../../explore/users/store/model';
import { useRiskScore } from '../../api/hooks/use_risk_score';
@ -19,91 +19,88 @@ jest.mock('../../api/hooks/use_risk_score');
jest.mock('../../../common/containers/query_toggle');
jest.mock('../../../common/lib/kibana');
describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
'Risk Tab Body entityType: %s',
(riskEntity) => {
const defaultProps = {
entityName: 'testEntity',
indexNames: [],
setQuery: jest.fn(),
skip: false,
startDate: '2019-06-25T04:31:59.345Z',
endDate: '2019-06-25T06:31:59.345Z',
type: riskEntity === RiskScoreEntity.host ? HostsType.page : UsersType.page,
describe.each([EntityType.host, EntityType.user])('Risk Tab Body entityType: %s', (riskEntity) => {
const defaultProps = {
entityName: 'testEntity',
indexNames: [],
setQuery: jest.fn(),
skip: false,
startDate: '2019-06-25T04:31:59.345Z',
endDate: '2019-06-25T06:31:59.345Z',
type: riskEntity === EntityType.host ? HostsType.page : UsersType.page,
riskEntity,
};
const mockUseRiskScore = useRiskScore as jest.Mock;
const mockUseQueryToggle = useQueryToggle as jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
mockUseRiskScore.mockReturnValue({
loading: false,
inspect: {
dsl: [],
response: [],
},
isInspected: false,
totalCount: 0,
refetch: jest.fn(),
hasEngineBeenInstalled: true,
});
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() });
});
it('calls with correct arguments for each entity', () => {
render(
<TestProviders>
<RiskDetailsTabBody {...defaultProps} />
</TestProviders>
);
expect(mockUseRiskScore).toBeCalledWith({
filterQuery: {
terms: {
[`${riskEntity}.name`]: ['testEntity'],
},
},
onlyLatest: false,
riskEntity,
};
const mockUseRiskScore = useRiskScore as jest.Mock;
const mockUseQueryToggle = useQueryToggle as jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
mockUseRiskScore.mockReturnValue({
loading: false,
inspect: {
dsl: [],
response: [],
},
isInspected: false,
totalCount: 0,
refetch: jest.fn(),
isModuleEnabled: true,
});
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() });
skip: false,
timerange: {
from: '2019-06-25T04:31:59.345Z',
to: '2019-06-25T06:31:59.345Z',
},
});
});
it('calls with correct arguments for each entity', () => {
render(
<TestProviders>
<RiskDetailsTabBody {...defaultProps} />
</TestProviders>
);
expect(mockUseRiskScore).toBeCalledWith({
filterQuery: {
terms: {
[`${riskEntity}.name`]: ['testEntity'],
},
},
onlyLatest: false,
riskEntity,
skip: false,
timerange: {
from: '2019-06-25T04:31:59.345Z',
to: '2019-06-25T06:31:59.345Z',
},
});
});
it("doesn't skip when both toggleStatus are true", () => {
render(
<TestProviders>
<RiskDetailsTabBody {...defaultProps} />
</TestProviders>
);
expect(mockUseRiskScore.mock.calls[0][0].skip).toEqual(false);
});
it("doesn't skip when both toggleStatus are true", () => {
render(
<TestProviders>
<RiskDetailsTabBody {...defaultProps} />
</TestProviders>
);
expect(mockUseRiskScore.mock.calls[0][0].skip).toEqual(false);
});
it("doesn't skip when at least one toggleStatus is true", () => {
mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: true, setToggleStatus: jest.fn() });
mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: false, setToggleStatus: jest.fn() });
it("doesn't skip when at least one toggleStatus is true", () => {
mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: true, setToggleStatus: jest.fn() });
mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: false, setToggleStatus: jest.fn() });
render(
<TestProviders>
<RiskDetailsTabBody {...defaultProps} />
</TestProviders>
);
expect(mockUseRiskScore.mock.calls[0][0].skip).toEqual(false);
});
render(
<TestProviders>
<RiskDetailsTabBody {...defaultProps} />
</TestProviders>
);
expect(mockUseRiskScore.mock.calls[0][0].skip).toEqual(false);
});
it('does skip when both toggleStatus are false', () => {
mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() });
it('does skip when both toggleStatus are false', () => {
mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() });
render(
<TestProviders>
<RiskDetailsTabBody {...defaultProps} />
</TestProviders>
);
expect(mockUseRiskScore.mock.calls[0][0].skip).toEqual(true);
});
}
);
render(
<TestProviders>
<RiskDetailsTabBody {...defaultProps} />
</TestProviders>
);
expect(mockUseRiskScore.mock.calls[0][0].skip).toEqual(true);
});
});

View file

@ -25,7 +25,7 @@ import { TopRiskScoreContributors } from '../top_risk_score_contributors';
import { TopRiskScoreContributorsAlerts } from '../top_risk_score_contributors_alerts';
import { useQueryToggle } from '../../../common/containers/query_toggle';
import type { HostRiskScore, UserRiskScore } from '../../../../common/search_strategy';
import { buildEntityNameFilter, RiskScoreEntity } from '../../../../common/search_strategy';
import { EntityType, buildEntityNameFilter } from '../../../../common/search_strategy';
import type { UsersComponentsQueryProps } from '../../../explore/users/pages/navigation/types';
import type { HostsComponentsQueryProps } from '../../../explore/hosts/pages/navigation/types';
import { useDashboardHref } from '../../../common/hooks/use_dashboard_href';
@ -43,25 +43,25 @@ const StyledEuiFlexGroup = styled(EuiFlexGroup)`
type ComponentsQueryProps = HostsComponentsQueryProps | UsersComponentsQueryProps;
const getDashboardTitle = (riskEntity: RiskScoreEntity) =>
riskEntity === RiskScoreEntity.host ? RISKY_HOSTS_DASHBOARD_TITLE : RISKY_USERS_DASHBOARD_TITLE;
const getDashboardTitle = (riskEntity: EntityType) =>
riskEntity === EntityType.host ? RISKY_HOSTS_DASHBOARD_TITLE : RISKY_USERS_DASHBOARD_TITLE;
const RiskDetailsTabBodyComponent: React.FC<
Pick<ComponentsQueryProps, 'startDate' | 'endDate' | 'setQuery' | 'deleteQuery'> & {
entityName: string;
riskEntity: RiskScoreEntity;
riskEntity: EntityType;
}
> = ({ entityName, startDate, endDate, setQuery, deleteQuery, riskEntity }) => {
const queryId = useMemo(
() =>
riskEntity === RiskScoreEntity.host
riskEntity === EntityType.host
? HostRiskScoreQueryId.HOST_DETAILS_RISK_SCORE
: UserRiskScoreQueryId.USER_DETAILS_RISK_SCORE,
[riskEntity]
);
const severitySelectionRedux = useDeepEqualSelector((state: State) =>
riskEntity === RiskScoreEntity.host
riskEntity === EntityType.host
? hostsSelectors.hostRiskScoreSeverityFilterSelector()(state, hostsModel.HostsType.details)
: usersSelectors.userRiskScoreSeverityFilterSelector()(state)
);
@ -82,7 +82,7 @@ const RiskDetailsTabBodyComponent: React.FC<
useQueryToggle(`${queryId} contributors`);
const filterQuery = useMemo(
() => (entityName ? buildEntityNameFilter([entityName], riskEntity) : {}),
() => (entityName ? buildEntityNameFilter(riskEntity, [entityName]) : {}),
[entityName, riskEntity]
);
@ -99,7 +99,7 @@ const RiskDetailsTabBodyComponent: React.FC<
const rules = useMemo(() => {
const lastRiskItem = data && data.length > 0 ? data[data.length - 1] : null;
if (lastRiskItem) {
return riskEntity === RiskScoreEntity.host
return riskEntity === EntityType.host
? (lastRiskItem as HostRiskScore).host.risk.rule_risks
: (lastRiskItem as UserRiskScore).user.risk.rule_risks;
}
@ -187,10 +187,8 @@ const RiskDetailsTabBodyComponent: React.FC<
<EuiFlexItem grow={2}>
<RiskScoreOverTime
from={startDate}
loading={loading}
queryId={queryId}
riskEntity={riskEntity}
riskScore={data}
title={i18n.RISK_SCORE_OVER_TIME(riskEntity)}
to={endDate}
toggleQuery={toggleOverTimeQuery}

View file

@ -6,10 +6,10 @@
*/
import { i18n } from '@kbn/i18n';
import type { RiskScoreEntity } from '../../../../common/search_strategy';
import type { EntityType } from '../../../../common/entity_analytics/types';
import { getRiskEntityTranslation } from '../risk_score/translations';
export const RISK_SCORE_OVER_TIME = (riskEntity: RiskScoreEntity) =>
export const RISK_SCORE_OVER_TIME = (riskEntity: EntityType) =>
i18n.translate('xpack.securitySolution.riskTabBody.scoreOverTimeTitle', {
defaultMessage: '{riskEntity} risk score over time',
values: {

View file

@ -9,9 +9,9 @@ import { render, fireEvent, within } from '@testing-library/react';
import React from 'react';
import { RiskInformationButtonEmpty } from '.';
import { TestProviders } from '../../../common/mock';
import { RiskScoreEntity } from '../../../../common/search_strategy';
import { EntityType } from '../../../../common/entity_analytics/types';
describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
describe.each([EntityType.host, EntityType.user])(
'Risk Information entityType: %s',
(riskEntity) => {
describe('RiskInformationButtonEmpty', () => {

View file

@ -31,15 +31,15 @@ import { css } from '@emotion/react';
import * as i18n from './translations';
import { useOnOpenCloseHandler } from '../../../helper_hooks';
import { RiskScoreLevel } from '../severity/common';
import type { RiskScoreEntity } from '../../../../common/search_strategy';
import type { EntityType } from '../../../../common/entity_analytics/types';
import { RiskSeverity } from '../../../../common/search_strategy';
import {
CriticalityLevels,
CriticalityModifiers,
} from '../../../../common/entity_analytics/asset_criticality';
import { EntityAnalyticsLearnMoreLink } from '../risk_score_onboarding/entity_analytics_doc_link';
import { BETA } from '../risk_score_onboarding/translations';
import { AssetCriticalityBadge } from '../asset_criticality';
import { BETA } from '../../../common/translations';
const SpacedOrderedList = styled.ol`
li {
@ -105,7 +105,7 @@ const getCriticalityLevelTableColumns = (): Array<
export const HOST_RISK_INFO_BUTTON_CLASS = 'HostRiskInformation__button';
export const USER_RISK_INFO_BUTTON_CLASS = 'UserRiskInformation__button';
export const RiskInformationButtonEmpty = ({ riskEntity }: { riskEntity: RiskScoreEntity }) => {
export const RiskInformationButtonEmpty = ({ riskEntity }: { riskEntity: EntityType }) => {
const [isFlyoutVisible, handleOnOpen, handleOnClose] = useOnOpenCloseHandler();
return (

View file

@ -73,3 +73,7 @@ export const INFO_BUTTON_TEXT = i18n.translate(
defaultMessage: 'How is risk score calculated?',
}
);
export const BETA = i18n.translate('xpack.securitySolution.riskScore.technicalPreviewLabel', {
defaultMessage: 'Beta',
});

View file

@ -6,7 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
import { RiskScoreEntity } from '../../../../common/search_strategy';
import { EntityType } from '../../../../common/entity_analytics/types';
export const HOST = i18n.translate('xpack.securitySolution.riskScore.overview.hostTitle', {
defaultMessage: 'Host',
@ -24,6 +24,14 @@ export const USERS = i18n.translate('xpack.securitySolution.riskScore.overview.u
defaultMessage: 'Users',
});
export const SERVICE = i18n.translate('xpack.securitySolution.riskScore.overview.serviceTitle', {
defaultMessage: 'Service',
});
export const SERVICES = i18n.translate('xpack.securitySolution.riskScore.overview.services', {
defaultMessage: 'Services',
});
export const ENTITY = i18n.translate('xpack.securitySolution.riskScore.overview.entityTitle', {
defaultMessage: 'Entity',
});
@ -32,7 +40,7 @@ export const ENTITIES = i18n.translate('xpack.securitySolution.riskScore.overvie
defaultMessage: 'Entities',
});
export const RISK_SCORE_TITLE = (riskEntity: RiskScoreEntity) =>
export const RISK_SCORE_TITLE = (riskEntity: EntityType) =>
i18n.translate('xpack.securitySolution.riskScore.overview.riskScoreTitle', {
defaultMessage: '{riskEntity} Risk Score',
values: {
@ -47,7 +55,7 @@ export const RISK_SCORING_TITLE = i18n.translate(
}
);
export const ENTITY_RISK_LEVEL = (riskEntity?: RiskScoreEntity) =>
export const ENTITY_RISK_LEVEL = (riskEntity?: EntityType) =>
riskEntity
? i18n.translate('xpack.securitySolution.entityAnalytics.riskDashboard.riskLevelTitle', {
defaultMessage: '{riskEntity} risk level',
@ -63,7 +71,7 @@ export const ENTITY_RISK_LEVEL = (riskEntity?: RiskScoreEntity) =>
);
export const getRiskEntityTranslation = (
riskEntity?: RiskScoreEntity,
riskEntity?: EntityType,
lowercase = false,
plural = false
) => {
@ -72,14 +80,16 @@ export const getRiskEntityTranslation = (
};
export const getRiskEntityTranslationText = (
riskEntity: RiskScoreEntity | undefined,
riskEntity: EntityType | undefined,
plural: boolean
) => {
switch (riskEntity) {
case RiskScoreEntity.host:
case EntityType.host:
return plural ? HOSTS : HOST;
case RiskScoreEntity.user:
case EntityType.user:
return plural ? USERS : USER;
case EntityType.service:
return plural ? SERVICES : SERVICE;
default:
return plural ? ENTITIES : ENTITY;
}

View file

@ -6,9 +6,8 @@
*/
import { render, screen } from '@testing-library/react';
import React from 'react';
import { RiskScoreEntity } from '../../../../common/search_strategy';
import { EntityType } from '../../../../common/search_strategy';
import { TestProviders } from '../../../common/mock';
import { RiskScoreEnableButton } from './risk_score_enable_button';
describe('RiskScoreEnableButton', () => {
@ -20,7 +19,7 @@ describe('RiskScoreEnableButton', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe.each([[RiskScoreEntity.host], [RiskScoreEntity.user]])('%s', (riskScoreEntity) => {
describe.each([[EntityType.host], [EntityType.user]])('%s', (riskScoreEntity) => {
it('Renders expected children', () => {
render(
<TestProviders>

View file

@ -9,7 +9,7 @@ import { EuiButton } from '@elastic/eui';
import React, { useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import type { RiskScoreEntity } from '../../../../common/search_strategy';
import type { EntityType } from '../../../../common/search_strategy';
import { useSpaceId } from '../../../common/hooks/use_space_id';
import { useKibana } from '../../../common/lib/kibana';
import type { inputsModel } from '../../../common/store';
@ -27,7 +27,7 @@ const RiskScoreEnableButtonComponent = ({
timerange,
}: {
refetch: inputsModel.Refetch;
riskScoreEntity: RiskScoreEntity;
riskScoreEntity: EntityType;
disabled?: boolean;
timerange: {
from: string;

View file

@ -7,29 +7,26 @@
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { RiskScoreEntity } from '../../../../common/search_strategy';
import { capitalize } from 'lodash/fp';
import type { EntityType } from '../../../../common/search_strategy';
const RiskScoreHeaderTitleComponent = ({
riskScoreEntity,
title,
}: {
riskScoreEntity: RiskScoreEntity;
riskScoreEntity: EntityType;
title?: string;
}) => (
<>
{title ??
(riskScoreEntity === RiskScoreEntity.user ? (
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.usersRiskDashboard.title"
defaultMessage="User Risk Scores"
/>
) : (
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.hostsRiskDashboard.title"
defaultMessage="Host Risk Scores"
/>
))}
{title ?? (
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.usersRiskDashboard.title"
defaultMessage="{entityType} Risk Scores"
values={{
entityType: capitalize(riskScoreEntity),
}}
/>
)}
</>
);

View file

@ -6,44 +6,59 @@
*/
import { EuiEmptyPrompt, EuiPanel, EuiToolTip } from '@elastic/eui';
import React, { useMemo } from 'react';
import { RiskScoreEntity } from '../../../../common/search_strategy';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { HeaderSection } from '../../../common/components/header_section';
import * as i18n from './translations';
import { RiskScoreHeaderTitle } from './risk_score_header_title';
import { RiskScoreRestartButton } from './risk_score_restart_button';
import type { inputsModel } from '../../../common/store';
import { useIsNewRiskScoreModuleInstalled } from '../../api/hooks/use_risk_engine_status';
import type { EntityType } from '../../../../common/search_strategy';
export const RESTART_TOOLTIP = i18n.translate(
'xpack.securitySolution.riskScore.usersDashboardRestartTooltip',
{
defaultMessage:
'The risk score calculation might take a while to run. However, by pressing restart, you can force it to run immediately.',
}
);
const RiskScoresNoDataDetectedComponent = ({
entityType,
refetch,
}: {
entityType: RiskScoreEntity;
entityType: EntityType;
refetch: inputsModel.Refetch;
}) => {
const isNewRiskScoreModuleInstalled = useIsNewRiskScoreModuleInstalled();
const translations = useMemo(
() => ({
title:
entityType === RiskScoreEntity.user ? i18n.USER_WARNING_TITLE : i18n.HOST_WARNING_TITLE,
body: entityType === RiskScoreEntity.user ? i18n.USER_WARNING_BODY : i18n.HOST_WARNING_BODY,
}),
[entityType]
);
return (
<EuiPanel data-test-subj={`${entityType}-risk-score-no-data-detected`} hasBorder>
<HeaderSection title={<RiskScoreHeaderTitle riskScoreEntity={entityType} />} titleSize="s" />
<EuiEmptyPrompt
title={<h2>{translations.title}</h2>}
body={translations.body}
title={
<h2>
<FormattedMessage
id="xpack.securitySolution.riskScore.entityDashboardWarningPanelTitle"
defaultMessage="No {entityType} risk score data available to display"
values={{
entityType,
}}
/>
</h2>
}
body={
<FormattedMessage
id="xpack.securitySolution.riskScore.entityDashboardWarningPanelBody"
defaultMessage={`We havent found any {entityType} risk score data. Check if you have any global filters in the global KQL search bar. If you have just enabled the {entityType} risk module, the risk engine might need an hour to generate {entityType} risk score data and display in this panel.`}
values={{ entityType }}
/>
}
actions={
<>
{!isNewRiskScoreModuleInstalled && (
<EuiToolTip content={i18n.RESTART_TOOLTIP}>
<EuiToolTip content={RESTART_TOOLTIP}>
<RiskScoreRestartButton refetch={refetch} riskScoreEntity={entityType} />
</EuiToolTip>
)}

View file

@ -7,7 +7,7 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent, { type UserEvent } from '@testing-library/user-event';
import React from 'react';
import { RiskScoreEntity } from '../../../../common/search_strategy';
import { EntityType } from '../../../../common/search_strategy';
import { TestProviders } from '../../../common/mock';
import { RiskScoreRestartButton } from './risk_score_restart_button';
@ -49,7 +49,7 @@ describe('RiskScoreRestartButton', () => {
jest.clearAllMocks();
jest.useRealTimers();
});
describe.each([[RiskScoreEntity.host], [RiskScoreEntity.user]])('%s', (riskScoreEntity) => {
describe.each([[EntityType.host], [EntityType.user]])('%s', (riskScoreEntity) => {
it('Renders expected children', () => {
render(
<TestProviders>

View file

@ -9,7 +9,7 @@ import { EuiButton } from '@elastic/eui';
import React, { useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import type { RiskScoreEntity } from '../../../../common/search_strategy';
import type { EntityType } from '../../../../common/search_strategy';
import { useSpaceId } from '../../../common/hooks/use_space_id';
import { useKibana } from '../../../common/lib/kibana';
import type { inputsModel } from '../../../common/store';
@ -22,7 +22,7 @@ const RiskScoreRestartButtonComponent = ({
riskScoreEntity,
}: {
refetch: inputsModel.Refetch;
riskScoreEntity: RiskScoreEntity;
riskScoreEntity: EntityType;
}) => {
const { fetch, isLoading } = useFetch(
REQUEST_NAMES.REFRESH_RISK_SCORE,

View file

@ -1,57 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import type { RiskScoreEntity } from '../../../../common/entity_analytics/risk_engine';
import { getRiskEntityTranslation } from '../risk_score/translations';
export const BETA = i18n.translate('xpack.securitySolution.riskScore.technicalPreviewLabel', {
defaultMessage: 'Beta',
});
export const HOST_WARNING_TITLE = i18n.translate(
'xpack.securitySolution.riskScore.hostsDashboardWarningPanelTitle',
{
defaultMessage: 'No host risk score data available to display',
}
);
export const USER_WARNING_TITLE = i18n.translate(
'xpack.securitySolution.riskScore.usersDashboardWarningPanelTitle',
{
defaultMessage: 'No user risk score data available to display',
}
);
export const HOST_WARNING_BODY = i18n.translate(
'xpack.securitySolution.riskScore.hostsDashboardWarningPanelBody',
{
defaultMessage: `We havent found any host risk score data. Check if you have any global filters in the global KQL search bar. If you have just enabled the host risk module, the risk engine might need an hour to generate host risk score data and display in this panel.`,
}
);
export const USER_WARNING_BODY = i18n.translate(
'xpack.securitySolution.riskScore.usersDashboardWarningPanelBody',
{
defaultMessage: `We havent found any user risk score data. Check if you have any global filters in the global KQL search bar. If you have just enabled the user risk module, the risk engine might need an hour to generate user risk score data and display in this panel.`,
}
);
export const RESTART_TOOLTIP = i18n.translate(
'xpack.securitySolution.riskScore.usersDashboardRestartTooltip',
{
defaultMessage:
'The risk score calculation might take a while to run. However, by pressing restart, you can force it to run immediately.',
}
);
export const RISK_DATA_TITLE = (riskEntity: RiskScoreEntity) =>
i18n.translate('xpack.securitySolution.alertDetails.overview.hostRiskDataTitle', {
defaultMessage: '{riskEntity} Risk Data',
values: {
riskEntity: getRiskEntityTranslation(riskEntity),
},
});

View file

@ -7,7 +7,7 @@
import { coreMock } from '@kbn/core/public/mocks';
import type { HttpSetup } from '@kbn/core/public';
import { RiskScoreEntity } from '../../../../common/search_strategy';
import { EntityType } from '../../../../common/search_strategy';
import {
getIngestPipelineName,
getLegacyIngestPipelineName,
@ -38,7 +38,7 @@ const mockTimerange = {
to: 'endDate',
};
const mockRefetch = jest.fn();
describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
describe.each([EntityType.host, EntityType.user])(
`installRiskScoreModule - %s`,
(riskScoreEntity) => {
beforeAll(async () => {
@ -74,7 +74,7 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
}
);
describe.each([[RiskScoreEntity.host], [RiskScoreEntity.user]])(
describe.each([[EntityType.host], [EntityType.user]])(
'uninstallRiskScoreModule - %s',
(riskScoreEntity) => {
beforeAll(async () => {
@ -113,7 +113,7 @@ describe.each([[RiskScoreEntity.host], [RiskScoreEntity.user]])(
});
it('Delete legacy stored scripts', () => {
if (riskScoreEntity === RiskScoreEntity.user) {
if (riskScoreEntity === EntityType.user) {
expect((api.deleteStoredScripts as jest.Mock).mock.calls[0][0].ids).toMatchInlineSnapshot(`
Array [
"ml_userriskscore_levels_script",
@ -142,7 +142,7 @@ describe.each([[RiskScoreEntity.host], [RiskScoreEntity.user]])(
}
);
describe.each([[RiskScoreEntity.host], [RiskScoreEntity.user]])(
describe.each([[EntityType.host], [EntityType.user]])(
'Restart Transforms - %s',
(riskScoreEntity) => {
beforeAll(async () => {

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