mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[SecuritySolution] Add Service entity type to Entity Analytics (#204437)
## Summary * Refactor types to prevent usages of `host` and `user`. * Use `EntityType` instead. * Use a generic function that receives `EntityType` as a parameter instead of custom user and host functions * Consolidate duplicated entity types * Add service to entity types and update all references on the EntityAnalyticsDashboards page, Risk score page and Entity Store page. * Refactor Risk score APIs to be more generic and accept EntityType and a param * Refactor if statement like `isUserRiskScore` to be more generic and accept `service` * Delete `RiskScoreEntity` in favour of `EntityType`. * Update the branch to support the universal entity ### Not included * Service Flyout ### Images    ### Generic Entity Support We need to support risk score and asset criticality for Generic/Universal entities according to https://github.com/elastic/security-team/issues/10740 > We expect that the below will be supported: > > Entity flyout for service/generic entity > Entity risk scoring for service/generic entity > Asset criticality assignments for service/generic entity This PR already implements that support. However, I have introduced a function per feature that returns the enabled entity types. At the moment, I defined universal/generic entities as unsupported for this PR to preserve the current behaviour. But to allow universal/generic entities, we only need to delete a couple of lines. Risk Score will need extra work because the entity types are hard-coded on some parts of the code. ### How to test it 1 * Start kabana with security solution data * You can use the document generator with `yarn start entity-store` * Enable Entity Store and Risk engine * Test the EA features, and they should work normally 2 * Start kabana with security solution data * You can use the document generator with `yarn start entity-store` * Enable the `serviceEntityStoreEnabled` flag * Enable Entity Store and Risk engine * Test the EA features, and you should see a new type of Entity called 'service' * Service Entity should work with all Entity analytics features 3 * Start kabana with security solution data * You can use the document generator with `yarn start entity-store` * Enable the `assetInventoryStoreEnabled` flag * Enable Entity Store and Risk engine * Test the EA features, and you should not see universal/generic entity except for the entity store status pages ### Checklist Reviewers should verify this PR satisfies this list as well. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) ## Release note Add the "service" type to Security Entity Analytics - Entity Store. It will find services by the `service.name` field, calculate risk score, and allow asset criticality assignment. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
eae7b7dcff
commit
6c31cf73cc
192 changed files with 2326 additions and 1531 deletions
|
@ -9878,7 +9878,7 @@ paths:
|
|||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: entities_types
|
||||
name: entity_types
|
||||
required: true
|
||||
schema:
|
||||
items:
|
||||
|
@ -47321,6 +47321,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
|
||||
|
@ -47545,6 +47546,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
|
||||
|
|
|
@ -12041,7 +12041,7 @@ paths:
|
|||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: entities_types
|
||||
name: entity_types
|
||||
required: true
|
||||
schema:
|
||||
items:
|
||||
|
@ -54196,6 +54196,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
|
||||
|
@ -54420,6 +54421,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
|
||||
|
|
|
@ -38077,8 +38077,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.",
|
||||
|
@ -38141,8 +38139,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}",
|
||||
|
@ -38155,7 +38151,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",
|
||||
|
@ -39820,8 +39815,6 @@
|
|||
"xpack.securitySolution.riskScore.errors.privileges.check": "Vérifier les privilèges",
|
||||
"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.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.kpi.failSearchDescription": "Impossible de lancer une recherche sur le score de risque",
|
||||
"xpack.securitySolution.riskScore.moduleTurnedOff": "Le score de risque des entités a été désactivé",
|
||||
"xpack.securitySolution.riskScore.moduleTurnedOn": "Le score de risque des entités a été activé",
|
||||
|
@ -39842,8 +39835,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",
|
||||
|
@ -39856,8 +39847,6 @@
|
|||
"xpack.securitySolution.riskScore.riskScorePreview.usefulLinks": "Liens utiles",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.users.hide": "Masquer les utilisateurs",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.users.show": "Afficher les utilisateurs",
|
||||
"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é",
|
||||
|
|
|
@ -37935,8 +37935,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ファイルをインポートして、アセット重要度を一括で割り当てます。これにより、データの精度が保証され、手作業の入力エラーが減ります。",
|
||||
|
@ -37999,8 +37997,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}名",
|
||||
|
@ -38013,7 +38009,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": "すべてのインジケーター一致",
|
||||
|
@ -39680,8 +39675,6 @@
|
|||
"xpack.securitySolution.riskScore.errors.privileges.check": "権限を確認",
|
||||
"xpack.securitySolution.riskScore.errors.privileges.needToHave": "次の項目が必要です。",
|
||||
"xpack.securitySolution.riskScore.failSearchDescription": "リスクスコアで検索を実行できませんでした",
|
||||
"xpack.securitySolution.riskScore.hostsDashboardWarningPanelBody": "ホストリスクスコアデータが見つかりません。グローバルKQL検索バーにグローバルフィルターがあるかどうかを確認してください。ホストリスクモジュールを有効にしたばかりの場合は、リスクエンジンがホストリスクスコアデータを生成し、このパネルに表示するまでに1時間かかることがあります。",
|
||||
"xpack.securitySolution.riskScore.hostsDashboardWarningPanelTitle": "表示するホストリスクスコアデータがありません",
|
||||
"xpack.securitySolution.riskScore.kpi.failSearchDescription": "リスクスコアで検索を実行できませんでした",
|
||||
"xpack.securitySolution.riskScore.moduleTurnedOff": "エンティティリスクスコアがオフになりました",
|
||||
"xpack.securitySolution.riskScore.moduleTurnedOn": "エンティティリスクスコアがオンになりました",
|
||||
|
@ -39702,8 +39695,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": "プレビュー",
|
||||
|
@ -39716,8 +39707,6 @@
|
|||
"xpack.securitySolution.riskScore.riskScorePreview.usefulLinks": "便利なリンク",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.users.hide": "ユーザーを非表示",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.users.show": "ユーザーを表示",
|
||||
"xpack.securitySolution.riskScore.usersDashboardWarningPanelBody": "ユーザーリスクスコアデータが見つかりません。グローバルKQL検索バーにグローバルフィルターがあるかどうかを確認してください。ユーザーリスクモジュールを有効にしたばかりの場合は、リスクエンジンがユーザーリスクスコアデータを生成し、このパネルに表示するまでに1時間かかることがあります。",
|
||||
"xpack.securitySolution.riskScore.usersDashboardWarningPanelTitle": "表示するユーザーリスクスコアデータがありません",
|
||||
"xpack.securitySolution.riskTabBody.scoreOverTimeTitle": "経時的な{riskEntity}リスクスコア",
|
||||
"xpack.securitySolution.riskTabBody.viewDashboardButtonLabel": "ソースダッシュボードを表示",
|
||||
"xpack.securitySolution.rowRenderer.executedProcessDescription": "実行されたプロセス",
|
||||
|
|
|
@ -37373,8 +37373,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 文件来批量分配资产关键度。这确保了数据准确度,并减少了手动输入错误。",
|
||||
|
@ -37437,8 +37435,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}名称",
|
||||
|
@ -37450,7 +37446,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": "所有指标匹配",
|
||||
|
@ -39101,8 +39096,6 @@
|
|||
"xpack.securitySolution.riskScore.errors.privileges.check": "检查权限",
|
||||
"xpack.securitySolution.riskScore.errors.privileges.needToHave": "您需要具有:",
|
||||
"xpack.securitySolution.riskScore.failSearchDescription": "无法对风险分数执行搜索",
|
||||
"xpack.securitySolution.riskScore.hostsDashboardWarningPanelBody": "找不到任何主机风险分数数据。检查全局 KQL 搜索栏中是否具有任何全局筛选。如果刚刚启用了主机风险模块,风险引擎可能需要一小时才能生成并在此面板中显示主机风险分数数据。",
|
||||
"xpack.securitySolution.riskScore.hostsDashboardWarningPanelTitle": "没有可显示的主机风险分数数据",
|
||||
"xpack.securitySolution.riskScore.kpi.failSearchDescription": "无法对风险分数执行搜索",
|
||||
"xpack.securitySolution.riskScore.moduleTurnedOff": "已关闭实体风险分数",
|
||||
"xpack.securitySolution.riskScore.moduleTurnedOn": "已打开实体风险分数",
|
||||
|
@ -39123,8 +39116,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": "预览",
|
||||
|
@ -39137,8 +39128,6 @@
|
|||
"xpack.securitySolution.riskScore.riskScorePreview.usefulLinks": "有用的链接",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.users.hide": "隐藏用户",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.users.show": "显示用户",
|
||||
"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]
|
||||
|
|
|
@ -93,6 +93,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(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
@ -106,3 +106,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
|
|
@ -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 {
|
||||
riskEngineResourcesInstalled: boolean;
|
||||
riskEngineConfigurationCreated: 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,37 +5,27 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EntityTypeToIdentifierField, EntityType } from '../../../../entity_analytics/types';
|
||||
import type { ESQuery } from '../../../../typed_json';
|
||||
import {
|
||||
RiskScoreEntity,
|
||||
getRiskScoreLatestIndex,
|
||||
getRiskScoreTimeSeriesIndex,
|
||||
} from '../../../../entity_analytics/risk_engine';
|
||||
export { RiskQueries } from '../../../../api/search_strategy';
|
||||
|
||||
export const getHostRiskIndex = (spaceId: string, onlyLatest: boolean = true): string => {
|
||||
return onlyLatest ? getRiskScoreLatestIndex(spaceId) : getRiskScoreTimeSeriesIndex(spaceId);
|
||||
};
|
||||
|
||||
export const getUserRiskIndex = (spaceId: string, onlyLatest: boolean = true): string => {
|
||||
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 };
|
||||
|
|
|
@ -539,7 +539,7 @@ paths:
|
|||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: entities_types
|
||||
name: entity_types
|
||||
required: true
|
||||
schema:
|
||||
items:
|
||||
|
@ -981,6 +981,7 @@ components:
|
|||
oneOf:
|
||||
- $ref: '#/components/schemas/UserEntity'
|
||||
- $ref: '#/components/schemas/HostEntity'
|
||||
- $ref: '#/components/schemas/ServiceEntity'
|
||||
EntityRiskLevels:
|
||||
enum:
|
||||
- Unknown
|
||||
|
@ -1217,6 +1218,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
|
||||
|
|
|
@ -539,7 +539,7 @@ paths:
|
|||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: entities_types
|
||||
name: entity_types
|
||||
required: true
|
||||
schema:
|
||||
items:
|
||||
|
@ -981,6 +981,7 @@ components:
|
|||
oneOf:
|
||||
- $ref: '#/components/schemas/UserEntity'
|
||||
- $ref: '#/components/schemas/HostEntity'
|
||||
- $ref: '#/components/schemas/ServiceEntity'
|
||||
EntityRiskLevels:
|
||||
enum:
|
||||
- Unknown
|
||||
|
@ -1217,6 +1218,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;
|
||||
|
|
|
@ -89,7 +89,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,
|
||||
|
|
|
@ -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` },
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
|
|
@ -11,10 +11,11 @@ import { TestProviders } from '../../../common/mock';
|
|||
import { useSearchStrategy } from '../../../common/containers/use_search_strategy';
|
||||
import { useAppToasts } from '../../../common/hooks/use_app_toasts';
|
||||
import { useAppToastsMock } from '../../../common/hooks/use_app_toasts.mock';
|
||||
import { RiskScoreEntity } from '../../../../common/search_strategy';
|
||||
import { EntityType } from '../../../../common/search_strategy';
|
||||
import { useRiskEngineStatus } from './use_risk_engine_status';
|
||||
import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities';
|
||||
import type { RiskEngineStatusResponse } from '../../../../common/api/entity_analytics';
|
||||
import { EntityRiskQueries } from '../../../../common/api/search_strategy';
|
||||
jest.mock('../../../common/components/ml/hooks/use_ml_capabilities', () => ({
|
||||
useMlCapabilities: jest.fn(),
|
||||
}));
|
||||
|
@ -83,129 +84,126 @@ const mockRiskEngineStatus = (status: RiskEngineStatusResponse['risk_engine_stat
|
|||
isFetching: false,
|
||||
});
|
||||
};
|
||||
describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
|
||||
'useRiskScore entityType: %s',
|
||||
(riskEntity) => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
appToastsMock = useAppToastsMock.create();
|
||||
(useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
|
||||
mockUseSearchStrategy.mockReturnValue(defaultSearchResponse);
|
||||
mockUseMlCapabilities.mockReturnValue({
|
||||
isPlatinumOrTrialLicense: true,
|
||||
});
|
||||
describe.each([EntityType.host, EntityType.user])('useRiskScore entityType: %s', (riskEntity) => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
appToastsMock = useAppToastsMock.create();
|
||||
(useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
|
||||
mockUseSearchStrategy.mockReturnValue(defaultSearchResponse);
|
||||
mockUseMlCapabilities.mockReturnValue({
|
||||
isPlatinumOrTrialLicense: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('does not search if license is not valid', () => {
|
||||
makeLicenseInvalid();
|
||||
mockRiskEngineStatus('ENABLED');
|
||||
test('does not search if license is not valid', () => {
|
||||
makeLicenseInvalid();
|
||||
mockRiskEngineStatus('ENABLED');
|
||||
|
||||
const { result } = renderHook(() => useRiskScore({ riskEntity }), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
expect(mockSearch).not.toHaveBeenCalled();
|
||||
const { result } = renderHook(() => useRiskScore({ riskEntity }), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
expect(mockSearch).not.toHaveBeenCalled();
|
||||
expect(result.current).toEqual({
|
||||
loading: false,
|
||||
...defaultRisk,
|
||||
hasEngineBeenInstalled: true,
|
||||
isAuthorized: false,
|
||||
refetch: result.current.refetch,
|
||||
});
|
||||
});
|
||||
test('does not search if engine is not installed', () => {
|
||||
mockRiskEngineStatus('NOT_INSTALLED');
|
||||
const { result } = renderHook(() => useRiskScore({ riskEntity }), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
expect(mockSearch).not.toHaveBeenCalled();
|
||||
expect(result.current).toEqual({
|
||||
loading: false,
|
||||
...defaultRisk,
|
||||
refetch: result.current.refetch,
|
||||
});
|
||||
});
|
||||
|
||||
test('handle index not found error', () => {
|
||||
mockRiskEngineStatus('ENABLED');
|
||||
mockUseSearchStrategy.mockReturnValue({
|
||||
...defaultSearchResponse,
|
||||
error: {
|
||||
attributes: {
|
||||
caused_by: {
|
||||
type: 'index_not_found_exception',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const { result } = renderHook(() => useRiskScore({ riskEntity }), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
expect(result.current).toEqual({
|
||||
loading: false,
|
||||
...defaultRisk,
|
||||
hasEngineBeenInstalled: true,
|
||||
refetch: result.current.refetch,
|
||||
error: {
|
||||
attributes: {
|
||||
caused_by: {
|
||||
type: 'index_not_found_exception',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('show error toast', () => {
|
||||
mockRiskEngineStatus('ENABLED');
|
||||
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 engine is enabled', () => {
|
||||
mockRiskEngineStatus('ENABLED');
|
||||
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('returns result', async () => {
|
||||
mockRiskEngineStatus('ENABLED');
|
||||
|
||||
mockUseSearchStrategy.mockReturnValue({
|
||||
...defaultSearchResponse,
|
||||
result: {
|
||||
data: [],
|
||||
totalCount: 0,
|
||||
},
|
||||
});
|
||||
const { result } = renderHook(() => useRiskScore({ riskEntity }), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual({
|
||||
loading: false,
|
||||
...defaultRisk,
|
||||
hasEngineBeenInstalled: true,
|
||||
isAuthorized: false,
|
||||
data: [],
|
||||
refetch: result.current.refetch,
|
||||
});
|
||||
});
|
||||
test('does not search if engine is not installed', () => {
|
||||
mockRiskEngineStatus('NOT_INSTALLED');
|
||||
const { result } = renderHook(() => useRiskScore({ riskEntity }), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
expect(mockSearch).not.toHaveBeenCalled();
|
||||
expect(result.current).toEqual({
|
||||
loading: false,
|
||||
...defaultRisk,
|
||||
refetch: result.current.refetch,
|
||||
});
|
||||
});
|
||||
|
||||
test('handle index not found error', () => {
|
||||
mockRiskEngineStatus('ENABLED');
|
||||
mockUseSearchStrategy.mockReturnValue({
|
||||
...defaultSearchResponse,
|
||||
error: {
|
||||
attributes: {
|
||||
caused_by: {
|
||||
type: 'index_not_found_exception',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const { result } = renderHook(() => useRiskScore({ riskEntity }), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
expect(result.current).toEqual({
|
||||
loading: false,
|
||||
...defaultRisk,
|
||||
hasEngineBeenInstalled: true,
|
||||
refetch: result.current.refetch,
|
||||
error: {
|
||||
attributes: {
|
||||
caused_by: {
|
||||
type: 'index_not_found_exception',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('show error toast', () => {
|
||||
mockRiskEngineStatus('ENABLED');
|
||||
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 engine is enabled', () => {
|
||||
mockRiskEngineStatus('ENABLED');
|
||||
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('returns result', async () => {
|
||||
mockRiskEngineStatus('ENABLED');
|
||||
|
||||
mockUseSearchStrategy.mockReturnValue({
|
||||
...defaultSearchResponse,
|
||||
result: {
|
||||
data: [],
|
||||
totalCount: 0,
|
||||
},
|
||||
});
|
||||
const { result } = renderHook(() => useRiskScore({ riskEntity }), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual({
|
||||
loading: false,
|
||||
...defaultRisk,
|
||||
hasEngineBeenInstalled: true,
|
||||
data: [],
|
||||
refetch: result.current.refetch,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,27 +8,27 @@
|
|||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EntityRiskQueries } from '../../../../common/api/search_strategy';
|
||||
import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities';
|
||||
import { createFilter } from '../../../common/containers/helpers';
|
||||
import type { RiskScoreSortField, StrategyResponseType } from '../../../../common/search_strategy';
|
||||
import { RiskQueries, RiskScoreEntity } from '../../../../common/search_strategy';
|
||||
import type {
|
||||
EntityType,
|
||||
RiskScoreSortField,
|
||||
RiskScoreStrategyResponse,
|
||||
StrategyResponseType,
|
||||
} 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';
|
||||
import type { inputsModel } from '../../../common/store';
|
||||
import { useSearchStrategy } from '../../../common/containers/use_search_strategy';
|
||||
import { useGetDefaulRiskIndex } from '../../hooks/use_get_default_risk_index';
|
||||
import { useGetDefaultRiskIndex } from '../../hooks/use_get_default_risk_index';
|
||||
import { useHasSecurityCapability } from '../../../helper_hooks';
|
||||
import { useRiskEngineStatus } 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;
|
||||
|
@ -58,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,
|
||||
|
@ -76,9 +73,8 @@ export const useRiskScore = <T extends RiskScoreEntity.host | RiskScoreEntity.us
|
|||
riskEntity,
|
||||
includeAlertsCount = false,
|
||||
}: UseRiskScore<T>): RiskScoreState<T> => {
|
||||
const defaultIndex = useGetDefaulRiskIndex(riskEntity, onlyLatest);
|
||||
const factoryQueryType =
|
||||
riskEntity === RiskScoreEntity.host ? RiskQueries.hostsRiskScore : RiskQueries.usersRiskScore;
|
||||
const defaultIndex = useGetDefaultRiskIndex(riskEntity, onlyLatest);
|
||||
const factoryQueryType = EntityRiskQueries.list;
|
||||
const { querySize, cursorStart } = pagination || {};
|
||||
const { addError } = useAppToasts();
|
||||
const { isPlatinumOrTrialLicense } = useMlCapabilities();
|
||||
|
@ -93,7 +89,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 || !hasEngineBeenInstalled || isStatusLoading || !isAuthorized,
|
||||
|
|
|
@ -8,12 +8,9 @@
|
|||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { RiskScoreEntity } from '../../../../common/search_strategy';
|
||||
import {
|
||||
RiskQueries,
|
||||
RiskSeverity,
|
||||
EMPTY_SEVERITY_COUNT,
|
||||
} from '../../../../common/search_strategy';
|
||||
import { EntityRiskQueries } from '../../../../common/api/search_strategy';
|
||||
import type { EntityType } from '../../../../common/search_strategy';
|
||||
import { RiskSeverity, 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';
|
||||
|
@ -21,7 +18,7 @@ import { useSearchStrategy } from '../../../common/containers/use_search_strateg
|
|||
import type { InspectResponse } from '../../../types';
|
||||
import type { inputsModel } from '../../../common/store';
|
||||
import { useAppToasts } from '../../../common/hooks/use_app_toasts';
|
||||
import { useGetDefaulRiskIndex } from '../../hooks/use_get_default_risk_index';
|
||||
import { useGetDefaultRiskIndex } from '../../hooks/use_get_default_risk_index';
|
||||
import { useRiskEngineStatus } from './use_risk_engine_status';
|
||||
|
||||
interface RiskScoreKpi {
|
||||
|
@ -37,7 +34,7 @@ interface RiskScoreKpi {
|
|||
interface UseRiskScoreKpiProps {
|
||||
filterQuery?: string | ESQuery;
|
||||
skip?: boolean;
|
||||
riskEntity: RiskScoreEntity;
|
||||
riskEntity: EntityType;
|
||||
timerange?: { to: string; from: string };
|
||||
}
|
||||
|
||||
|
@ -48,7 +45,7 @@ export const useRiskScoreKpi = ({
|
|||
timerange,
|
||||
}: UseRiskScoreKpiProps): RiskScoreKpi => {
|
||||
const { addError } = useAppToasts();
|
||||
const defaultIndex = useGetDefaulRiskIndex(riskEntity);
|
||||
const defaultIndex = useGetDefaultRiskIndex(riskEntity);
|
||||
const {
|
||||
data: riskEngineStatus,
|
||||
isFetching: isStatusLoading,
|
||||
|
@ -56,8 +53,8 @@ export const useRiskScoreKpi = ({
|
|||
} = useRiskEngineStatus();
|
||||
const riskEngineHasBeenEnabled = riskEngineStatus?.risk_engine_status !== 'NOT_INSTALLED';
|
||||
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 {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { EuiEmptyPrompt, EuiPanel, EuiToolTip } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { RiskScoreEntity } from '../../../../common/search_strategy';
|
||||
import type { EntityType } from '../../../../common/search_strategy';
|
||||
import { HeaderSection } from '../../../common/components/header_section';
|
||||
import * as i18n from './translations';
|
||||
import { EntityAnalyticsLearnMoreLink } from '../entity_analytics_learn_more_link';
|
||||
|
@ -20,7 +20,7 @@ const EnableRiskScoreComponent = ({
|
|||
entityType,
|
||||
}: {
|
||||
isDisabled: boolean;
|
||||
entityType: RiskScoreEntity;
|
||||
entityType: EntityType;
|
||||
}) => {
|
||||
if (!isDisabled) {
|
||||
return null;
|
||||
|
|
|
@ -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,7 @@ export const ENABLE_RISK_SCORE_POPOVER = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const ENABLE_RISK_SCORE = (riskEntity: RiskScoreEntity) =>
|
||||
export const ENABLE_RISK_SCORE = (riskEntity: EntityType) =>
|
||||
i18n.translate('xpack.securitySolution.enableRiskScore.enableRiskScore', {
|
||||
defaultMessage: 'Enable {riskEntity} Risk Score',
|
||||
values: {
|
||||
|
@ -23,7 +23,7 @@ export const ENABLE_RISK_SCORE = (riskEntity: RiskScoreEntity) =>
|
|||
},
|
||||
});
|
||||
|
||||
export const ENABLE_RISK_SCORE_DESCRIPTION = (riskEntity: RiskScoreEntity) =>
|
||||
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.',
|
||||
|
|
|
@ -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';
|
||||
|
@ -32,7 +38,6 @@ 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';
|
||||
|
@ -41,13 +46,18 @@ import { RiskScoreHeaderTitle } from '../risk_score_header_title';
|
|||
|
||||
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) => {
|
||||
|
@ -56,23 +66,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]
|
||||
);
|
||||
|
@ -125,6 +139,7 @@ const EntityAnalyticsRiskScoresComponent = ({ riskEntity }: { riskEntity: RiskSc
|
|||
deleteQuery,
|
||||
inspect: inspectKpi,
|
||||
});
|
||||
|
||||
const {
|
||||
data,
|
||||
loading: isTableLoading,
|
||||
|
@ -207,7 +222,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(),
|
||||
hasEngineBeenInstalled: 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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,7 +18,7 @@ import { usersSelectors } from '../../../explore/users/store';
|
|||
import { useQueryInspector } from '../../../common/components/page/manage_query';
|
||||
import { TopRiskScoreContributorsAlerts } from '../top_risk_score_contributors_alerts';
|
||||
import { useQueryToggle } from '../../../common/containers/query_toggle';
|
||||
import { buildEntityNameFilter, RiskScoreEntity } from '../../../../common/search_strategy';
|
||||
import { buildEntityNameFilter, EntityType } from '../../../../common/search_strategy';
|
||||
import type { UsersComponentsQueryProps } from '../../../explore/users/pages/navigation/types';
|
||||
import type { HostsComponentsQueryProps } from '../../../explore/hosts/pages/navigation/types';
|
||||
import { HostRiskScoreQueryId, UserRiskScoreQueryId } from '../../common/utils';
|
||||
|
@ -36,19 +36,19 @@ type ComponentsQueryProps = HostsComponentsQueryProps | UsersComponentsQueryProp
|
|||
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)
|
||||
);
|
||||
|
@ -65,7 +65,7 @@ const RiskDetailsTabBodyComponent: React.FC<
|
|||
useQueryToggle(`${queryId} contributors`);
|
||||
|
||||
const filterQuery = useMemo(
|
||||
() => (entityName ? buildEntityNameFilter([entityName], riskEntity) : {}),
|
||||
() => (entityName ? buildEntityNameFilter(riskEntity, [entityName]) : {}),
|
||||
[entityName, riskEntity]
|
||||
);
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -32,7 +32,7 @@ import { BETA } from '../../../common/translations';
|
|||
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,
|
||||
|
@ -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 (
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -7,28 +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,53 +6,36 @@
|
|||
*/
|
||||
|
||||
import { EuiEmptyPrompt, EuiPanel } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { RiskScoreEntity } from '../../../common/search_strategy';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { EntityType } from '../../../common/search_strategy';
|
||||
import { RiskScoreHeaderTitle } from './risk_score_header_title';
|
||||
import { HeaderSection } from '../../common/components/header_section';
|
||||
|
||||
const HOST_WARNING_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.hostsDashboardWarningPanelTitle',
|
||||
{
|
||||
defaultMessage: 'No host risk score data available to display',
|
||||
}
|
||||
);
|
||||
|
||||
const USER_WARNING_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.usersDashboardWarningPanelTitle',
|
||||
{
|
||||
defaultMessage: 'No user risk score data available to display',
|
||||
}
|
||||
);
|
||||
|
||||
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.`,
|
||||
}
|
||||
);
|
||||
|
||||
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.`,
|
||||
}
|
||||
);
|
||||
|
||||
const RiskScoresNoDataDetectedComponent = ({ entityType }: { entityType: RiskScoreEntity }) => {
|
||||
const translations = useMemo(
|
||||
() => ({
|
||||
title: entityType === RiskScoreEntity.user ? USER_WARNING_TITLE : HOST_WARNING_TITLE,
|
||||
body: entityType === RiskScoreEntity.user ? USER_WARNING_BODY : HOST_WARNING_BODY,
|
||||
}),
|
||||
[entityType]
|
||||
);
|
||||
|
||||
const RiskScoresNoDataDetectedComponent = ({ entityType }: { entityType: 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} />
|
||||
<EuiEmptyPrompt
|
||||
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 }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -9,7 +9,7 @@ import { render } from '@testing-library/react';
|
|||
import React from 'react';
|
||||
import { RiskScoreOverTime } from '.';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import { RiskScoreEntity } from '../../../../common/search_strategy';
|
||||
import { EntityType } from '../../../../common/entity_analytics/types';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
|
||||
|
||||
const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock;
|
||||
|
@ -30,7 +30,7 @@ jest.mock('../../../common/hooks/use_space_id', () => ({
|
|||
}));
|
||||
|
||||
const props = {
|
||||
riskEntity: RiskScoreEntity.host,
|
||||
riskEntity: EntityType.host,
|
||||
riskScore: [],
|
||||
loading: false,
|
||||
from: '2020-07-07T08:20:18.966Z',
|
||||
|
|
|
@ -12,21 +12,15 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
|
|||
import { HeaderSection } from '../../../common/components/header_section';
|
||||
import { InspectButtonContainer } from '../../../common/components/inspect';
|
||||
|
||||
import type {
|
||||
HostRiskScore,
|
||||
RiskScoreEntity,
|
||||
UserRiskScore,
|
||||
} from '../../../../common/search_strategy';
|
||||
import type { EntityType } from '../../../../common/search_strategy';
|
||||
import { useSpaceId } from '../../../common/hooks/use_space_id';
|
||||
import { VisualizationEmbeddable } from '../../../common/components/visualization_actions/visualization_embeddable';
|
||||
import { getRiskScoreOverTimeAreaAttributes } from '../../lens_attributes/risk_score_over_time_area';
|
||||
|
||||
export interface RiskScoreOverTimeProps {
|
||||
export interface RiskScoreOverTimeProps<T extends EntityType> {
|
||||
from: string;
|
||||
to: string;
|
||||
loading: boolean;
|
||||
riskScore?: Array<HostRiskScore | UserRiskScore>;
|
||||
riskEntity: RiskScoreEntity;
|
||||
riskEntity: T;
|
||||
queryId: string;
|
||||
title: string;
|
||||
toggleStatus: boolean;
|
||||
|
@ -37,61 +31,62 @@ const CHART_HEIGHT = 180;
|
|||
|
||||
export const scoreFormatter = (d: number) => Math.round(d).toString();
|
||||
|
||||
const RiskScoreOverTimeComponent: React.FC<RiskScoreOverTimeProps> = ({
|
||||
from,
|
||||
to,
|
||||
riskScore,
|
||||
loading,
|
||||
queryId,
|
||||
riskEntity,
|
||||
title,
|
||||
toggleStatus,
|
||||
toggleQuery,
|
||||
}) => {
|
||||
const spaceId = useSpaceId();
|
||||
const timerange = useMemo(
|
||||
() => ({
|
||||
from,
|
||||
to,
|
||||
}),
|
||||
[from, to]
|
||||
);
|
||||
return (
|
||||
<InspectButtonContainer>
|
||||
<EuiPanel hasBorder data-test-subj="RiskScoreOverTime">
|
||||
<EuiFlexGroup gutterSize={'none'}>
|
||||
<EuiFlexItem grow={1}>
|
||||
<HeaderSection
|
||||
title={title}
|
||||
hideSubtitle
|
||||
toggleQuery={toggleQuery}
|
||||
toggleStatus={toggleStatus}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
{toggleStatus && (
|
||||
<EuiFlexGroup gutterSize="none" direction="column">
|
||||
const RiskScoreOverTimeComponent = React.memo(
|
||||
<T extends EntityType>({
|
||||
from,
|
||||
to,
|
||||
queryId,
|
||||
riskEntity,
|
||||
title,
|
||||
toggleStatus,
|
||||
toggleQuery,
|
||||
}: RiskScoreOverTimeProps<T>) => {
|
||||
const spaceId = useSpaceId();
|
||||
const timerange = useMemo(
|
||||
() => ({
|
||||
from,
|
||||
to,
|
||||
}),
|
||||
[from, to]
|
||||
);
|
||||
return (
|
||||
<InspectButtonContainer>
|
||||
<EuiPanel hasBorder data-test-subj="RiskScoreOverTime">
|
||||
<EuiFlexGroup gutterSize={'none'}>
|
||||
<EuiFlexItem grow={1}>
|
||||
{spaceId && (
|
||||
<VisualizationEmbeddable
|
||||
applyGlobalQueriesAndFilters={false}
|
||||
timerange={timerange}
|
||||
getLensAttributes={getRiskScoreOverTimeAreaAttributes}
|
||||
stackByField={riskEntity}
|
||||
id={`${queryId}-embeddable`}
|
||||
height={CHART_HEIGHT}
|
||||
extraOptions={{ spaceId }}
|
||||
/>
|
||||
)}
|
||||
<HeaderSection
|
||||
title={title}
|
||||
hideSubtitle
|
||||
toggleQuery={toggleQuery}
|
||||
toggleStatus={toggleStatus}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiPanel>
|
||||
</InspectButtonContainer>
|
||||
);
|
||||
};
|
||||
|
||||
RiskScoreOverTimeComponent.displayName = 'RiskScoreOverTimeComponent';
|
||||
export const RiskScoreOverTime = React.memo(RiskScoreOverTimeComponent);
|
||||
RiskScoreOverTime.displayName = 'RiskScoreOverTime';
|
||||
{toggleStatus && (
|
||||
<EuiFlexGroup gutterSize="none" direction="column">
|
||||
<EuiFlexItem grow={1}>
|
||||
{spaceId && (
|
||||
<VisualizationEmbeddable
|
||||
applyGlobalQueriesAndFilters={false}
|
||||
timerange={timerange}
|
||||
getLensAttributes={getRiskScoreOverTimeAreaAttributes}
|
||||
stackByField={riskEntity}
|
||||
id={`${queryId}-embeddable`}
|
||||
height={CHART_HEIGHT}
|
||||
extraOptions={{ spaceId }}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiPanel>
|
||||
</InspectButtonContainer>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const RiskScoreOverTime = React.memo(
|
||||
RiskScoreOverTimeComponent
|
||||
) as typeof RiskScoreOverTimeComponent & { displayName: string }; // This is needed to male React.memo work with generic
|
||||
RiskScoreOverTimeComponent.displayName = 'RiskScoreOverTime';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useState, useMemo, Fragment } from 'react';
|
||||
import {
|
||||
EuiAccordion,
|
||||
EuiPanel,
|
||||
|
@ -22,11 +22,10 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import type { BoolQuery } from '@kbn/es-query';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { EntityType } from '../../../common/entity_analytics/types';
|
||||
import { EntityTypeToIdentifierField } from '../../../common/entity_analytics/types';
|
||||
import type { EntityRiskScoreRecord } from '../../../common/api/entity_analytics/common';
|
||||
import {
|
||||
RiskScoreEntity,
|
||||
RISK_SCORE_INDEX_PATTERN,
|
||||
} from '../../../common/entity_analytics/risk_engine';
|
||||
import { RISK_SCORE_INDEX_PATTERN } from '../../../common/entity_analytics/risk_engine';
|
||||
import { RiskScorePreviewTable } from './risk_score_preview_table';
|
||||
import * as i18n from '../translations';
|
||||
import { useRiskScorePreview } from '../api/hooks/use_preview_risk_scores';
|
||||
|
@ -34,12 +33,14 @@ import { SourcererScopeName } from '../../sourcerer/store/model';
|
|||
import { useSourcererDataView } from '../../sourcerer/containers';
|
||||
import type { RiskEngineMissingPrivilegesResponse } from '../hooks/use_missing_risk_engine_privileges';
|
||||
import { userHasRiskEngineReadPermissions } from '../common';
|
||||
import { EntityIconByType } from './entity_store/helpers';
|
||||
import { useRiskEngineEntityTypes } from '../hooks/use_enabled_entity_types';
|
||||
interface IRiskScorePreviewPanel {
|
||||
showMessage: string;
|
||||
hideMessage: string;
|
||||
showMessage: React.ReactNode;
|
||||
hideMessage: React.ReactNode;
|
||||
isLoading: boolean;
|
||||
items: EntityRiskScoreRecord[];
|
||||
type: RiskScoreEntity;
|
||||
type: EntityType;
|
||||
}
|
||||
|
||||
const getRiskiestScores = (scores: EntityRiskScoreRecord[] = [], field: string) =>
|
||||
|
@ -125,7 +126,7 @@ const RiskScorePreviewPanel = ({
|
|||
buttonContent={trigger === 'closed' ? showMessage : hideMessage}
|
||||
forceState={trigger}
|
||||
onToggle={onToggle}
|
||||
extraAction={<EuiIcon type={type === RiskScoreEntity.host ? 'storage' : 'user'} />}
|
||||
extraAction={<EuiIcon type={EntityIconByType[type]} />}
|
||||
>
|
||||
<>
|
||||
<EuiSpacer size={'m'} />
|
||||
|
@ -141,6 +142,8 @@ const RiskEnginePreview: React.FC<{ includeClosedAlerts: boolean; from: string;
|
|||
from,
|
||||
to,
|
||||
}) => {
|
||||
const entityTypes = useRiskEngineEntityTypes();
|
||||
|
||||
const [filters] = useState<{ bool: BoolQuery }>({
|
||||
bool: { must: [], filter: [], should: [], must_not: [] },
|
||||
});
|
||||
|
@ -157,9 +160,6 @@ const RiskEnginePreview: React.FC<{ includeClosedAlerts: boolean; from: string;
|
|||
exclude_alert_statuses: includeClosedAlerts ? [] : ['closed'],
|
||||
});
|
||||
|
||||
const hosts = getRiskiestScores(data?.scores.host, 'host.name');
|
||||
const users = getRiskiestScores(data?.scores.user, 'user.name');
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<EuiCallOut
|
||||
|
@ -187,23 +187,37 @@ const RiskEnginePreview: React.FC<{ includeClosedAlerts: boolean; from: string;
|
|||
<EuiSpacer />
|
||||
<EuiSpacer />
|
||||
|
||||
<RiskScorePreviewPanel
|
||||
items={hosts}
|
||||
showMessage={i18n.SHOW_HOSTS_RISK_SCORE}
|
||||
hideMessage={i18n.HIDE_HOSTS_RISK_SCORE}
|
||||
isLoading={isLoading}
|
||||
type={RiskScoreEntity.host}
|
||||
/>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<RiskScorePreviewPanel
|
||||
items={users}
|
||||
showMessage={i18n.SHOW_USERS_RISK_SCORE}
|
||||
hideMessage={i18n.HIDE_USERS_RISK_SCORE}
|
||||
isLoading={isLoading}
|
||||
type={RiskScoreEntity.user}
|
||||
/>
|
||||
{entityTypes.map((entityType) => (
|
||||
<Fragment key={entityType}>
|
||||
<RiskScorePreviewPanel
|
||||
items={getRiskiestScores(
|
||||
data?.scores[entityType],
|
||||
EntityTypeToIdentifierField[entityType]
|
||||
)}
|
||||
showMessage={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.riskScore.riskScorePreview.show"
|
||||
defaultMessage="Show {entityType}s"
|
||||
values={{
|
||||
entityType,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
hideMessage={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.riskScore.riskScorePreview.hide"
|
||||
defaultMessage="Hide {entityType}s"
|
||||
values={{
|
||||
entityType,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
isLoading={isLoading}
|
||||
type={entityType}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -12,9 +12,8 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import type { EntityRiskScoreRecord } from '../../../common/api/entity_analytics/common';
|
||||
import type { RiskSeverity } from '../../../common/search_strategy';
|
||||
import { RiskScoreLevel } from './severity/common';
|
||||
|
||||
import { HostDetailsLink, UserDetailsLink } from '../../common/components/links';
|
||||
import { RiskScoreEntity } from '../../../common/entity_analytics/risk_engine';
|
||||
import { EntityDetailsLink } from '../../common/components/links';
|
||||
import type { EntityType } from '../../../common/entity_analytics/types';
|
||||
|
||||
type RiskScoreColumn = EuiBasicTableColumn<EntityRiskScoreRecord> & {
|
||||
field: keyof EntityRiskScoreRecord;
|
||||
|
@ -25,7 +24,7 @@ export const RiskScorePreviewTable = ({
|
|||
type,
|
||||
}: {
|
||||
items: EntityRiskScoreRecord[];
|
||||
type: RiskScoreEntity;
|
||||
type: EntityType;
|
||||
}) => {
|
||||
const columns: RiskScoreColumn[] = [
|
||||
{
|
||||
|
@ -36,13 +35,9 @@ export const RiskScorePreviewTable = ({
|
|||
defaultMessage="Name"
|
||||
/>
|
||||
),
|
||||
render: (itemName: string) => {
|
||||
return type === RiskScoreEntity.host ? (
|
||||
<HostDetailsLink hostName={itemName} />
|
||||
) : (
|
||||
<UserDetailsLink userName={itemName} />
|
||||
);
|
||||
},
|
||||
render: (entityName: string) => (
|
||||
<EntityDetailsLink entityName={entityName} entityType={type} />
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'calculated_level',
|
||||
|
@ -80,9 +75,7 @@ export const RiskScorePreviewTable = ({
|
|||
|
||||
return (
|
||||
<EuiInMemoryTable<EntityRiskScoreRecord>
|
||||
data-test-subj={
|
||||
type === RiskScoreEntity.host ? 'host-risk-preview-table' : 'user-risk-preview-table'
|
||||
}
|
||||
data-test-subj={`${type}-risk-preview-table`}
|
||||
responsiveBreakpoint={false}
|
||||
items={items}
|
||||
columns={columns}
|
||||
|
|
|
@ -10,7 +10,7 @@ import React from 'react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { sumBy } from 'lodash/fp';
|
||||
|
||||
import type { HostRiskScore, RiskStats, UserRiskScore } from '../../../../common/search_strategy';
|
||||
import type { EntityRiskScore, EntityType, RiskStats } from '../../../../common/search_strategy';
|
||||
import { formatRiskScore } from '../../common';
|
||||
|
||||
interface TableItem {
|
||||
|
@ -107,24 +107,11 @@ export const getItems: (entityData: EntityData | undefined) => TableItem[] = (en
|
|||
];
|
||||
};
|
||||
|
||||
export function isUserRiskData(
|
||||
riskData: UserRiskScore | HostRiskScore | undefined
|
||||
): riskData is UserRiskScore {
|
||||
return !!riskData && (riskData as UserRiskScore).user !== undefined;
|
||||
}
|
||||
|
||||
export const getEntityData = (
|
||||
riskData: UserRiskScore | HostRiskScore | undefined
|
||||
export const getEntityData = <T extends EntityType>(
|
||||
entityType: T,
|
||||
riskData: EntityRiskScore<T> | undefined
|
||||
): EntityData | undefined => {
|
||||
if (!riskData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isUserRiskData(riskData)) {
|
||||
return riskData.user;
|
||||
}
|
||||
|
||||
return riskData.host;
|
||||
return riskData?.[entityType];
|
||||
};
|
||||
|
||||
export const LENS_VISUALIZATION_HEIGHT = 126; // Static height in pixels specified by design
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import React from 'react';
|
||||
import type { Story } from '@storybook/react';
|
||||
import { TestProvider } from '@kbn/expandable-flyout/src/test/provider';
|
||||
import { EntityType } from '../../../../common/search_strategy';
|
||||
import { StorybookProviders } from '../../../common/mock/storybook_providers';
|
||||
import { mockRiskScoreState } from '../../../flyout/shared/mocks';
|
||||
import { FlyoutRiskSummary } from './risk_summary';
|
||||
|
@ -28,6 +29,7 @@ export const Default: Story<void> = () => {
|
|||
queryId={'testQuery'}
|
||||
recalculatingScore={false}
|
||||
isLinkEnabled
|
||||
entityType={EntityType.user}
|
||||
/>
|
||||
</div>
|
||||
</TestProvider>
|
||||
|
@ -47,6 +49,7 @@ export const LinkEnabledInPreviewMode: Story<void> = () => {
|
|||
openDetailsPanel={() => {}}
|
||||
isLinkEnabled
|
||||
isPreviewMode
|
||||
entityType={EntityType.user}
|
||||
/>
|
||||
</div>
|
||||
</TestProvider>
|
||||
|
@ -65,6 +68,7 @@ export const LinkDisabled: Story<void> = () => {
|
|||
recalculatingScore={false}
|
||||
openDetailsPanel={() => {}}
|
||||
isLinkEnabled={false}
|
||||
entityType={EntityType.user}
|
||||
/>
|
||||
</div>
|
||||
</TestProvider>
|
||||
|
|
|
@ -18,6 +18,7 @@ import type {
|
|||
VisualizationEmbeddableProps,
|
||||
} from '../../../common/components/visualization_actions/types';
|
||||
import type { Query } from '@kbn/es-query';
|
||||
import { EntityType } from '../../../../common/search_strategy';
|
||||
|
||||
const mockVisualizationEmbeddable = jest
|
||||
.fn()
|
||||
|
@ -42,6 +43,7 @@ describe('FlyoutRiskSummary', () => {
|
|||
openDetailsPanel={() => {}}
|
||||
recalculatingScore={false}
|
||||
isLinkEnabled
|
||||
entityType={EntityType.host}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -79,6 +81,7 @@ describe('FlyoutRiskSummary', () => {
|
|||
recalculatingScore={false}
|
||||
isLinkEnabled
|
||||
isPreviewMode
|
||||
entityType={EntityType.host}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -97,6 +100,7 @@ describe('FlyoutRiskSummary', () => {
|
|||
openDetailsPanel={() => {}}
|
||||
recalculatingScore={false}
|
||||
isLinkEnabled
|
||||
entityType={EntityType.host}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -112,6 +116,7 @@ describe('FlyoutRiskSummary', () => {
|
|||
openDetailsPanel={() => {}}
|
||||
recalculatingScore={false}
|
||||
isLinkEnabled
|
||||
entityType={EntityType.host}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -128,6 +133,8 @@ describe('FlyoutRiskSummary', () => {
|
|||
openDetailsPanel={() => {}}
|
||||
recalculatingScore={false}
|
||||
isLinkEnabled={false}
|
||||
isPreviewMode
|
||||
entityType={EntityType.host}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -144,6 +151,7 @@ describe('FlyoutRiskSummary', () => {
|
|||
openDetailsPanel={() => {}}
|
||||
recalculatingScore={false}
|
||||
isLinkEnabled
|
||||
entityType={EntityType.host}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -160,6 +168,7 @@ describe('FlyoutRiskSummary', () => {
|
|||
openDetailsPanel={() => {}}
|
||||
recalculatingScore={false}
|
||||
isLinkEnabled
|
||||
entityType={EntityType.host}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -176,6 +185,7 @@ describe('FlyoutRiskSummary', () => {
|
|||
openDetailsPanel={() => {}}
|
||||
recalculatingScore={false}
|
||||
isLinkEnabled
|
||||
entityType={EntityType.host}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -204,6 +214,7 @@ describe('FlyoutRiskSummary', () => {
|
|||
openDetailsPanel={() => {}}
|
||||
recalculatingScore={false}
|
||||
isLinkEnabled
|
||||
entityType={EntityType.host}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -227,6 +238,7 @@ describe('FlyoutRiskSummary', () => {
|
|||
openDetailsPanel={() => {}}
|
||||
recalculatingScore={false}
|
||||
isLinkEnabled
|
||||
entityType={EntityType.user}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -250,6 +262,7 @@ describe('FlyoutRiskSummary', () => {
|
|||
openDetailsPanel={() => {}}
|
||||
recalculatingScore={false}
|
||||
isLinkEnabled
|
||||
entityType={EntityType.user}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
|
|
@ -22,13 +22,15 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import dateMath from '@kbn/datemath';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { capitalize } from 'lodash/fp';
|
||||
import type { EntityType } from '../../../../common/entity_analytics/types';
|
||||
import { EntityTypeToIdentifierField } from '../../../../common/entity_analytics/types';
|
||||
import { useKibana } from '../../../common/lib/kibana/kibana_react';
|
||||
import type { EntityDetailsPath } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header';
|
||||
import { EntityDetailsLeftPanelTab } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header';
|
||||
import { InspectButton, InspectButtonContainer } from '../../../common/components/inspect';
|
||||
import { ONE_WEEK_IN_HOURS } from '../../../flyout/entity_details/shared/constants';
|
||||
import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date';
|
||||
import { RiskScoreEntity } from '../../../../common/entity_analytics/risk_engine';
|
||||
import { VisualizationEmbeddable } from '../../../common/components/visualization_actions/visualization_embeddable';
|
||||
import { ExpandablePanel } from '../../../flyout/shared/components/expandable_panel';
|
||||
import type { RiskScoreState } from '../../api/hooks/use_risk_score';
|
||||
|
@ -38,7 +40,6 @@ import {
|
|||
columnsArray,
|
||||
getEntityData,
|
||||
getItems,
|
||||
isUserRiskData,
|
||||
LAST_30_DAYS,
|
||||
LENS_VISUALIZATION_HEIGHT,
|
||||
LENS_VISUALIZATION_MIN_WIDTH,
|
||||
|
@ -46,8 +47,9 @@ import {
|
|||
} from './common';
|
||||
import { EntityEventTypes } from '../../../common/lib/telemetry';
|
||||
|
||||
export interface RiskSummaryProps<T extends RiskScoreEntity> {
|
||||
export interface RiskSummaryProps<T extends EntityType> {
|
||||
riskScoreData: RiskScoreState<T>;
|
||||
entityType: T;
|
||||
recalculatingScore: boolean;
|
||||
queryId: string;
|
||||
openDetailsPanel: (path: EntityDetailsPath) => void;
|
||||
|
@ -55,8 +57,9 @@ export interface RiskSummaryProps<T extends RiskScoreEntity> {
|
|||
isPreviewMode?: boolean;
|
||||
}
|
||||
|
||||
const FlyoutRiskSummaryComponent = <T extends RiskScoreEntity>({
|
||||
const FlyoutRiskSummaryComponent = <T extends EntityType>({
|
||||
riskScoreData,
|
||||
entityType,
|
||||
recalculatingScore,
|
||||
queryId,
|
||||
openDetailsPanel,
|
||||
|
@ -66,34 +69,32 @@ const FlyoutRiskSummaryComponent = <T extends RiskScoreEntity>({
|
|||
const { telemetry } = useKibana().services;
|
||||
const { data } = riskScoreData;
|
||||
const riskData = data && data.length > 0 ? data[0] : undefined;
|
||||
const entityData = getEntityData(riskData);
|
||||
const entityData = getEntityData<T>(entityType, riskData);
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const lensAttributes = useMemo(() => {
|
||||
const entityName = entityData?.name ?? '';
|
||||
const fieldName = isUserRiskData(riskData) ? 'user.name' : 'host.name';
|
||||
const fieldName = EntityTypeToIdentifierField[entityType];
|
||||
|
||||
return getRiskScoreSummaryAttributes({
|
||||
severity: entityData?.risk?.calculated_level,
|
||||
query: `${fieldName}: ${entityName}`,
|
||||
spaceId: 'default',
|
||||
riskEntity: isUserRiskData(riskData) ? RiskScoreEntity.user : RiskScoreEntity.host,
|
||||
riskEntity: entityType,
|
||||
});
|
||||
}, [entityData?.name, entityData?.risk?.calculated_level, riskData]);
|
||||
}, [entityData?.name, entityData?.risk?.calculated_level, entityType]);
|
||||
|
||||
const xsFontSize = useEuiFontSize('xxs').fontSize;
|
||||
const rows = useMemo(() => getItems(entityData), [entityData]);
|
||||
|
||||
const onToggle = useCallback(
|
||||
(isOpen: boolean) => {
|
||||
const entity = isUserRiskData(riskData) ? 'user' : 'host';
|
||||
|
||||
telemetry.reportEvent(EntityEventTypes.ToggleRiskSummaryClicked, {
|
||||
entity,
|
||||
entity: entityType,
|
||||
action: isOpen ? 'show' : 'hide',
|
||||
});
|
||||
},
|
||||
[riskData, telemetry]
|
||||
[entityType, telemetry]
|
||||
);
|
||||
|
||||
const casesAttachmentMetadata = useMemo(
|
||||
|
@ -105,12 +106,12 @@ const FlyoutRiskSummaryComponent = <T extends RiskScoreEntity>({
|
|||
'Risk score for {entityType, select, user {user} other {host}} {entityName}',
|
||||
values: {
|
||||
entityName: entityData?.name,
|
||||
entityType: isUserRiskData(riskData) ? 'user' : 'host',
|
||||
entityType,
|
||||
},
|
||||
}
|
||||
),
|
||||
}),
|
||||
[entityData?.name, riskData]
|
||||
[entityData?.name, entityType]
|
||||
);
|
||||
|
||||
const riskDataTimestamp = riskData?.['@timestamp'];
|
||||
|
@ -137,7 +138,7 @@ const FlyoutRiskSummaryComponent = <T extends RiskScoreEntity>({
|
|||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.entityDetails.title"
|
||||
defaultMessage="{entity} risk summary"
|
||||
values={{ entity: isUserRiskData(riskData) ? 'User' : 'Host' }}
|
||||
values={{ entity: capitalize(entityType) }}
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
|
@ -277,5 +278,7 @@ const FlyoutRiskSummaryComponent = <T extends RiskScoreEntity>({
|
|||
);
|
||||
};
|
||||
|
||||
export const FlyoutRiskSummary = React.memo(FlyoutRiskSummaryComponent);
|
||||
export const FlyoutRiskSummary = React.memo(
|
||||
FlyoutRiskSummaryComponent
|
||||
) as typeof FlyoutRiskSummaryComponent & { displayName: string }; // This is needed to male React.memo work with generic
|
||||
FlyoutRiskSummary.displayName = 'RiskSummary';
|
||||
|
|
|
@ -7,9 +7,10 @@
|
|||
import React from 'react';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import { SeverityFilter } from './severity_filter';
|
||||
import { RiskScoreEntity, RiskSeverity } from '../../../../common/search_strategy';
|
||||
import { EntityType } from '../../../../common/entity_analytics/types';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import { createTelemetryServiceMock } from '../../../common/lib/telemetry/telemetry_service.mock';
|
||||
import { RiskSeverity } from '../../../../common/search_strategy';
|
||||
|
||||
const mockedTelemetry = createTelemetryServiceMock();
|
||||
jest.mock('../../../common/lib/kibana', () => {
|
||||
|
@ -30,7 +31,7 @@ describe('SeverityFilter', () => {
|
|||
it('sends telemetry when selecting a classification', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<SeverityFilter selectedItems={[]} onSelect={jest.fn()} riskEntity={RiskScoreEntity.user} />
|
||||
<SeverityFilter selectedItems={[]} onSelect={jest.fn()} riskEntity={EntityType.user} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -53,7 +54,7 @@ describe('SeverityFilter', () => {
|
|||
RiskSeverity.Unknown,
|
||||
]}
|
||||
onSelect={jest.fn()}
|
||||
riskEntity={RiskScoreEntity.user}
|
||||
riskEntity={EntityType.user}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
|
|
@ -6,17 +6,18 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import type { RiskSeverity } from '../../../../common/search_strategy';
|
||||
import type { MultiselectFilterProps } from '../../../common/components/multiselect_filter';
|
||||
import { MultiselectFilter } from '../../../common/components/multiselect_filter';
|
||||
import { SEVERITY_UI_SORT_ORDER } from '../../common/utils';
|
||||
import type { RiskScoreEntity, RiskSeverity } from '../../../../common/search_strategy';
|
||||
import type { EntityType } from '../../../../common/entity_analytics/types';
|
||||
import { RiskScoreLevel } from './common';
|
||||
import { ENTITY_RISK_LEVEL } from '../risk_score/translations';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { EntityEventTypes } from '../../../common/lib/telemetry';
|
||||
|
||||
export interface SeverityFilterProps {
|
||||
riskEntity?: RiskScoreEntity;
|
||||
riskEntity?: EntityType;
|
||||
onSelect: (newSelection: RiskSeverity[]) => void;
|
||||
selectedItems: RiskSeverity[];
|
||||
}
|
||||
|
|
|
@ -15,8 +15,12 @@ import { HeaderSection } from '../../../common/components/header_section';
|
|||
|
||||
import * as i18n from './translations';
|
||||
import type { RiskInputs } from '../../../../common/entity_analytics/risk_engine';
|
||||
import { RiskScoreEntity } from '../../../../common/entity_analytics/risk_engine';
|
||||
import type { HostRiskScore, UserRiskScore } from '../../../../common/search_strategy';
|
||||
import type { EntityRiskScore } from '../../../../common/search_strategy';
|
||||
import {
|
||||
EntityType,
|
||||
type HostRiskScore,
|
||||
type UserRiskScore,
|
||||
} from '../../../../common/search_strategy';
|
||||
import { ALERTS_TABLE_REGISTRY_CONFIG_IDS } from '../../../../common/constants';
|
||||
import { AlertsTableComponent } from '../../../detections/components/alerts_table';
|
||||
import { GroupedAlertsTable } from '../../../detections/components/alerts_table/alerts_grouping';
|
||||
|
@ -28,21 +32,21 @@ import { useSourcererDataView } from '../../../sourcerer/containers';
|
|||
import { SourcererScopeName } from '../../../sourcerer/store/model';
|
||||
import { RiskInformationButtonEmpty } from '../risk_information';
|
||||
|
||||
export interface TopRiskScoreContributorsAlertsProps {
|
||||
export interface TopRiskScoreContributorsAlertsProps<T extends EntityType> {
|
||||
toggleStatus: boolean;
|
||||
toggleQuery?: (status: boolean) => void;
|
||||
riskScore: HostRiskScore | UserRiskScore;
|
||||
riskEntity: RiskScoreEntity;
|
||||
riskScore: EntityRiskScore<T>;
|
||||
riskEntity: T;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export const TopRiskScoreContributorsAlerts: React.FC<TopRiskScoreContributorsAlertsProps> = ({
|
||||
export const TopRiskScoreContributorsAlerts = <T extends EntityType>({
|
||||
toggleStatus,
|
||||
toggleQuery,
|
||||
riskScore,
|
||||
riskEntity,
|
||||
loading,
|
||||
}) => {
|
||||
}: TopRiskScoreContributorsAlertsProps<T>) => {
|
||||
const { to, from } = useGlobalTime();
|
||||
const [{ loading: userInfoLoading, signalIndexName, hasIndexWrite, hasIndexMaintenance }] =
|
||||
useUserData();
|
||||
|
@ -57,8 +61,9 @@ export const TopRiskScoreContributorsAlerts: React.FC<TopRiskScoreContributorsAl
|
|||
const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector);
|
||||
|
||||
const inputFilters = useMemo(() => {
|
||||
// TODO Add support for services on a follow-up PR
|
||||
const riskScoreEntity =
|
||||
riskEntity === RiskScoreEntity.host
|
||||
riskEntity === EntityType.host
|
||||
? (riskScore as HostRiskScore).host
|
||||
: (riskScore as UserRiskScore).user;
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import React, { useEffect, useMemo, useState } from 'react';
|
|||
import { noop } from 'lodash/fp';
|
||||
|
||||
import { EuiPanel } from '@elastic/eui';
|
||||
import { EMPTY_SEVERITY_COUNT, EntityType } from '../../../common/search_strategy';
|
||||
import { useRiskScoreKpi } from '../api/hooks/use_risk_score_kpi';
|
||||
import { useRiskScore } from '../api/hooks/use_risk_score';
|
||||
import { UserRiskScoreQueryId } from '../common/utils';
|
||||
|
@ -20,7 +21,6 @@ import type { State } from '../../common/store';
|
|||
import { UserRiskScoreTable } from './user_risk_score_table';
|
||||
import { usersSelectors } from '../../explore/users/store';
|
||||
import { useQueryToggle } from '../../common/containers/query_toggle';
|
||||
import { EMPTY_SEVERITY_COUNT, RiskScoreEntity } from '../../../common/search_strategy';
|
||||
import { useMissingRiskEnginePrivileges } from '../hooks/use_missing_risk_engine_privileges';
|
||||
import { RiskEnginePrivilegesCallOut } from './risk_engine_privileges_callout';
|
||||
import { useUpsellingComponent } from '../../common/hooks/use_upselling';
|
||||
|
@ -70,7 +70,7 @@ export const UserRiskScoreQueryTabBody = ({
|
|||
useRiskScore({
|
||||
filterQuery,
|
||||
pagination,
|
||||
riskEntity: RiskScoreEntity.user,
|
||||
riskEntity: EntityType.user,
|
||||
skip: querySkip,
|
||||
sort,
|
||||
timerange,
|
||||
|
@ -78,7 +78,7 @@ export const UserRiskScoreQueryTabBody = ({
|
|||
|
||||
const { severityCount, loading: isKpiLoading } = useRiskScoreKpi({
|
||||
filterQuery,
|
||||
riskEntity: RiskScoreEntity.user,
|
||||
riskEntity: EntityType.user,
|
||||
skip: querySkip,
|
||||
});
|
||||
|
||||
|
@ -100,7 +100,7 @@ export const UserRiskScoreQueryTabBody = ({
|
|||
if (isDisabled) {
|
||||
return (
|
||||
<EuiPanel hasBorder>
|
||||
<EnableRiskScore isDisabled={isDisabled} entityType={RiskScoreEntity.host} />
|
||||
<EnableRiskScore isDisabled={isDisabled} entityType={EntityType.host} />
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
@ -111,7 +111,7 @@ export const UserRiskScoreQueryTabBody = ({
|
|||
data &&
|
||||
data.length === 0
|
||||
) {
|
||||
return <RiskScoresNoDataDetected entityType={RiskScoreEntity.user} />;
|
||||
return <RiskScoresNoDataDetected entityType={EntityType.user} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
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