mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
# 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\n\n\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\n\n\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\n\n\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:
parent
06063bba37
commit
e6b7b5c9e9
216 changed files with 2538 additions and 1771 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 l’exactitude 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 n’avons pas trouvé de données de score de risque de l’hôte. Vérifiez si vous avez des filtres globaux dans la barre de recherche KQL globale. Si vous venez d’activer le module de risque de l’hôte, le moteur de risque peut mettre une heure à générer les données de score de risque de l’hô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 n’avons pas trouvé de données de score de risque de l’utilisateur. Vérifiez si vous avez des filtres globaux dans la barre de recherche KQL globale. Si vous venez d’activer le module de risque de l’utilisateur, le moteur de risque peut mettre une heure à générer les données de score de risque de l’utilisateur 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é",
|
||||
|
|
|
@ -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": "実行されたプロセス",
|
||||
|
|
|
@ -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": "已执行进程",
|
||||
|
|
|
@ -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(),
|
||||
})
|
||||
),
|
||||
]);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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>;
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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),
|
||||
]),
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
};
|
|
@ -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[];
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
export * from './risk_weights';
|
||||
export * from './identifier_types';
|
||||
export * from './range';
|
||||
export * from './risk_levels';
|
||||
export * from './types';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<{
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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` },
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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])
|
||||
),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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]],
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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' }],
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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: (
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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' &&
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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"}}'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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}`)) {
|
||||
|
|
|
@ -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} />;
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
|
|
|
@ -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 haven’t 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>
|
||||
)}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 haven’t 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 haven’t 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),
|
||||
},
|
||||
});
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue