mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Entity Analytics][9.0] Remove all legacy risk engine code and features (#201810)
This commit is contained in:
parent
d8b0b6e926
commit
80baa2cd9e
223 changed files with 454 additions and 15922 deletions
|
@ -378,7 +378,6 @@ module.exports = {
|
|||
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]entity_analytics[\/\\]components[\/\\]risk_details_tab_body[\/\\]index.tsx/,
|
||||
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]entity_analytics[\/\\]components[\/\\]risk_information[\/\\]index.tsx/,
|
||||
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]entity_analytics[\/\\]components[\/\\]risk_score_donut_chart[\/\\]index.tsx/,
|
||||
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]entity_analytics[\/\\]components[\/\\]risk_score_onboarding[\/\\]use_risk_score_toast_content.tsx/,
|
||||
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]entity_analytics[\/\\]components[\/\\]severity[\/\\]common[\/\\]index.tsx/,
|
||||
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]entity_analytics[\/\\]components[\/\\]severity[\/\\]severity_bar.tsx/,
|
||||
/x-pack[\/\\]solutions[\/\\]security[\/\\]plugins[\/\\]security_solution[\/\\]public[\/\\]entity_analytics[\/\\]components[\/\\]styled_basic_table.tsx/,
|
||||
|
|
|
@ -129,10 +129,6 @@ export const DATA_DATASETS_INDEX_PATTERNS = [
|
|||
|
||||
// meow attacks
|
||||
{ pattern: '*meow*', patternName: 'meow' },
|
||||
|
||||
// experimental ml
|
||||
{ pattern: 'ml_host_risk_score_latest_*', patternName: 'host_risk_score' },
|
||||
{ pattern: 'ml_user_risk_score_latest_*', patternName: 'user_risk_score' },
|
||||
] as const;
|
||||
|
||||
// Get the unique list of index patterns (some are duplicated for documentation purposes)
|
||||
|
|
|
@ -76,12 +76,6 @@ describe('get_data_telemetry', () => {
|
|||
{ name: 'some-random-logs', docCount: 100, sizeInBytes: 10 },
|
||||
{ name: 'my-prod-filebeat-123', docCount: 100, sizeInBytes: 10 },
|
||||
{ name: 'logs-endpoint.1234', docCount: 0 }, // Matching pattern with a dot in the name
|
||||
{ name: 'ml_host_risk_score_latest_default', docCount: 0 },
|
||||
{ name: 'ml_host_risk_score_latest', docCount: 0 }, // This should not match,
|
||||
{ name: 'ml_host_risk_score', docCount: 0 }, // This should not match
|
||||
{ name: 'ml_user_risk_score_latest_default', docCount: 0 },
|
||||
{ name: 'ml_user_risk_score_latest', docCount: 0 }, // This should not match,
|
||||
{ name: 'ml_user_risk_score', docCount: 0 }, // This should not match
|
||||
// New Indexing strategy: everything can be inferred from the constant_keyword values
|
||||
{
|
||||
name: '.ds-logs-nginx.access-default-000001',
|
||||
|
@ -199,16 +193,6 @@ describe('get_data_telemetry', () => {
|
|||
index_count: 1,
|
||||
doc_count: 0,
|
||||
},
|
||||
{
|
||||
pattern_name: 'host_risk_score',
|
||||
index_count: 1,
|
||||
doc_count: 0,
|
||||
},
|
||||
{
|
||||
pattern_name: 'user_risk_score',
|
||||
index_count: 1,
|
||||
doc_count: 0,
|
||||
},
|
||||
{
|
||||
data_stream: { dataset: 'nginx.access', type: 'logs' },
|
||||
shipper: 'filebeat',
|
||||
|
|
|
@ -34732,7 +34732,6 @@
|
|||
"xpack.securitySolution.alertCountByRuleByStatus.ruleName": "kibana.alert.rule.name",
|
||||
"xpack.securitySolution.alertCountByRuleByStatus.status": "Statut",
|
||||
"xpack.securitySolution.alertCountByRuleByStatus.tooltipTitle": "Nom de règle",
|
||||
"xpack.securitySolution.alertDetails.overview.hostRiskDataTitle": "Données de risque de {riskEntity}",
|
||||
"xpack.securitySolution.alertDetails.summary.readLess": "Lire moins",
|
||||
"xpack.securitySolution.alertDetails.summary.readMore": "En savoir plus",
|
||||
"xpack.securitySolution.alerts.badge.readOnly.tooltip": "Impossible de mettre à jour les alertes",
|
||||
|
@ -37194,7 +37193,6 @@
|
|||
"xpack.securitySolution.enableRiskScore.enableRiskScore": "Activer le score de risque de {riskEntity}",
|
||||
"xpack.securitySolution.enableRiskScore.enableRiskScoreDescription": "Une fois que vous avez activé cette fonctionnalité, vous pouvez obtenir un accès rapide aux scores de risque de {riskEntity} dans cette section. Les données pourront prendre jusqu'à une heure pour être générées après l'activation du module.",
|
||||
"xpack.securitySolution.enableRiskScore.enableRiskScorePopoverTitle": "Les alertes doivent être disponibles avant d'activer le module",
|
||||
"xpack.securitySolution.enableRiskScore.upgradeRiskScore": "Mettre à niveau le score de risque de {riskEntity}",
|
||||
"xpack.securitySolution.endpoint.action.chooseFromTheList": "Choisissez une action dans la liste",
|
||||
"xpack.securitySolution.endpoint.action.permissionDenied": "Autorisation refusée",
|
||||
"xpack.securitySolution.endpoint.actions.agentDetails": "Afficher les détails de l'agent",
|
||||
|
@ -38909,9 +38907,6 @@
|
|||
"xpack.securitySolution.hosts.navigaton.matrixHistogram.errorFetchingAuthenticationsData": "Impossible d'interroger les données d'authentifications",
|
||||
"xpack.securitySolution.hosts.navigaton.matrixHistogram.errorFetchingEventsData": "Impossible d'interroger les données d'événements",
|
||||
"xpack.securitySolution.hosts.pageTitle": "Hôtes",
|
||||
"xpack.securitySolution.hosts.topRiskScoreContributors.rankColumnTitle": "Rang",
|
||||
"xpack.securitySolution.hosts.topRiskScoreContributors.ruleNameColumnTitle": "Nom de règle",
|
||||
"xpack.securitySolution.hosts.topRiskScoreContributors.title": "Principaux contributeurs de score de risque",
|
||||
"xpack.securitySolution.hosts.topRiskScoreContributorsTable.title": "Principaux contributeurs de score de risque",
|
||||
"xpack.securitySolution.hostsRiskTable.filteredHostsTitle": "Afficher les hôtes à risque {severity}",
|
||||
"xpack.securitySolution.hostsRiskTable.hostNameTitle": "Nom d'hôte",
|
||||
|
@ -39798,8 +39793,6 @@
|
|||
"xpack.securitySolution.responseActionsList.list.status": "Statut",
|
||||
"xpack.securitySolution.responseActionsList.list.time": "Heure",
|
||||
"xpack.securitySolution.responseActionsList.list.user": "Utilisateur",
|
||||
"xpack.securitySolution.risk_score.toast.viewDashboard": "Afficher le tableau de bord",
|
||||
"xpack.securitySolution.riskDeprecated.entity.upgradeRiskScoreDescription": "Les données actuelles ne sont plus prises en charge. Veuillez migrer vos données et mettre à niveau le module. Les données pourront prendre jusqu'à une heure pour être générées après l'activation du module.",
|
||||
"xpack.securitySolution.riskEngine.missingPrivilegesCallOut.messageBody.clusterPrivilegesTitle": "Privilèges de cluster Elasticsearch manquants :",
|
||||
"xpack.securitySolution.riskEngine.missingPrivilegesCallOut.messageBody.essenceDescription": "Vous avez besoin des privilèges suivants pour accéder totalement à cette fonctionnalité. Contactez votre administrateur si vous avez besoin d'aide. En savoir plus sur {docs}.",
|
||||
"xpack.securitySolution.riskEngine.missingPrivilegesCallOut.messageBody.indexPrivilegesTitle": "Privilèges d'index Elasticsearch manquants :",
|
||||
|
@ -39830,16 +39823,6 @@
|
|||
"xpack.securitySolution.riskInformation.title": "Analyse de risque des entités",
|
||||
"xpack.securitySolution.riskInformation.unknownRiskDescription": "Inférieur à 20",
|
||||
"xpack.securitySolution.riskInformation.weightColumnHeader": "Pondération des risques par défaut",
|
||||
"xpack.securitySolution.riskScore.api.ingestPipeline.create.errorMessageTitle": "Impossible de créer un pipeline d'ingestion",
|
||||
"xpack.securitySolution.riskScore.api.ingestPipeline.delete.errorMessageTitle": "Impossible de supprimer {totalCount, plural, =1 {le pipeline} other {les pipelines}} d'ingestion",
|
||||
"xpack.securitySolution.riskScore.api.storedScript.create.errorMessageTitle": "Impossible de créer un script stocké",
|
||||
"xpack.securitySolution.riskScore.api.storedScript.delete.errorMessageTitle": "Impossible de supprimer un script stocké",
|
||||
"xpack.securitySolution.riskScore.api.transforms.create.errorMessageTitle": "Impossible de créer la transformation",
|
||||
"xpack.securitySolution.riskScore.api.transforms.delete.errorMessageTitle": "Impossible de supprimer {totalCount, plural, =1 {la transformation} other {les transformations}}",
|
||||
"xpack.securitySolution.riskScore.api.transforms.getState.errorMessageTitle": "Impossible d'obtenir l'état de transformation",
|
||||
"xpack.securitySolution.riskScore.api.transforms.getState.notFoundMessageTitle": "Transformation introuvable",
|
||||
"xpack.securitySolution.riskScore.api.transforms.start.errorMessageTitle": "Impossible de démarrer {totalCount, plural, =1 {la transformation} other {les transformations}}",
|
||||
"xpack.securitySolution.riskScore.api.transforms.stop.errorMessageTitle": "Impossible d'arrêter {totalCount, plural, =1 {la transformation} other {les transformations}}",
|
||||
"xpack.securitySolution.riskScore.enableButtonTitle": "Activer",
|
||||
"xpack.securitySolution.riskScore.errorPanel.errors": "Erreurs",
|
||||
"xpack.securitySolution.riskScore.errorPanel.message": "Le statut du moteur à risque n'a pas pu être changé. Résoudre les problèmes suivants et réessayer :",
|
||||
|
@ -39847,13 +39830,9 @@
|
|||
"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.hostRiskScoresEnabledTitle": "Scores de risque de l'hôte activés",
|
||||
"xpack.securitySolution.riskScore.hostsDashboardWarningPanelBody": "Nous n’avons pas trouvé de données de score de risque de l’hôte. Vérifiez si vous avez des filtres globaux dans la barre de recherche KQL globale. Si vous venez d’activer le module de risque de l’hôte, le moteur de risque peut mettre une heure à générer les données de score de risque de l’hôte et les afficher dans ce panneau.",
|
||||
"xpack.securitySolution.riskScore.hostsDashboardWarningPanelTitle": "Aucune donnée de score de risque de l'hôte disponible pour l'affichage",
|
||||
"xpack.securitySolution.riskScore.install.errorMessageTitle": "Erreur d'installation",
|
||||
"xpack.securitySolution.riskScore.kpi.failSearchDescription": "Impossible de lancer une recherche sur le score de risque",
|
||||
"xpack.securitySolution.riskScore.maxSpacePanel.message": "Vous pouvez désactiver l'évaluation de l'entité dans l'espace où elle est actuellement activée avant de l'activer dans cet espace",
|
||||
"xpack.securitySolution.riskScore.maxSpacePanel.title": "Vous ne pouvez pas autoriser le score de risque dans plus de {maxSpaces, plural, =1 {# espace Kibana} other {# espaces Kibana}}.",
|
||||
"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é",
|
||||
"xpack.securitySolution.riskScore.overview.alerts": "Alertes",
|
||||
|
@ -39868,11 +39847,8 @@
|
|||
"xpack.securitySolution.riskScore.previewTable.levelColumnTitle": "Niveau",
|
||||
"xpack.securitySolution.riskScore.previewTable.nameColumnTitle": "Nom",
|
||||
"xpack.securitySolution.riskScore.previewTable.scoreNormColumnTitle": "Norme de score",
|
||||
"xpack.securitySolution.riskScore.restartButtonTitle": "Redémarrer",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.eaDocsDashboard": "Tableau de bord d'analyse des entités",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.eaDocsEntities": "Comment le score de risque est-il calculé ?",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.eaDocsHosts": "Score de risque de l'hôte",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.eaDocsUsers": "Score de risque de l'utilisateur",
|
||||
"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",
|
||||
|
@ -39890,35 +39866,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.savedObjects.bulkCreateFailureTitle": "Impossible d’importer les objets enregistrés",
|
||||
"xpack.securitySolution.riskScore.savedObjects.bulkCreateSuccessTitle": "{totalCount} {totalCount, plural, =1 {objet enregistré importé} other {objets enregistrés importés}}",
|
||||
"xpack.securitySolution.riskScore.savedObjects.bulkDeleteFailureTitle": "Impossible de supprimer les objets enregistrés",
|
||||
"xpack.securitySolution.riskScore.savedObjects.enableRiskScoreSuccessTitle": "{items} ont bien été importés",
|
||||
"xpack.securitySolution.riskScore.savedObjects.failedToCreateTagTitle": "Impossible d'importer les objets enregistrés : {savedObjectTemplate} n'a pas été créé, car la balise n'a pas pu être créée : {tagName}",
|
||||
"xpack.securitySolution.riskScore.savedObjects.failedToFindTagTitle": "Impossible d'importer les objets enregistrés : {savedObjectTemplate} n'a pas été créé, car la balise n'a pas pu être trouvée : {tagName}",
|
||||
"xpack.securitySolution.riskScore.savedObjects.templateAlreadyExistsTitle": "Impossible d'importer les objets enregistrés : {savedObjectTemplate} n'a pas été créé, car il existe déjà",
|
||||
"xpack.securitySolution.riskScore.savedObjects.templateNotFoundTitle": "Impossible d'importer les objets enregistrés : {savedObjectTemplate} n'a pas été créé, car le modèle n'a pas été trouvé",
|
||||
"xpack.securitySolution.riskScore.startUpdate": "Lancer la mise à jour",
|
||||
"xpack.securitySolution.riskScore.technicalPreviewLabel": "Bêta",
|
||||
"xpack.securitySolution.riskScore.transform.notFoundTitle": "Impossible de vérifier l'état de transformation, car {transformId} n'a pas été trouvé",
|
||||
"xpack.securitySolution.riskScore.transform.start.stateConflictTitle": "Impossible de démarrer la transformation {transformId}, car son état est : {state}",
|
||||
"xpack.securitySolution.riskScore.transform.transformExistsTitle": "Impossible de créer la transformation, car {transformId} existe déjà",
|
||||
"xpack.securitySolution.riskScore.uninstall.errorMessageTitle": "Erreur de désinstallation",
|
||||
"xpack.securitySolution.riskScore.updateAvailable": "Mise à jour disponible",
|
||||
"xpack.securitySolution.riskScore.updatePanel.Dismiss": "Rejeter",
|
||||
"xpack.securitySolution.riskScore.updatePanel.goToManage": "Gérer",
|
||||
"xpack.securitySolution.riskScore.updatePanel.message": "Un nouveau moteur d'évaluation du risque des entités est disponible. Mettre à jour maintenant pour obtenir les dernières fonctionnalités.",
|
||||
"xpack.securitySolution.riskScore.updatePanel.title": "Nouveau moteur d'évaluation du risque des entités disponible",
|
||||
"xpack.securitySolution.riskScore.updateRiskEngineModa.title": "Voulez-vous mettre à jour le moteur de risque des entités ?",
|
||||
"xpack.securitySolution.riskScore.updateRiskEngineModal.buttonNo": "Non, pas maintenant",
|
||||
"xpack.securitySolution.riskScore.updateRiskEngineModal.buttonYes": "Oui, faire la mise à jour maintenant",
|
||||
"xpack.securitySolution.riskScore.updateRiskEngineModal.existingData_1": "Les données héritées de score de risque ne seront pas supprimées,",
|
||||
"xpack.securitySolution.riskScore.updateRiskEngineModal.existingData_2": "elles seront toujours présentes dans l'index, mais sans être disponibles dans l'interface utilisateur. Il vous faudra retirer manuellement les données héritées de score de risque.",
|
||||
"xpack.securitySolution.riskScore.updateRiskEngineModal.existingUserHost_1": "Les transformations existantes de score de risque de l'utilisateur et de l'hôte seront supprimées,",
|
||||
"xpack.securitySolution.riskScore.updateRiskEngineModal.existingUserHost_2": "puisqu'elles ne seront plus nécessaires.",
|
||||
"xpack.securitySolution.riskScore.updatingRiskEngine": "Mise à jour du moteur de risque...",
|
||||
"xpack.securitySolution.riskScore.userRiskScoresEnabledTitle": "Scores de risque de l'utilisateur activés",
|
||||
"xpack.securitySolution.riskScore.usersDashboardRestartTooltip": "Le calcul du score de risque pourra prendre un certain temps à se lancer. Cependant, en appuyant sur Redémarrer, vous pouvez le forcer à s'exécuter immédiatement.",
|
||||
"xpack.securitySolution.riskScore.usersDashboardWarningPanelBody": "Nous n’avons pas trouvé de données de score de risque de l’utilisateur. Vérifiez si vous avez des filtres globaux dans la barre de recherche KQL globale. Si vous venez d’activer le module de risque de l’utilisateur, le moteur de risque peut mettre une heure à générer les données de score de risque de l’utilisateur et à les afficher dans ce panneau.",
|
||||
"xpack.securitySolution.riskScore.usersDashboardWarningPanelTitle": "Aucune donnée de score de risque de l'utilisateur disponible pour l'affichage",
|
||||
"xpack.securitySolution.riskTabBody.scoreOverTimeTitle": "Score de risque de {riskEntity} sur la durée",
|
||||
|
|
|
@ -34593,7 +34593,6 @@
|
|||
"xpack.securitySolution.alertCountByRuleByStatus.ruleName": "kibana.alert.rule.name",
|
||||
"xpack.securitySolution.alertCountByRuleByStatus.status": "ステータス",
|
||||
"xpack.securitySolution.alertCountByRuleByStatus.tooltipTitle": "ルール名",
|
||||
"xpack.securitySolution.alertDetails.overview.hostRiskDataTitle": "{riskEntity}リスクデータ",
|
||||
"xpack.securitySolution.alertDetails.summary.readLess": "表示を減らす",
|
||||
"xpack.securitySolution.alertDetails.summary.readMore": "続きを読む",
|
||||
"xpack.securitySolution.alerts.badge.readOnly.tooltip": "アラートを更新できません",
|
||||
|
@ -37053,7 +37052,6 @@
|
|||
"xpack.securitySolution.enableRiskScore.enableRiskScore": "{riskEntity}リスクスコアを有効化",
|
||||
"xpack.securitySolution.enableRiskScore.enableRiskScoreDescription": "この機能を有効化すると、このセクションで{riskEntity}リスクスコアにすばやくアクセスできます。モジュールを有効化した後、データの生成までに1時間かかる場合があります。",
|
||||
"xpack.securitySolution.enableRiskScore.enableRiskScorePopoverTitle": "モジュールを有効にする前に、アラートが使用可能でなければなりません",
|
||||
"xpack.securitySolution.enableRiskScore.upgradeRiskScore": "{riskEntity}リスクスコアをアップグレード",
|
||||
"xpack.securitySolution.endpoint.action.chooseFromTheList": "リストからアクションを選択",
|
||||
"xpack.securitySolution.endpoint.action.permissionDenied": "パーミッションが拒否されました",
|
||||
"xpack.securitySolution.endpoint.actions.agentDetails": "エージェント詳細を表示",
|
||||
|
@ -38768,9 +38766,6 @@
|
|||
"xpack.securitySolution.hosts.navigaton.matrixHistogram.errorFetchingAuthenticationsData": "認証データをクエリできませんでした",
|
||||
"xpack.securitySolution.hosts.navigaton.matrixHistogram.errorFetchingEventsData": "イベントデータをクエリできませんでした",
|
||||
"xpack.securitySolution.hosts.pageTitle": "ホスト",
|
||||
"xpack.securitySolution.hosts.topRiskScoreContributors.rankColumnTitle": "ランク",
|
||||
"xpack.securitySolution.hosts.topRiskScoreContributors.ruleNameColumnTitle": "ルール名",
|
||||
"xpack.securitySolution.hosts.topRiskScoreContributors.title": "上位のリスクスコアの要因",
|
||||
"xpack.securitySolution.hosts.topRiskScoreContributorsTable.title": "上位のリスクスコアの要因",
|
||||
"xpack.securitySolution.hostsRiskTable.filteredHostsTitle": "{severity}のリスクがあるホストを表示",
|
||||
"xpack.securitySolution.hostsRiskTable.hostNameTitle": "ホスト名",
|
||||
|
@ -39658,8 +39653,6 @@
|
|||
"xpack.securitySolution.responseActionsList.list.status": "ステータス",
|
||||
"xpack.securitySolution.responseActionsList.list.time": "時間",
|
||||
"xpack.securitySolution.responseActionsList.list.user": "ユーザー",
|
||||
"xpack.securitySolution.risk_score.toast.viewDashboard": "ダッシュボードを表示",
|
||||
"xpack.securitySolution.riskDeprecated.entity.upgradeRiskScoreDescription": "現在のデータはサポートされていません。データを移行し、モジュールをアップグレードしてください。モジュールを有効化した後、データの生成までに1時間かかる場合があります。",
|
||||
"xpack.securitySolution.riskEngine.missingPrivilegesCallOut.messageBody.clusterPrivilegesTitle": "不足しているElasticsearchクラスター権限:",
|
||||
"xpack.securitySolution.riskEngine.missingPrivilegesCallOut.messageBody.essenceDescription": "この機能のすべてにアクセスするには、次の権限が必要です。サポートについては、管理者にお問い合わせください。{docs}の詳細をお読みください。",
|
||||
"xpack.securitySolution.riskEngine.missingPrivilegesCallOut.messageBody.indexPrivilegesTitle": "Elasticsearchインデックス権限がありません。",
|
||||
|
@ -39690,16 +39683,6 @@
|
|||
"xpack.securitySolution.riskInformation.title": "Entity Risk Analytics",
|
||||
"xpack.securitySolution.riskInformation.unknownRiskDescription": "20未満",
|
||||
"xpack.securitySolution.riskInformation.weightColumnHeader": "デフォルトリスクウィジェット",
|
||||
"xpack.securitySolution.riskScore.api.ingestPipeline.create.errorMessageTitle": "インジェストパイプラインを作成できませんでした",
|
||||
"xpack.securitySolution.riskScore.api.ingestPipeline.delete.errorMessageTitle": "インジェスト{totalCount, plural, other {パイプライン}}を削除できませんでした",
|
||||
"xpack.securitySolution.riskScore.api.storedScript.create.errorMessageTitle": "保存されたスクリプトを作成できませんでした",
|
||||
"xpack.securitySolution.riskScore.api.storedScript.delete.errorMessageTitle": "保存されたスクリプトを削除できませんでした",
|
||||
"xpack.securitySolution.riskScore.api.transforms.create.errorMessageTitle": "トランスフォームを作成できませんでした",
|
||||
"xpack.securitySolution.riskScore.api.transforms.delete.errorMessageTitle": "{totalCount, plural, other {トランスフォーム}}を削除できませんでした",
|
||||
"xpack.securitySolution.riskScore.api.transforms.getState.errorMessageTitle": "トランスフォーム状態を取得できませんでした",
|
||||
"xpack.securitySolution.riskScore.api.transforms.getState.notFoundMessageTitle": "トランスフォームが見つかりません",
|
||||
"xpack.securitySolution.riskScore.api.transforms.start.errorMessageTitle": "{totalCount, plural, other {トランスフォーム}}を開始できませんでした",
|
||||
"xpack.securitySolution.riskScore.api.transforms.stop.errorMessageTitle": "{totalCount, plural, other {トランスフォーム}}を停止できませんでした",
|
||||
"xpack.securitySolution.riskScore.enableButtonTitle": "有効にする",
|
||||
"xpack.securitySolution.riskScore.errorPanel.errors": "エラー",
|
||||
"xpack.securitySolution.riskScore.errorPanel.message": "リスクエンジンステータスを変更できませんでした。次の項目を修正して、再試行してください。",
|
||||
|
@ -39707,13 +39690,9 @@
|
|||
"xpack.securitySolution.riskScore.errors.privileges.check": "権限を確認",
|
||||
"xpack.securitySolution.riskScore.errors.privileges.needToHave": "次の項目が必要です。",
|
||||
"xpack.securitySolution.riskScore.failSearchDescription": "リスクスコアで検索を実行できませんでした",
|
||||
"xpack.securitySolution.riskScore.hostRiskScoresEnabledTitle": "ホストリスクスコア有効",
|
||||
"xpack.securitySolution.riskScore.hostsDashboardWarningPanelBody": "ホストリスクスコアデータが見つかりません。グローバルKQL検索バーにグローバルフィルターがあるかどうかを確認してください。ホストリスクモジュールを有効にしたばかりの場合は、リスクエンジンがホストリスクスコアデータを生成し、このパネルに表示するまでに1時間かかることがあります。",
|
||||
"xpack.securitySolution.riskScore.hostsDashboardWarningPanelTitle": "表示するホストリスクスコアデータがありません",
|
||||
"xpack.securitySolution.riskScore.install.errorMessageTitle": "インストールエラー",
|
||||
"xpack.securitySolution.riskScore.kpi.failSearchDescription": "リスクスコアで検索を実行できませんでした",
|
||||
"xpack.securitySolution.riskScore.maxSpacePanel.message": "このスペースで有効化する前に、現在有効なスペースでエンティティリスクスコアリングを無効化できます。",
|
||||
"xpack.securitySolution.riskScore.maxSpacePanel.title": "エンティティリスクスコアリングを有効にできるのは、{maxSpaces, plural, other {# 個のKibanaスペース}}までです。",
|
||||
"xpack.securitySolution.riskScore.moduleTurnedOff": "エンティティリスクスコアがオフになりました",
|
||||
"xpack.securitySolution.riskScore.moduleTurnedOn": "エンティティリスクスコアがオンになりました",
|
||||
"xpack.securitySolution.riskScore.overview.alerts": "アラート",
|
||||
|
@ -39728,11 +39707,8 @@
|
|||
"xpack.securitySolution.riskScore.previewTable.levelColumnTitle": "レベル",
|
||||
"xpack.securitySolution.riskScore.previewTable.nameColumnTitle": "名前",
|
||||
"xpack.securitySolution.riskScore.previewTable.scoreNormColumnTitle": "スコア基準",
|
||||
"xpack.securitySolution.riskScore.restartButtonTitle": "再起動",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.eaDocsDashboard": "エンティティ分析ダッシュボード",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.eaDocsEntities": "リスクスコアを計算する方法",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.eaDocsHosts": "ホストリスクスコア",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.eaDocsUsers": "ユーザーリスクスコア",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.entityRiskScoring": "エンティティリスクスコア",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.errorMessage": "プレビューを作成しているときに問題が発生しました。再試行してください。",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.errorTitle": "プレビューが失敗しました",
|
||||
|
@ -39750,35 +39726,6 @@
|
|||
"xpack.securitySolution.riskScore.riskScorePreview.usefulLinks": "便利なリンク",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.users.hide": "ユーザーを非表示",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.users.show": "ユーザーを表示",
|
||||
"xpack.securitySolution.riskScore.savedObjects.bulkCreateFailureTitle": "保存されたオブジェクトをインポートできませんでした",
|
||||
"xpack.securitySolution.riskScore.savedObjects.bulkCreateSuccessTitle": "{totalCount} {totalCount, plural, other {個の保存されたオブジェクト}}が正常にインポートされました",
|
||||
"xpack.securitySolution.riskScore.savedObjects.bulkDeleteFailureTitle": "保存されたオブジェクトを削除できませんでした",
|
||||
"xpack.securitySolution.riskScore.savedObjects.enableRiskScoreSuccessTitle": "{items}が正常にインポートされました",
|
||||
"xpack.securitySolution.riskScore.savedObjects.failedToCreateTagTitle": "保存されたオブジェクトをインポートできませんでした:タグ{tagName}を作成できなかったため、{savedObjectTemplate}が作成されませんでした",
|
||||
"xpack.securitySolution.riskScore.savedObjects.failedToFindTagTitle": "保存されたオブジェクトをインポートできませんでした:タグ{tagName}を検出できなかったため、{savedObjectTemplate}が作成されませんでした",
|
||||
"xpack.securitySolution.riskScore.savedObjects.templateAlreadyExistsTitle": "保存されたオブジェクトをインポートできませんでした:{savedObjectTemplate}はすでに存在するため、作成されませんでした",
|
||||
"xpack.securitySolution.riskScore.savedObjects.templateNotFoundTitle": "保存されたオブジェクトをインポートできませんでした:{savedObjectTemplate}が見つからないため、作成されませんでした",
|
||||
"xpack.securitySolution.riskScore.startUpdate": "更新を開始",
|
||||
"xpack.securitySolution.riskScore.technicalPreviewLabel": "ベータ",
|
||||
"xpack.securitySolution.riskScore.transform.notFoundTitle": "{transformId}が見つからないため、トランスフォームを確認できませんでした",
|
||||
"xpack.securitySolution.riskScore.transform.start.stateConflictTitle": "状態が\"{state}\"のため、トランスフォーム{transformId}が開始しません",
|
||||
"xpack.securitySolution.riskScore.transform.transformExistsTitle": "{transformId}がすでに存在するため、トランスフォームを作成できませんでした",
|
||||
"xpack.securitySolution.riskScore.uninstall.errorMessageTitle": "アンインストールエラー",
|
||||
"xpack.securitySolution.riskScore.updateAvailable": "更新が利用可能です",
|
||||
"xpack.securitySolution.riskScore.updatePanel.Dismiss": "閉じる",
|
||||
"xpack.securitySolution.riskScore.updatePanel.goToManage": "管理",
|
||||
"xpack.securitySolution.riskScore.updatePanel.message": "新しいエンティティリスクスコアリングエンジンが利用可能です。今すぐ更新して最新機能をご利用ください。",
|
||||
"xpack.securitySolution.riskScore.updatePanel.title": "新しいエンティティリスクスコアリングエンジンが利用可能です",
|
||||
"xpack.securitySolution.riskScore.updateRiskEngineModa.title": "エンティティリスクエンジンを更新しますか?",
|
||||
"xpack.securitySolution.riskScore.updateRiskEngineModal.buttonNo": "いいえ。今はしません",
|
||||
"xpack.securitySolution.riskScore.updateRiskEngineModal.buttonYes": "はい。今すぐ更新します!",
|
||||
"xpack.securitySolution.riskScore.updateRiskEngineModal.existingData_1": "レガシーリスクスコアデータは削除されません。",
|
||||
"xpack.securitySolution.riskScore.updateRiskEngineModal.existingData_2": "インデックスには存在しますが、ユーザーインターフェースでは利用できなくなります。レガシーリスクスコアデータは手動で削除する必要があります。",
|
||||
"xpack.securitySolution.riskScore.updateRiskEngineModal.existingUserHost_1": "既存のユーザーとホストのリスクスコア変換は",
|
||||
"xpack.securitySolution.riskScore.updateRiskEngineModal.existingUserHost_2": "必要がないため、削除されます。",
|
||||
"xpack.securitySolution.riskScore.updatingRiskEngine": "リスクエンジンを更新中...",
|
||||
"xpack.securitySolution.riskScore.userRiskScoresEnabledTitle": "ユーザーリスクスコア有効",
|
||||
"xpack.securitySolution.riskScore.usersDashboardRestartTooltip": "リスクスコア計算の実行には少し時間がかかる場合があります。ただし、再起動を押すと、すぐに強制的に実行できます。",
|
||||
"xpack.securitySolution.riskScore.usersDashboardWarningPanelBody": "ユーザーリスクスコアデータが見つかりません。グローバルKQL検索バーにグローバルフィルターがあるかどうかを確認してください。ユーザーリスクモジュールを有効にしたばかりの場合は、リスクエンジンがユーザーリスクスコアデータを生成し、このパネルに表示するまでに1時間かかることがあります。",
|
||||
"xpack.securitySolution.riskScore.usersDashboardWarningPanelTitle": "表示するユーザーリスクスコアデータがありません",
|
||||
"xpack.securitySolution.riskTabBody.scoreOverTimeTitle": "経時的な{riskEntity}リスクスコア",
|
||||
|
|
|
@ -34058,7 +34058,6 @@
|
|||
"xpack.securitySolution.alertCountByRuleByStatus.ruleName": "kibana.alert.rule.name",
|
||||
"xpack.securitySolution.alertCountByRuleByStatus.status": "状态",
|
||||
"xpack.securitySolution.alertCountByRuleByStatus.tooltipTitle": "规则名称",
|
||||
"xpack.securitySolution.alertDetails.overview.hostRiskDataTitle": "{riskEntity}风险数据",
|
||||
"xpack.securitySolution.alertDetails.summary.readLess": "阅读更少内容",
|
||||
"xpack.securitySolution.alertDetails.summary.readMore": "阅读更多内容",
|
||||
"xpack.securitySolution.alerts.badge.readOnly.tooltip": "无法更新告警",
|
||||
|
@ -36506,7 +36505,6 @@
|
|||
"xpack.securitySolution.enableRiskScore.enableRiskScore": "启用{riskEntity}风险分数",
|
||||
"xpack.securitySolution.enableRiskScore.enableRiskScoreDescription": "一旦启用此功能,您将可以在此部分快速访问{riskEntity}风险分数。启用此模板后,可能需要一小时才能生成数据。",
|
||||
"xpack.securitySolution.enableRiskScore.enableRiskScorePopoverTitle": "启用模块之前,告警需要处于可用状态",
|
||||
"xpack.securitySolution.enableRiskScore.upgradeRiskScore": "升级{riskEntity}风险分数",
|
||||
"xpack.securitySolution.endpoint.action.chooseFromTheList": "从列表中选择操作",
|
||||
"xpack.securitySolution.endpoint.action.permissionDenied": "权限被拒绝",
|
||||
"xpack.securitySolution.endpoint.actions.agentDetails": "查看代理详情",
|
||||
|
@ -38193,9 +38191,6 @@
|
|||
"xpack.securitySolution.hosts.navigaton.matrixHistogram.errorFetchingAuthenticationsData": "无法查询身份验证数据",
|
||||
"xpack.securitySolution.hosts.navigaton.matrixHistogram.errorFetchingEventsData": "无法查询事件数据",
|
||||
"xpack.securitySolution.hosts.pageTitle": "主机",
|
||||
"xpack.securitySolution.hosts.topRiskScoreContributors.rankColumnTitle": "排名",
|
||||
"xpack.securitySolution.hosts.topRiskScoreContributors.ruleNameColumnTitle": "规则名称",
|
||||
"xpack.securitySolution.hosts.topRiskScoreContributors.title": "风险分数主要因素",
|
||||
"xpack.securitySolution.hosts.topRiskScoreContributorsTable.title": "风险分数主要因素",
|
||||
"xpack.securitySolution.hostsRiskTable.filteredHostsTitle": "查看{severity}风险主机",
|
||||
"xpack.securitySolution.hostsRiskTable.hostNameTitle": "主机名",
|
||||
|
@ -39079,8 +39074,6 @@
|
|||
"xpack.securitySolution.responseActionsList.list.status": "状态",
|
||||
"xpack.securitySolution.responseActionsList.list.time": "时间",
|
||||
"xpack.securitySolution.responseActionsList.list.user": "用户",
|
||||
"xpack.securitySolution.risk_score.toast.viewDashboard": "查看仪表板",
|
||||
"xpack.securitySolution.riskDeprecated.entity.upgradeRiskScoreDescription": "当前数据不再受支持。请迁移您的数据并升级该模块。启用此模板后,可能需要一小时才能生成数据。",
|
||||
"xpack.securitySolution.riskEngine.missingPrivilegesCallOut.messageBody.clusterPrivilegesTitle": "缺少 Elasticsearch 集群权限:",
|
||||
"xpack.securitySolution.riskEngine.missingPrivilegesCallOut.messageBody.essenceDescription": "您需要以下权限,才能完全使用此功能。有关进一步帮助,请联系您的管理员。阅读有关 {docs} 的更多内容。",
|
||||
"xpack.securitySolution.riskEngine.missingPrivilegesCallOut.messageBody.indexPrivilegesTitle": "缺少 Elasticsearch 索引权限:",
|
||||
|
@ -39111,16 +39104,6 @@
|
|||
"xpack.securitySolution.riskInformation.title": "实体风险分析",
|
||||
"xpack.securitySolution.riskInformation.unknownRiskDescription": "小于 20",
|
||||
"xpack.securitySolution.riskInformation.weightColumnHeader": "默认风险权重",
|
||||
"xpack.securitySolution.riskScore.api.ingestPipeline.create.errorMessageTitle": "无法创建采集管道",
|
||||
"xpack.securitySolution.riskScore.api.ingestPipeline.delete.errorMessageTitle": "无法删除采集{totalCount, plural, other {管道}}",
|
||||
"xpack.securitySolution.riskScore.api.storedScript.create.errorMessageTitle": "无法创建存储脚本",
|
||||
"xpack.securitySolution.riskScore.api.storedScript.delete.errorMessageTitle": "无法删除存储脚本",
|
||||
"xpack.securitySolution.riskScore.api.transforms.create.errorMessageTitle": "无法创建转换",
|
||||
"xpack.securitySolution.riskScore.api.transforms.delete.errorMessageTitle": "无法删除{totalCount, plural, other {转换}}",
|
||||
"xpack.securitySolution.riskScore.api.transforms.getState.errorMessageTitle": "无法获取转换状态",
|
||||
"xpack.securitySolution.riskScore.api.transforms.getState.notFoundMessageTitle": "找不到转换",
|
||||
"xpack.securitySolution.riskScore.api.transforms.start.errorMessageTitle": "无法启动{totalCount, plural, other {转换}}",
|
||||
"xpack.securitySolution.riskScore.api.transforms.stop.errorMessageTitle": "无法停止{totalCount, plural, other {转换}}",
|
||||
"xpack.securitySolution.riskScore.enableButtonTitle": "启用",
|
||||
"xpack.securitySolution.riskScore.errorPanel.errors": "错误",
|
||||
"xpack.securitySolution.riskScore.errorPanel.message": "无法更改风险引擎状态。修复以下问题,然后重试:",
|
||||
|
@ -39128,13 +39111,9 @@
|
|||
"xpack.securitySolution.riskScore.errors.privileges.check": "检查权限",
|
||||
"xpack.securitySolution.riskScore.errors.privileges.needToHave": "您需要具有:",
|
||||
"xpack.securitySolution.riskScore.failSearchDescription": "无法对风险分数执行搜索",
|
||||
"xpack.securitySolution.riskScore.hostRiskScoresEnabledTitle": "已启用主机风险分数",
|
||||
"xpack.securitySolution.riskScore.hostsDashboardWarningPanelBody": "找不到任何主机风险分数数据。检查全局 KQL 搜索栏中是否具有任何全局筛选。如果刚刚启用了主机风险模块,风险引擎可能需要一小时才能生成并在此面板中显示主机风险分数数据。",
|
||||
"xpack.securitySolution.riskScore.hostsDashboardWarningPanelTitle": "没有可显示的主机风险分数数据",
|
||||
"xpack.securitySolution.riskScore.install.errorMessageTitle": "安装错误",
|
||||
"xpack.securitySolution.riskScore.kpi.failSearchDescription": "无法对风险分数执行搜索",
|
||||
"xpack.securitySolution.riskScore.maxSpacePanel.message": "在此工作区中启用实体风险评分之前,您可以在当前已启用实体风险评分的工作区中将其禁用",
|
||||
"xpack.securitySolution.riskScore.maxSpacePanel.title": "无法在超过 {maxSpaces, plural, other {# 个 Kibana 工作区}}中启用实体风险评分。",
|
||||
"xpack.securitySolution.riskScore.moduleTurnedOff": "已关闭实体风险分数",
|
||||
"xpack.securitySolution.riskScore.moduleTurnedOn": "已打开实体风险分数",
|
||||
"xpack.securitySolution.riskScore.overview.alerts": "告警",
|
||||
|
@ -39149,11 +39128,8 @@
|
|||
"xpack.securitySolution.riskScore.previewTable.levelColumnTitle": "级别",
|
||||
"xpack.securitySolution.riskScore.previewTable.nameColumnTitle": "名称",
|
||||
"xpack.securitySolution.riskScore.previewTable.scoreNormColumnTitle": "评分标准",
|
||||
"xpack.securitySolution.riskScore.restartButtonTitle": "重新启动",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.eaDocsDashboard": "实体分析仪表板",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.eaDocsEntities": "如何计算风险分数?",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.eaDocsHosts": "主机风险分数",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.eaDocsUsers": "用户风险分数",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.entityRiskScoring": "实体风险分数",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.errorMessage": "创建预览时出现了问题。请重试。",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.errorTitle": "预览失败",
|
||||
|
@ -39171,35 +39147,6 @@
|
|||
"xpack.securitySolution.riskScore.riskScorePreview.usefulLinks": "有用的链接",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.users.hide": "隐藏用户",
|
||||
"xpack.securitySolution.riskScore.riskScorePreview.users.show": "显示用户",
|
||||
"xpack.securitySolution.riskScore.savedObjects.bulkCreateFailureTitle": "无法导入已保存对象",
|
||||
"xpack.securitySolution.riskScore.savedObjects.bulkCreateSuccessTitle": "已成功导入 {totalCount} 个{totalCount, plural, other {已保存对象}}",
|
||||
"xpack.securitySolution.riskScore.savedObjects.bulkDeleteFailureTitle": "无法删除已保存对象",
|
||||
"xpack.securitySolution.riskScore.savedObjects.enableRiskScoreSuccessTitle": "已成功导入 {items}",
|
||||
"xpack.securitySolution.riskScore.savedObjects.failedToCreateTagTitle": "无法导入已保存对象:未创建 {savedObjectTemplate},因为无法创建标签:{tagName}",
|
||||
"xpack.securitySolution.riskScore.savedObjects.failedToFindTagTitle": "无法导入已保存对象:未创建 {savedObjectTemplate},因为找不到标签:{tagName}",
|
||||
"xpack.securitySolution.riskScore.savedObjects.templateAlreadyExistsTitle": "无法导入已保存对象:未创建 {savedObjectTemplate},因为其已存在",
|
||||
"xpack.securitySolution.riskScore.savedObjects.templateNotFoundTitle": "无法导入已保存对象:未创建 {savedObjectTemplate},因为找不到模板",
|
||||
"xpack.securitySolution.riskScore.startUpdate": "开始更新",
|
||||
"xpack.securitySolution.riskScore.technicalPreviewLabel": "公测版",
|
||||
"xpack.securitySolution.riskScore.transform.notFoundTitle": "无法检查转换状态,因为找不到 {transformId}",
|
||||
"xpack.securitySolution.riskScore.transform.start.stateConflictTitle": "未启动转换 {transformId},因为其状态为:{state}",
|
||||
"xpack.securitySolution.riskScore.transform.transformExistsTitle": "无法创建转换,因为 {transformId} 已存在",
|
||||
"xpack.securitySolution.riskScore.uninstall.errorMessageTitle": "卸载错误",
|
||||
"xpack.securitySolution.riskScore.updateAvailable": "有可用更新",
|
||||
"xpack.securitySolution.riskScore.updatePanel.Dismiss": "关闭",
|
||||
"xpack.securitySolution.riskScore.updatePanel.goToManage": "管理",
|
||||
"xpack.securitySolution.riskScore.updatePanel.message": "有新的实体风险评分引擎可用。立即进行更新以获取最新功能。",
|
||||
"xpack.securitySolution.riskScore.updatePanel.title": "新的实体风险评分引擎可用",
|
||||
"xpack.securitySolution.riskScore.updateRiskEngineModa.title": "是否要更新实体风险引擎?",
|
||||
"xpack.securitySolution.riskScore.updateRiskEngineModal.buttonNo": "否,暂不更新",
|
||||
"xpack.securitySolution.riskScore.updateRiskEngineModal.buttonYes": "是,立即更新!",
|
||||
"xpack.securitySolution.riskScore.updateRiskEngineModal.existingData_1": "将不会删除旧版风险分数数据",
|
||||
"xpack.securitySolution.riskScore.updateRiskEngineModal.existingData_2": ",它将仍然存在于索引中,但在用户界面中不再可用。您需要手动移除旧版风险分数数据。",
|
||||
"xpack.securitySolution.riskScore.updateRiskEngineModal.existingUserHost_1": "将删除现有用户和主机风险分数转换",
|
||||
"xpack.securitySolution.riskScore.updateRiskEngineModal.existingUserHost_2": ",因为不再需要它们。",
|
||||
"xpack.securitySolution.riskScore.updatingRiskEngine": "正在更新风险引擎......",
|
||||
"xpack.securitySolution.riskScore.userRiskScoresEnabledTitle": "已启用用户风险分数",
|
||||
"xpack.securitySolution.riskScore.usersDashboardRestartTooltip": "风险分数计算可能需要一段时间运行。但是,通过按'重新启动',您可以立即强制运行该计算。",
|
||||
"xpack.securitySolution.riskScore.usersDashboardWarningPanelBody": "找不到任何用户风险分数数据。检查全局 KQL 搜索栏中是否具有任何全局筛选。如果刚刚启用了用户风险模块,风险引擎可能需要一小时才能生成并在此面板中显示用户风险分数数据。",
|
||||
"xpack.securitySolution.riskScore.usersDashboardWarningPanelTitle": "没有可显示的用户风险分数数据",
|
||||
"xpack.securitySolution.riskTabBody.scoreOverTimeTitle": "一段时间的{riskEntity}风险分数",
|
||||
|
|
|
@ -7,6 +7,5 @@
|
|||
|
||||
export * from './asset_criticality';
|
||||
export * from './risk_engine';
|
||||
export * from './risk_score';
|
||||
export * from './entity_store';
|
||||
export { EntityAnalyticsPrivileges } from './common';
|
||||
|
|
|
@ -21,7 +21,6 @@ export const InitRiskEngineResult = z.object({
|
|||
risk_engine_enabled: z.boolean(),
|
||||
risk_engine_resources_installed: z.boolean(),
|
||||
risk_engine_configuration_created: z.boolean(),
|
||||
legacy_risk_engine_disabled: z.boolean(),
|
||||
errors: z.array(z.string()),
|
||||
});
|
||||
|
||||
|
|
|
@ -45,7 +45,6 @@ components:
|
|||
- risk_engine_enabled
|
||||
- risk_engine_resources_installed
|
||||
- risk_engine_configuration_created
|
||||
- legacy_risk_engine_disabled
|
||||
- errors
|
||||
properties:
|
||||
risk_engine_enabled:
|
||||
|
@ -54,8 +53,6 @@ components:
|
|||
type: boolean
|
||||
risk_engine_configuration_created:
|
||||
type: boolean
|
||||
legacy_risk_engine_disabled:
|
||||
type: boolean
|
||||
errors:
|
||||
type: array
|
||||
items:
|
||||
|
|
|
@ -43,7 +43,6 @@ export const RiskEngineTaskStatus = z.object({
|
|||
|
||||
export type RiskEngineStatusResponse = z.infer<typeof RiskEngineStatusResponse>;
|
||||
export const RiskEngineStatusResponse = z.object({
|
||||
legacy_risk_engine_status: RiskEngineStatus,
|
||||
risk_engine_status: RiskEngineStatus,
|
||||
risk_engine_task_status: RiskEngineTaskStatus.optional(),
|
||||
});
|
||||
|
|
|
@ -57,11 +57,8 @@ components:
|
|||
RiskEngineStatusResponse:
|
||||
type: object
|
||||
required:
|
||||
- legacy_risk_engine_status
|
||||
- risk_engine_status
|
||||
properties:
|
||||
legacy_risk_engine_status:
|
||||
$ref: '#/components/schemas/RiskEngineStatus'
|
||||
risk_engine_status:
|
||||
$ref: '#/components/schemas/RiskEngineStatus'
|
||||
risk_engine_task_status:
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { TypeOf } from '@kbn/config-schema';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
export const createEsIndexRequestBody = schema.object({
|
||||
index: schema.string({ minLength: 1 }),
|
||||
mappings: schema.maybe(
|
||||
schema.oneOf([schema.string(), schema.recordOf(schema.string({ minLength: 1 }), schema.any())])
|
||||
),
|
||||
});
|
||||
|
||||
export type CreateEsIndexRequestBody = TypeOf<typeof createEsIndexRequestBody>;
|
|
@ -1,27 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createPrebuiltSavedObjectsRequestBody } from './create_prebuilt_saved_objects_route';
|
||||
|
||||
describe('createPrebuiltSavedObjectsRequestBody', () => {
|
||||
it('should throw error', () => {
|
||||
expect(() =>
|
||||
createPrebuiltSavedObjectsRequestBody.params.validate({ template_name: '123' })
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it.each([['hostRiskScoreDashboards', 'userRiskScoreDashboards']])(
|
||||
'should allow template %p',
|
||||
async (template) => {
|
||||
expect(
|
||||
createPrebuiltSavedObjectsRequestBody.params.validate({ template_name: template })
|
||||
).toEqual({
|
||||
template_name: template,
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
|
@ -1,17 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
export const createPrebuiltSavedObjectsRequestBody = {
|
||||
params: schema.object({
|
||||
template_name: schema.oneOf([
|
||||
schema.literal('hostRiskScoreDashboards'),
|
||||
schema.literal('userRiskScoreDashboards'),
|
||||
]),
|
||||
}),
|
||||
};
|
|
@ -1,25 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type { TypeOf } from '@kbn/config-schema';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
export const createStoredScriptRequestBody = schema.object({
|
||||
id: schema.string({ minLength: 1 }),
|
||||
script: schema.object({
|
||||
lang: schema.oneOf([
|
||||
schema.string(),
|
||||
schema.literal('painless'),
|
||||
schema.literal('expression'),
|
||||
schema.literal('mustache'),
|
||||
schema.literal('java'),
|
||||
]),
|
||||
options: schema.maybe(schema.recordOf(schema.string(), schema.string())),
|
||||
source: schema.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type CreateStoredScriptRequestBody = TypeOf<typeof createStoredScriptRequestBody>;
|
|
@ -1,18 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
export const deletePrebuiltSavedObjectsRequestBody = {
|
||||
params: schema.object({
|
||||
template_name: schema.oneOf([
|
||||
schema.literal('hostRiskScoreDashboards'),
|
||||
schema.literal('userRiskScoreDashboards'),
|
||||
]),
|
||||
}),
|
||||
body: schema.nullable(schema.object({ deleteAll: schema.maybe(schema.boolean()) })),
|
||||
};
|
|
@ -1,15 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { TypeOf } from '@kbn/config-schema';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
export const deleteStoredScriptRequestBody = schema.object({
|
||||
id: schema.string({ minLength: 1 }),
|
||||
});
|
||||
|
||||
export type DeleteStoredScriptRequestBody = TypeOf<typeof deleteStoredScriptRequestBody>;
|
|
@ -1,16 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './create_index/create_index_route';
|
||||
export * from './create_prebuilt_saved_objects/create_prebuilt_saved_objects_route';
|
||||
export * from './create_stored_script/create_stored_script_route';
|
||||
export * from './delete_indices/delete_indices_route';
|
||||
export * from './delete_prebuilt_saved_objects/delete_prebuilt_saved_objects_route';
|
||||
export * from './delete_stored_script/delete_stored_script_route';
|
||||
export * from './index_status/index_status_route';
|
||||
export * from './install_modules/install_modules_route';
|
||||
export * from './read_prebuilt_dev_tool_content/read_prebuilt_dev_tool_content_route';
|
|
@ -1,13 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
|
||||
export const indexStatusRequestQuery = t.type({
|
||||
indexName: t.string,
|
||||
entity: t.string,
|
||||
});
|
|
@ -1,18 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { RiskScoreEntity } from '../../../../search_strategy';
|
||||
|
||||
export const onboardingRiskScoreRequestBody = {
|
||||
body: schema.object({
|
||||
riskScoreEntity: schema.oneOf([
|
||||
schema.literal(RiskScoreEntity.host),
|
||||
schema.literal(RiskScoreEntity.user),
|
||||
]),
|
||||
}),
|
||||
};
|
|
@ -1,17 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
export const readConsoleRequestBody = {
|
||||
params: schema.object({
|
||||
console_id: schema.oneOf([
|
||||
schema.literal('enable_host_risk_score'),
|
||||
schema.literal('enable_user_risk_score'),
|
||||
]),
|
||||
}),
|
||||
};
|
|
@ -16,7 +16,6 @@ export const allHostsSchema = requestBasicOptionsSchema.extend({
|
|||
sort,
|
||||
pagination,
|
||||
timerange,
|
||||
isNewRiskScoreModuleInstalled: z.boolean().default(false),
|
||||
factoryQueryType: z.literal(HostsQueries.hosts),
|
||||
});
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ export const relatedHostsRequestOptionsSchema = requestBasicOptionsSchema.extend
|
|||
skip: z.boolean().optional(),
|
||||
from: z.string(),
|
||||
inspect,
|
||||
isNewRiskScoreModuleInstalled: z.boolean().default(false),
|
||||
factoryQueryType: z.literal(RelatedEntitiesQueries.relatedHosts),
|
||||
});
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ export const relatedUsersRequestOptionsSchema = requestBasicOptionsSchema.extend
|
|||
skip: z.boolean().optional(),
|
||||
from: z.string(),
|
||||
inspect,
|
||||
isNewRiskScoreModuleInstalled: z.boolean().default(false),
|
||||
factoryQueryType: z.literal(RelatedEntitiesQueries.relatedUsers),
|
||||
});
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@ export const usersSchema = requestOptionsPaginatedSchema.extend({
|
|||
field: z.enum([UsersFields.name, UsersFields.lastSeen]),
|
||||
}),
|
||||
timerange,
|
||||
isNewRiskScoreModuleInstalled: z.boolean().default(false),
|
||||
factoryQueryType: z.literal(UsersQueries.users),
|
||||
});
|
||||
|
||||
|
|
|
@ -282,7 +282,6 @@ export const TIMELINE_COPY_URL = `${TIMELINE_URL}/_copy` as const;
|
|||
export const NOTE_URL = '/api/note' as const;
|
||||
export const PINNED_EVENT_URL = '/api/pinned_event' as const;
|
||||
export const SOURCERER_API_URL = '/internal/security_solution/sourcerer' as const;
|
||||
export const RISK_SCORE_INDEX_STATUS_API_URL = '/internal/risk_score/index_status' as const;
|
||||
|
||||
/**
|
||||
* Default signals index key for kibana.dev.yml
|
||||
|
@ -355,10 +354,6 @@ export const showAllOthersBucket: string[] = [
|
|||
'user.name',
|
||||
];
|
||||
|
||||
export const RISKY_HOSTS_INDEX_PREFIX = 'ml_host_risk_score_' as const;
|
||||
|
||||
export const RISKY_USERS_INDEX_PREFIX = 'ml_user_risk_score_' as const;
|
||||
|
||||
export const TRANSFORM_STATES = {
|
||||
ABORTING: 'aborting',
|
||||
FAILED: 'failed',
|
||||
|
|
|
@ -16,7 +16,6 @@ export enum RiskScoreEntity {
|
|||
export const SERVICE_RISK_SCORE_ENTITY = 'service';
|
||||
|
||||
export interface InitRiskEngineResult {
|
||||
legacyRiskEngineDisabled: boolean;
|
||||
riskEngineResourcesInstalled: boolean;
|
||||
riskEngineConfigurationCreated: boolean;
|
||||
riskEngineEnabled: boolean;
|
||||
|
|
|
@ -10,24 +10,6 @@
|
|||
*/
|
||||
export const INTERNAL_RISK_SCORE_URL = '/internal/risk_score' as const;
|
||||
export const PUBLIC_RISK_SCORE_URL = '/api/risk_score' as const;
|
||||
export const DEV_TOOL_PREBUILT_CONTENT =
|
||||
`${INTERNAL_RISK_SCORE_URL}/prebuilt_content/dev_tool/{console_id}` as const;
|
||||
export const devToolPrebuiltContentUrl = (spaceId: string, consoleId: string) =>
|
||||
`/s/${spaceId}${INTERNAL_RISK_SCORE_URL}/prebuilt_content/dev_tool/${consoleId}` as const;
|
||||
export const PREBUILT_SAVED_OBJECTS_BULK_CREATE =
|
||||
`${INTERNAL_RISK_SCORE_URL}/prebuilt_content/saved_objects/_bulk_create/{template_name}` as const;
|
||||
export const prebuiltSavedObjectsBulkCreateUrl = (templateName: string) =>
|
||||
`${INTERNAL_RISK_SCORE_URL}/prebuilt_content/saved_objects/_bulk_create/${templateName}` as const;
|
||||
export const PREBUILT_SAVED_OBJECTS_BULK_DELETE =
|
||||
`${INTERNAL_RISK_SCORE_URL}/prebuilt_content/saved_objects/_bulk_delete/{template_name}` as const;
|
||||
export const prebuiltSavedObjectsBulkDeleteUrl = (templateName: string) =>
|
||||
`${INTERNAL_RISK_SCORE_URL}/prebuilt_content/saved_objects/_bulk_delete/${templateName}` as const;
|
||||
export const RISK_SCORE_CREATE_INDEX = `${INTERNAL_RISK_SCORE_URL}/indices/create` as const;
|
||||
export const RISK_SCORE_DELETE_INDICES = `${INTERNAL_RISK_SCORE_URL}/indices/delete` as const;
|
||||
export const RISK_SCORE_CREATE_STORED_SCRIPT =
|
||||
`${INTERNAL_RISK_SCORE_URL}/stored_scripts/create` as const;
|
||||
export const RISK_SCORE_DELETE_STORED_SCRIPT =
|
||||
`${INTERNAL_RISK_SCORE_URL}/stored_scripts/delete` as const;
|
||||
export const RISK_SCORE_PREVIEW_URL = `${INTERNAL_RISK_SCORE_URL}/preview` as const;
|
||||
export const RISK_SCORE_ENTITY_CALCULATION_URL =
|
||||
`${INTERNAL_RISK_SCORE_URL}/calculation/entity` as const;
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getHostRiskIndex, getUserRiskIndex } from '.';
|
||||
|
||||
describe('hosts risk search_strategy getHostRiskIndex', () => {
|
||||
it('should properly return host index if space is specified', () => {
|
||||
expect(getHostRiskIndex('testName', true, false)).toEqual('ml_host_risk_score_latest_testName');
|
||||
});
|
||||
|
||||
it('should properly return user index if space is specified', () => {
|
||||
expect(getUserRiskIndex('testName', true, false)).toEqual('ml_user_risk_score_latest_testName');
|
||||
});
|
||||
|
||||
describe('with new risk score module installed', () => {
|
||||
it('should properly return host index if onlyLatest is false', () => {
|
||||
expect(getHostRiskIndex('default', false, true)).toEqual('risk-score.risk-score-default');
|
||||
});
|
||||
|
||||
it('should properly return host index if onlyLatest is true', () => {
|
||||
expect(getHostRiskIndex('default', true, true)).toEqual(
|
||||
'risk-score.risk-score-latest-default'
|
||||
);
|
||||
});
|
||||
|
||||
it('should properly return user index if onlyLatest is false', () => {
|
||||
expect(getUserRiskIndex('default', false, true)).toEqual('risk-score.risk-score-default');
|
||||
});
|
||||
|
||||
it('should properly return user index if onlyLatest is true', () => {
|
||||
expect(getUserRiskIndex('default', true, true)).toEqual(
|
||||
'risk-score.risk-score-latest-default'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import type { ESQuery } from '../../../../typed_json';
|
||||
import { RISKY_HOSTS_INDEX_PREFIX, RISKY_USERS_INDEX_PREFIX } from '../../../../constants';
|
||||
import {
|
||||
RiskScoreEntity,
|
||||
getRiskScoreLatestIndex,
|
||||
|
@ -14,32 +13,12 @@ import {
|
|||
} from '../../../../entity_analytics/risk_engine';
|
||||
export { RiskQueries } from '../../../../api/search_strategy';
|
||||
|
||||
/**
|
||||
* Make sure this aligns with the index in step 6, 9 in
|
||||
* prebuilt_dev_tool_content/console_templates/enable_host_risk_score.console
|
||||
*/
|
||||
export const getHostRiskIndex = (
|
||||
spaceId: string,
|
||||
onlyLatest: boolean = true,
|
||||
isNewRiskScoreModuleInstalled: boolean
|
||||
): string => {
|
||||
if (isNewRiskScoreModuleInstalled) {
|
||||
return onlyLatest ? getRiskScoreLatestIndex(spaceId) : getRiskScoreTimeSeriesIndex(spaceId);
|
||||
} else {
|
||||
return `${RISKY_HOSTS_INDEX_PREFIX}${onlyLatest ? 'latest_' : ''}${spaceId}`;
|
||||
}
|
||||
export const getHostRiskIndex = (spaceId: string, onlyLatest: boolean = true): string => {
|
||||
return onlyLatest ? getRiskScoreLatestIndex(spaceId) : getRiskScoreTimeSeriesIndex(spaceId);
|
||||
};
|
||||
|
||||
export const getUserRiskIndex = (
|
||||
spaceId: string,
|
||||
onlyLatest: boolean = true,
|
||||
isNewRiskScoreModuleInstalled: boolean
|
||||
): string => {
|
||||
if (isNewRiskScoreModuleInstalled) {
|
||||
return onlyLatest ? getRiskScoreLatestIndex(spaceId) : getRiskScoreTimeSeriesIndex(spaceId);
|
||||
} else {
|
||||
return `${RISKY_USERS_INDEX_PREFIX}${onlyLatest ? 'latest_' : ''}${spaceId}`;
|
||||
}
|
||||
export const getUserRiskIndex = (spaceId: string, onlyLatest: boolean = true): string => {
|
||||
return onlyLatest ? getRiskScoreLatestIndex(spaceId) : getRiskScoreTimeSeriesIndex(spaceId);
|
||||
};
|
||||
|
||||
export const buildHostNamesFilter = (hostNames: string[]) => {
|
||||
|
|
|
@ -1,736 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Risk Score Modules getCreateLatestTransformOptions - host 1`] = `
|
||||
Object {
|
||||
"dest": Object {
|
||||
"index": "ml_host_risk_score_latest_customSpaceId",
|
||||
},
|
||||
"frequency": "1h",
|
||||
"latest": Object {
|
||||
"sort": "@timestamp",
|
||||
"unique_key": Array [
|
||||
"host.name",
|
||||
],
|
||||
},
|
||||
"source": Object {
|
||||
"index": Array [
|
||||
"ml_host_risk_score_customSpaceId",
|
||||
],
|
||||
},
|
||||
"sync": Object {
|
||||
"time": Object {
|
||||
"delay": "2s",
|
||||
"field": "ingest_timestamp",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Risk Score Modules getCreateLatestTransformOptions - user 1`] = `
|
||||
Object {
|
||||
"dest": Object {
|
||||
"index": "ml_user_risk_score_latest_customSpaceId",
|
||||
},
|
||||
"frequency": "1h",
|
||||
"latest": Object {
|
||||
"sort": "@timestamp",
|
||||
"unique_key": Array [
|
||||
"user.name",
|
||||
],
|
||||
},
|
||||
"source": Object {
|
||||
"index": Array [
|
||||
"ml_user_risk_score_customSpaceId",
|
||||
],
|
||||
},
|
||||
"sync": Object {
|
||||
"time": Object {
|
||||
"delay": "2s",
|
||||
"field": "ingest_timestamp",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Risk Score Modules getCreateMLHostPivotTransformOptions 1`] = `
|
||||
Object {
|
||||
"dest": Object {
|
||||
"index": "ml_host_risk_score_customSpaceId",
|
||||
"pipeline": "ml_hostriskscore_ingest_pipeline_customSpaceId",
|
||||
},
|
||||
"frequency": "1h",
|
||||
"pivot": Object {
|
||||
"aggregations": Object {
|
||||
"@timestamp": Object {
|
||||
"max": Object {
|
||||
"field": "@timestamp",
|
||||
},
|
||||
},
|
||||
"host.risk": Object {
|
||||
"scripted_metric": Object {
|
||||
"combine_script": "return state",
|
||||
"init_script": Object {
|
||||
"id": "ml_hostriskscore_init_script_customSpaceId",
|
||||
},
|
||||
"map_script": Object {
|
||||
"id": "ml_hostriskscore_map_script_customSpaceId",
|
||||
},
|
||||
"params": Object {
|
||||
"lookback_time": 72,
|
||||
"max_risk": 100,
|
||||
"p": 1.5,
|
||||
"server_multiplier": 1.5,
|
||||
"tactic_base_multiplier": 0.25,
|
||||
"tactic_weights": Object {
|
||||
"TA0001": 1,
|
||||
"TA0002": 2,
|
||||
"TA0003": 3,
|
||||
"TA0004": 4,
|
||||
"TA0005": 4,
|
||||
"TA0006": 4,
|
||||
"TA0007": 4,
|
||||
"TA0008": 5,
|
||||
"TA0009": 6,
|
||||
"TA0010": 7,
|
||||
"TA0011": 6,
|
||||
"TA0040": 8,
|
||||
"TA0042": 1,
|
||||
"TA0043": 1,
|
||||
},
|
||||
"time_decay_constant": 6,
|
||||
"zeta_constant": 2.612,
|
||||
},
|
||||
"reduce_script": Object {
|
||||
"id": "ml_hostriskscore_reduce_script_customSpaceId",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"group_by": Object {
|
||||
"host.name": Object {
|
||||
"terms": Object {
|
||||
"field": "host.name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"source": Object {
|
||||
"index": Array [
|
||||
".alerts-security.alerts-customSpaceId",
|
||||
],
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"range": Object {
|
||||
"@timestamp": Object {
|
||||
"gte": "now-5d",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"sync": Object {
|
||||
"time": Object {
|
||||
"delay": "120s",
|
||||
"field": "@timestamp",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Risk Score Modules getCreateMLUserPivotTransformOptions 1`] = `
|
||||
Object {
|
||||
"dest": Object {
|
||||
"index": "ml_user_risk_score_customSpaceId",
|
||||
"pipeline": "ml_userriskscore_ingest_pipeline_customSpaceId",
|
||||
},
|
||||
"frequency": "1h",
|
||||
"pivot": Object {
|
||||
"aggregations": Object {
|
||||
"@timestamp": Object {
|
||||
"max": Object {
|
||||
"field": "@timestamp",
|
||||
},
|
||||
},
|
||||
"user.risk": Object {
|
||||
"scripted_metric": Object {
|
||||
"combine_script": "return state",
|
||||
"init_script": "state.rule_risk_stats = new HashMap();",
|
||||
"map_script": Object {
|
||||
"id": "ml_userriskscore_map_script_customSpaceId",
|
||||
},
|
||||
"params": Object {
|
||||
"max_risk": 100,
|
||||
"p": 1.5,
|
||||
"zeta_constant": 2.612,
|
||||
},
|
||||
"reduce_script": Object {
|
||||
"id": "ml_userriskscore_reduce_script_customSpaceId",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"group_by": Object {
|
||||
"user.name": Object {
|
||||
"terms": Object {
|
||||
"field": "user.name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"source": Object {
|
||||
"index": Array [
|
||||
".alerts-security.alerts-customSpaceId",
|
||||
],
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"range": Object {
|
||||
"@timestamp": Object {
|
||||
"gte": "now-90d",
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"match": Object {
|
||||
"signal.status": "open",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"sync": Object {
|
||||
"time": Object {
|
||||
"delay": "120s",
|
||||
"field": "@timestamp",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Risk Score Modules getCreateRiskScoreIndicesOptions - host 1`] = `
|
||||
Object {
|
||||
"index": "ml_host_risk_score_customSpaceId",
|
||||
"mappings": Object {
|
||||
"properties": Object {
|
||||
"@timestamp": Object {
|
||||
"type": "date",
|
||||
},
|
||||
"host": Object {
|
||||
"properties": Object {
|
||||
"name": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"risk": Object {
|
||||
"properties": Object {
|
||||
"calculated_level": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"calculated_score_norm": Object {
|
||||
"type": "float",
|
||||
},
|
||||
"multipliers": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"rule_risks": Object {
|
||||
"properties": Object {
|
||||
"rule_id": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"rule_name": Object {
|
||||
"fields": Object {
|
||||
"keyword": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
},
|
||||
"rule_risk": Object {
|
||||
"type": "float",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"ingest_timestamp": Object {
|
||||
"type": "date",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Risk Score Modules getCreateRiskScoreIndicesOptions - user 1`] = `
|
||||
Object {
|
||||
"index": "ml_user_risk_score_customSpaceId",
|
||||
"mappings": Object {
|
||||
"properties": Object {
|
||||
"@timestamp": Object {
|
||||
"type": "date",
|
||||
},
|
||||
"ingest_timestamp": Object {
|
||||
"type": "date",
|
||||
},
|
||||
"user": Object {
|
||||
"properties": Object {
|
||||
"name": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"risk": Object {
|
||||
"properties": Object {
|
||||
"calculated_level": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"calculated_score_norm": Object {
|
||||
"type": "float",
|
||||
},
|
||||
"multipliers": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"rule_risks": Object {
|
||||
"properties": Object {
|
||||
"rule_id": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"rule_name": Object {
|
||||
"fields": Object {
|
||||
"keyword": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
},
|
||||
"rule_risk": Object {
|
||||
"type": "float",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Risk Score Modules getCreateRiskScoreLatestIndicesOptions - host 1`] = `
|
||||
Object {
|
||||
"index": "ml_host_risk_score_latest_customSpaceId",
|
||||
"mappings": Object {
|
||||
"properties": Object {
|
||||
"@timestamp": Object {
|
||||
"type": "date",
|
||||
},
|
||||
"host": Object {
|
||||
"properties": Object {
|
||||
"name": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"risk": Object {
|
||||
"properties": Object {
|
||||
"calculated_level": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"calculated_score_norm": Object {
|
||||
"type": "float",
|
||||
},
|
||||
"multipliers": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"rule_risks": Object {
|
||||
"properties": Object {
|
||||
"rule_id": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"rule_name": Object {
|
||||
"fields": Object {
|
||||
"keyword": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
},
|
||||
"rule_risk": Object {
|
||||
"type": "float",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"ingest_timestamp": Object {
|
||||
"type": "date",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Risk Score Modules getCreateRiskScoreLatestIndicesOptions - user 1`] = `
|
||||
Object {
|
||||
"index": "ml_user_risk_score_latest_customSpaceId",
|
||||
"mappings": Object {
|
||||
"properties": Object {
|
||||
"@timestamp": Object {
|
||||
"type": "date",
|
||||
},
|
||||
"ingest_timestamp": Object {
|
||||
"type": "date",
|
||||
},
|
||||
"user": Object {
|
||||
"properties": Object {
|
||||
"name": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"risk": Object {
|
||||
"properties": Object {
|
||||
"calculated_level": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"calculated_score_norm": Object {
|
||||
"type": "float",
|
||||
},
|
||||
"multipliers": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"rule_risks": Object {
|
||||
"properties": Object {
|
||||
"rule_id": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"rule_name": Object {
|
||||
"fields": Object {
|
||||
"keyword": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
},
|
||||
"rule_risk": Object {
|
||||
"type": "float",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Risk Score Modules getRiskHostCreateInitScriptOptions 1`] = `
|
||||
Object {
|
||||
"id": "ml_hostriskscore_init_script_default",
|
||||
"script": Object {
|
||||
"lang": "painless",
|
||||
"source": "state.rule_risk_stats = new HashMap();
|
||||
state.host_variant_set = false;
|
||||
state.host_variant = new String();
|
||||
state.tactic_ids = new HashSet();",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Risk Score Modules getRiskHostCreateLevelScriptOptions 1`] = `
|
||||
Object {
|
||||
"id": "ml_hostriskscore_levels_script_default",
|
||||
"script": Object {
|
||||
"lang": "painless",
|
||||
"source": "double risk_score = (def)ctx.getByPath(params.risk_score);
|
||||
if (risk_score < 20) {
|
||||
ctx['host']['risk']['calculated_level'] = 'Unknown'
|
||||
}
|
||||
else if (risk_score >= 20 && risk_score < 40) {
|
||||
ctx['host']['risk']['calculated_level'] = 'Low'
|
||||
}
|
||||
else if (risk_score >= 40 && risk_score < 70) {
|
||||
ctx['host']['risk']['calculated_level'] = 'Moderate'
|
||||
}
|
||||
else if (risk_score >= 70 && risk_score < 90) {
|
||||
ctx['host']['risk']['calculated_level'] = 'High'
|
||||
}
|
||||
else if (risk_score >= 90) {
|
||||
ctx['host']['risk']['calculated_level'] = 'Critical'
|
||||
}",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Risk Score Modules getRiskHostCreateMapScriptOptions 1`] = `
|
||||
Object {
|
||||
"id": "ml_hostriskscore_map_script_default",
|
||||
"script": Object {
|
||||
"lang": "painless",
|
||||
"source": "// Get the host variant
|
||||
if (state.host_variant_set == false) {
|
||||
if (doc.containsKey(\\"host.os.full\\") && doc[\\"host.os.full\\"].size() != 0) {
|
||||
state.host_variant = doc[\\"host.os.full\\"].value;
|
||||
state.host_variant_set = true;
|
||||
}
|
||||
}
|
||||
// Aggregate all the tactics seen on the host
|
||||
if (doc.containsKey(\\"signal.rule.threat.tactic.id\\") && doc[\\"signal.rule.threat.tactic.id\\"].size() != 0) {
|
||||
state.tactic_ids.add(doc[\\"signal.rule.threat.tactic.id\\"].value);
|
||||
}
|
||||
// Get running sum of time-decayed risk score per rule name per shard
|
||||
String rule_name = doc[\\"signal.rule.name\\"].value;
|
||||
def stats = state.rule_risk_stats.getOrDefault(rule_name, [0.0,\\"\\",false]);
|
||||
int time_diff = (int)((System.currentTimeMillis() - doc[\\"@timestamp\\"].value.toInstant().toEpochMilli()) / (1000.0 * 60.0 * 60.0));
|
||||
double risk_derate = Math.min(1, Math.exp((params.lookback_time - time_diff) / params.time_decay_constant));
|
||||
stats[0] = Math.max(stats[0], doc[\\"signal.rule.risk_score\\"].value * risk_derate);
|
||||
if (stats[2] == false) {
|
||||
stats[1] = doc[\\"kibana.alert.rule.uuid\\"].value;
|
||||
stats[2] = true;
|
||||
}
|
||||
state.rule_risk_stats.put(rule_name, stats);",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Risk Score Modules getRiskHostCreateReduceScriptOptions 1`] = `
|
||||
Object {
|
||||
"id": "ml_hostriskscore_reduce_script_default",
|
||||
"script": Object {
|
||||
"lang": "painless",
|
||||
"source": "// Consolidating time decayed risks and tactics from across all shards
|
||||
Map total_risk_stats = new HashMap();
|
||||
String host_variant = new String();
|
||||
def tactic_ids = new HashSet();
|
||||
for (state in states) {
|
||||
for (key in state.rule_risk_stats.keySet()) {
|
||||
def rule_stats = state.rule_risk_stats.get(key);
|
||||
def stats = total_risk_stats.getOrDefault(key, [0.0,\\"\\",false]);
|
||||
stats[0] = Math.max(stats[0], rule_stats[0]);
|
||||
if (stats[2] == false) {
|
||||
stats[1] = rule_stats[1];
|
||||
stats[2] = true;
|
||||
}
|
||||
total_risk_stats.put(key, stats);
|
||||
}
|
||||
if (host_variant.length() == 0) {
|
||||
host_variant = state.host_variant;
|
||||
}
|
||||
tactic_ids.addAll(state.tactic_ids);
|
||||
}
|
||||
// Consolidating individual rule risks and arranging them in decreasing order
|
||||
List risks = new ArrayList();
|
||||
for (key in total_risk_stats.keySet()) {
|
||||
risks.add(total_risk_stats[key][0])
|
||||
}
|
||||
Collections.sort(risks, Collections.reverseOrder());
|
||||
// Calculating total host risk score
|
||||
double total_risk = 0.0;
|
||||
double risk_cap = params.max_risk * params.zeta_constant;
|
||||
for (int i=0;i<risks.length;i++) {
|
||||
total_risk += risks[i] / Math.pow((1+i), params.p);
|
||||
}
|
||||
// Normalizing the host risk score
|
||||
double total_norm_risk = 100 * total_risk / risk_cap;
|
||||
if (total_norm_risk < 40) {
|
||||
total_norm_risk = 2.125 * total_norm_risk;
|
||||
}
|
||||
else if (total_norm_risk >= 40 && total_norm_risk < 50) {
|
||||
total_norm_risk = 85 + (total_norm_risk - 40);
|
||||
}
|
||||
else {
|
||||
total_norm_risk = 95 + (total_norm_risk - 50) / 10;
|
||||
}
|
||||
// Calculating multipliers to the host risk score
|
||||
double risk_multiplier = 1.0;
|
||||
List multipliers = new ArrayList();
|
||||
// Add a multiplier if host is a server
|
||||
if (host_variant.toLowerCase().contains(\\"server\\")) {
|
||||
risk_multiplier *= params.server_multiplier;
|
||||
multipliers.add(\\"Host is a server\\");
|
||||
}
|
||||
// Add multipliers based on number and diversity of tactics seen on the host
|
||||
for (String tactic : tactic_ids) {
|
||||
multipliers.add(\\"Tactic \\"+tactic);
|
||||
risk_multiplier *= 1 + params.tactic_base_multiplier * params.tactic_weights.getOrDefault(tactic, 0);
|
||||
}
|
||||
// Calculating final risk
|
||||
double final_risk = total_norm_risk;
|
||||
if (risk_multiplier > 1.0) {
|
||||
double prior_odds = (total_norm_risk) / (100 - total_norm_risk);
|
||||
double updated_odds = prior_odds * risk_multiplier;
|
||||
final_risk = 100 * updated_odds / (1 + updated_odds);
|
||||
}
|
||||
// Adding additional metadata
|
||||
List rule_stats = new ArrayList();
|
||||
for (key in total_risk_stats.keySet()) {
|
||||
Map temp = new HashMap();
|
||||
temp[\\"rule_name\\"] = key;
|
||||
temp[\\"rule_risk\\"] = total_risk_stats[key][0];
|
||||
temp[\\"rule_id\\"] = total_risk_stats[key][1];
|
||||
rule_stats.add(temp);
|
||||
}
|
||||
|
||||
return [\\"calculated_score_norm\\": final_risk, \\"rule_risks\\": rule_stats, \\"multipliers\\": multipliers];",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Risk Score Modules getRiskScoreIngestPipelineOptions - host 1`] = `
|
||||
Object {
|
||||
"name": "ml_hostriskscore_ingest_pipeline_default",
|
||||
"processors": Array [
|
||||
Object {
|
||||
"set": Object {
|
||||
"field": "ingest_timestamp",
|
||||
"value": "{{_ingest.timestamp}}",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"fingerprint": Object {
|
||||
"fields": Array [
|
||||
"@timestamp",
|
||||
"_id",
|
||||
],
|
||||
"method": "SHA-256",
|
||||
"target_field": "_id",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"script": Object {
|
||||
"id": "ml_hostriskscore_levels_script_default",
|
||||
"params": Object {
|
||||
"risk_score": "host.risk.calculated_score_norm",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Risk Score Modules getRiskScoreIngestPipelineOptions - user 1`] = `
|
||||
Object {
|
||||
"name": "ml_userriskscore_ingest_pipeline_default",
|
||||
"processors": Array [
|
||||
Object {
|
||||
"set": Object {
|
||||
"field": "ingest_timestamp",
|
||||
"value": "{{_ingest.timestamp}}",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"fingerprint": Object {
|
||||
"fields": Array [
|
||||
"@timestamp",
|
||||
"_id",
|
||||
],
|
||||
"method": "SHA-256",
|
||||
"target_field": "_id",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"script": Object {
|
||||
"id": "ml_userriskscore_levels_script_default",
|
||||
"params": Object {
|
||||
"risk_score": "user.risk.calculated_score_norm",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Risk Score Modules getRiskUserCreateLevelScriptOptions 1`] = `
|
||||
Object {
|
||||
"id": "ml_userriskscore_levels_script_default",
|
||||
"script": Object {
|
||||
"lang": "painless",
|
||||
"source": "double risk_score = (def)ctx.getByPath(params.risk_score);
|
||||
if (risk_score < 20) {
|
||||
ctx['user']['risk']['calculated_level'] = 'Unknown'
|
||||
}
|
||||
else if (risk_score >= 20 && risk_score < 40) {
|
||||
ctx['user']['risk']['calculated_level'] = 'Low'
|
||||
}
|
||||
else if (risk_score >= 40 && risk_score < 70) {
|
||||
ctx['user']['risk']['calculated_level'] = 'Moderate'
|
||||
}
|
||||
else if (risk_score >= 70 && risk_score < 90) {
|
||||
ctx['user']['risk']['calculated_level'] = 'High'
|
||||
}
|
||||
else if (risk_score >= 90) {
|
||||
ctx['user']['risk']['calculated_level'] = 'Critical'
|
||||
}",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Risk Score Modules getRiskUserCreateMapScriptOptions 1`] = `
|
||||
Object {
|
||||
"id": "ml_userriskscore_map_script_default",
|
||||
"script": Object {
|
||||
"lang": "painless",
|
||||
"source": "// Get running sum of risk score per rule name per shard\\\\\\\\
|
||||
String rule_name = doc[\\"signal.rule.name\\"].value;
|
||||
def stats = state.rule_risk_stats.getOrDefault(rule_name, 0.0);
|
||||
stats = doc[\\"signal.rule.risk_score\\"].value;
|
||||
state.rule_risk_stats.put(rule_name, stats);",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Risk Score Modules getRiskUserCreateReduceScriptOptions 1`] = `
|
||||
Object {
|
||||
"id": "ml_userriskscore_reduce_script_default",
|
||||
"script": Object {
|
||||
"lang": "painless",
|
||||
"source": "// Consolidating time decayed risks from across all shards
|
||||
Map total_risk_stats = new HashMap();
|
||||
for (state in states) {
|
||||
for (key in state.rule_risk_stats.keySet()) {
|
||||
def rule_stats = state.rule_risk_stats.get(key);
|
||||
def stats = total_risk_stats.getOrDefault(key, 0.0);
|
||||
stats = rule_stats;
|
||||
total_risk_stats.put(key, stats);
|
||||
}
|
||||
}
|
||||
// Consolidating individual rule risks and arranging them in decreasing order
|
||||
List risks = new ArrayList();
|
||||
for (key in total_risk_stats.keySet()) {
|
||||
risks.add(total_risk_stats[key])
|
||||
}
|
||||
Collections.sort(risks, Collections.reverseOrder());
|
||||
// Calculating total risk and normalizing it to a range
|
||||
double total_risk = 0.0;
|
||||
double risk_cap = params.max_risk * params.zeta_constant;
|
||||
for (int i=0;i<risks.length;i++) {
|
||||
total_risk += risks[i] / Math.pow((1+i), params.p);
|
||||
}
|
||||
double total_norm_risk = 100 * total_risk / risk_cap;
|
||||
if (total_norm_risk < 40) {
|
||||
total_norm_risk = 2.125 * total_norm_risk;
|
||||
}
|
||||
else if (total_norm_risk >= 40 && total_norm_risk < 50) {
|
||||
total_norm_risk = 85 + (total_norm_risk - 40);
|
||||
}
|
||||
else {
|
||||
total_norm_risk = 95 + (total_norm_risk - 50) / 10;
|
||||
}
|
||||
|
||||
List rule_stats = new ArrayList();
|
||||
for (key in total_risk_stats.keySet()) {
|
||||
Map temp = new HashMap();
|
||||
temp[\\"rule_name\\"] = key;
|
||||
temp[\\"rule_risk\\"] = total_risk_stats[key];
|
||||
rule_stats.add(temp);
|
||||
}
|
||||
|
||||
return [\\"calculated_score_norm\\": total_norm_risk, \\"rule_risks\\": rule_stats];",
|
||||
},
|
||||
}
|
||||
`;
|
|
@ -1,166 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { RiskScoreEntity } from '../search_strategy';
|
||||
import {
|
||||
getCreateLatestTransformOptions,
|
||||
getCreateMLHostPivotTransformOptions,
|
||||
getCreateMLUserPivotTransformOptions,
|
||||
getCreateRiskScoreIndicesOptions,
|
||||
getCreateRiskScoreLatestIndicesOptions,
|
||||
getIngestPipelineName,
|
||||
getLatestTransformIndex,
|
||||
getPivotTransformIndex,
|
||||
getRiskScoreIngestPipelineOptions,
|
||||
getRiskScorePivotTransformId,
|
||||
getRiskHostCreateInitScriptOptions,
|
||||
getRiskHostCreateLevelScriptOptions,
|
||||
getRiskHostCreateMapScriptOptions,
|
||||
getRiskHostCreateReduceScriptOptions,
|
||||
getRiskScoreInitScriptId,
|
||||
getRiskScoreLevelScriptId,
|
||||
getRiskScoreMapScriptId,
|
||||
getRiskScoreReduceScriptId,
|
||||
getRiskUserCreateLevelScriptOptions,
|
||||
getRiskUserCreateMapScriptOptions,
|
||||
getRiskUserCreateReduceScriptOptions,
|
||||
getLegacyIngestPipelineName,
|
||||
getLegacyRiskScoreLevelScriptId,
|
||||
getLegacyRiskScoreInitScriptId,
|
||||
getLegacyRiskScoreMapScriptId,
|
||||
getLegacyRiskScoreReduceScriptId,
|
||||
} from './risk_score_modules';
|
||||
|
||||
const mockSpaceId = 'customSpaceId';
|
||||
|
||||
describe.each([[RiskScoreEntity.host], [RiskScoreEntity.user]])('Risk Score Modules', (entity) => {
|
||||
test(`getRiskScorePivotTransformId - ${entity}`, () => {
|
||||
const id = getRiskScorePivotTransformId(entity, mockSpaceId);
|
||||
expect(id).toMatchInlineSnapshot(`"ml_${entity}riskscore_pivot_transform_customSpaceId"`);
|
||||
});
|
||||
test(`getIngestPipelineName - ${entity}`, () => {
|
||||
const name = getIngestPipelineName(entity);
|
||||
expect(name).toMatchInlineSnapshot(`"ml_${entity}riskscore_ingest_pipeline_default"`);
|
||||
});
|
||||
test(`getPivotTransformIndex - ${entity}`, () => {
|
||||
const index = getPivotTransformIndex(entity, mockSpaceId);
|
||||
expect(index).toMatchInlineSnapshot(`"ml_${entity}_risk_score_customSpaceId"`);
|
||||
});
|
||||
test(`getLatestTransformIndex - ${entity}`, () => {
|
||||
const index = getLatestTransformIndex(entity, mockSpaceId);
|
||||
expect(index).toMatchInlineSnapshot(`"ml_${entity}_risk_score_latest_customSpaceId"`);
|
||||
});
|
||||
test(`getRiskScoreLevelScriptId - ${entity}`, () => {
|
||||
const index = getRiskScoreLevelScriptId(entity);
|
||||
expect(index).toMatchInlineSnapshot(`"ml_${entity}riskscore_levels_script_default"`);
|
||||
});
|
||||
test(`getRiskScoreInitScriptId - ${entity}`, () => {
|
||||
const index = getRiskScoreInitScriptId(entity);
|
||||
expect(index).toMatchInlineSnapshot(`"ml_${entity}riskscore_init_script_default"`);
|
||||
});
|
||||
test(`getRiskScoreMapScriptId - ${entity}`, () => {
|
||||
const index = getRiskScoreMapScriptId(entity);
|
||||
expect(index).toMatchInlineSnapshot(`"ml_${entity}riskscore_map_script_default"`);
|
||||
});
|
||||
test(`getRiskScoreReduceScriptId - ${entity}`, () => {
|
||||
const index = getRiskScoreReduceScriptId(entity);
|
||||
expect(index).toMatchInlineSnapshot(`"ml_${entity}riskscore_reduce_script_default"`);
|
||||
});
|
||||
test(`getLegacyIngestPipelineName - ${entity}`, () => {
|
||||
const name = getLegacyIngestPipelineName(entity);
|
||||
expect(name).toMatchInlineSnapshot(`"ml_${entity}riskscore_ingest_pipeline"`);
|
||||
});
|
||||
test(`getLegacyRiskScoreLevelScriptId - ${entity}`, () => {
|
||||
const index = getLegacyRiskScoreLevelScriptId(entity);
|
||||
expect(index).toMatchInlineSnapshot(`"ml_${entity}riskscore_levels_script"`);
|
||||
});
|
||||
test(`getLegacyRiskScoreInitScriptId - ${entity}`, () => {
|
||||
const index = getLegacyRiskScoreInitScriptId(entity);
|
||||
expect(index).toMatchInlineSnapshot(`"ml_${entity}riskscore_init_script"`);
|
||||
});
|
||||
test(`getLegacyRiskScoreMapScriptId - ${entity}`, () => {
|
||||
const index = getLegacyRiskScoreMapScriptId(entity);
|
||||
expect(index).toMatchInlineSnapshot(`"ml_${entity}riskscore_map_script"`);
|
||||
});
|
||||
test(`getLegacyRiskScoreReduceScriptId - ${entity}`, () => {
|
||||
const index = getLegacyRiskScoreReduceScriptId(entity);
|
||||
expect(index).toMatchInlineSnapshot(`"ml_${entity}riskscore_reduce_script"`);
|
||||
});
|
||||
test(`getRiskScoreIngestPipelineOptions - ${entity}`, () => {
|
||||
const options = getRiskScoreIngestPipelineOptions(entity);
|
||||
expect(options).toMatchSnapshot();
|
||||
});
|
||||
test(`getCreateRiskScoreIndicesOptions - ${entity}`, () => {
|
||||
const options = getCreateRiskScoreIndicesOptions({
|
||||
spaceId: mockSpaceId,
|
||||
riskScoreEntity: entity,
|
||||
});
|
||||
expect(options).toMatchSnapshot();
|
||||
});
|
||||
test(`getCreateRiskScoreLatestIndicesOptions - ${entity}`, () => {
|
||||
const options = getCreateRiskScoreLatestIndicesOptions({
|
||||
spaceId: mockSpaceId,
|
||||
riskScoreEntity: entity,
|
||||
});
|
||||
expect(options).toMatchSnapshot();
|
||||
});
|
||||
test(`getCreateLatestTransformOptions - ${entity}`, () => {
|
||||
const options = getCreateLatestTransformOptions({
|
||||
spaceId: mockSpaceId,
|
||||
riskScoreEntity: entity,
|
||||
});
|
||||
expect(options).toMatchSnapshot();
|
||||
});
|
||||
test(`getCreateML${
|
||||
entity.charAt(0).toUpperCase() + entity.slice(1)
|
||||
}PivotTransformOptions`, () => {
|
||||
const fn =
|
||||
entity === RiskScoreEntity.host
|
||||
? getCreateMLHostPivotTransformOptions
|
||||
: getCreateMLUserPivotTransformOptions;
|
||||
const options = fn({
|
||||
spaceId: mockSpaceId,
|
||||
});
|
||||
expect(options).toMatchSnapshot();
|
||||
});
|
||||
test(`getRisk${entity.charAt(0).toUpperCase() + entity.slice(1)}CreateLevelScriptOptions`, () => {
|
||||
const fn =
|
||||
entity === RiskScoreEntity.host
|
||||
? getRiskHostCreateLevelScriptOptions
|
||||
: getRiskUserCreateLevelScriptOptions;
|
||||
const options = fn();
|
||||
expect(options).toMatchSnapshot();
|
||||
});
|
||||
test(`getRisk${entity.charAt(0).toUpperCase() + entity.slice(1)}CreateMapScriptOptions`, () => {
|
||||
const fn =
|
||||
entity === RiskScoreEntity.host
|
||||
? getRiskHostCreateMapScriptOptions
|
||||
: getRiskUserCreateMapScriptOptions;
|
||||
const options = fn();
|
||||
expect(options).toMatchSnapshot();
|
||||
});
|
||||
test(`getRisk${
|
||||
entity.charAt(0).toUpperCase() + entity.slice(1)
|
||||
}CreateReduceScriptOptions`, () => {
|
||||
const fn =
|
||||
entity === RiskScoreEntity.host
|
||||
? getRiskHostCreateReduceScriptOptions
|
||||
: getRiskUserCreateReduceScriptOptions;
|
||||
const options = fn();
|
||||
expect(options).toMatchSnapshot();
|
||||
});
|
||||
|
||||
/**
|
||||
* User risk score doesn't have init script, so we only check for host
|
||||
*/
|
||||
if (entity === RiskScoreEntity.host) {
|
||||
test(`getRiskHostCreateInitScriptOptions`, () => {
|
||||
const options = getRiskHostCreateInitScriptOptions();
|
||||
expect(options).toMatchSnapshot();
|
||||
});
|
||||
}
|
||||
});
|
|
@ -1,587 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DEFAULT_ALERTS_INDEX } from '../constants';
|
||||
import { RiskScoreEntity, RiskScoreFields } from '../search_strategy';
|
||||
import type { Pipeline, Processor } from '../types/risk_scores';
|
||||
|
||||
/**
|
||||
* Aside from 8.4, all the transforms, scripts,
|
||||
* and ingest pipelines (and dashboard saved objects) are created with spaceId
|
||||
* so they won't affect each other across different spaces.
|
||||
*/
|
||||
export const getRiskScorePivotTransformId = (
|
||||
riskScoreEntity: RiskScoreEntity,
|
||||
spaceId = 'default'
|
||||
) => `ml_${riskScoreEntity}riskscore_pivot_transform_${spaceId}`;
|
||||
|
||||
export const getRiskScoreLatestTransformId = (
|
||||
riskScoreEntity: RiskScoreEntity,
|
||||
spaceId = 'default'
|
||||
) => `ml_${riskScoreEntity}riskscore_latest_transform_${spaceId}`;
|
||||
|
||||
export const getIngestPipelineName = (riskScoreEntity: RiskScoreEntity, spaceId = 'default') =>
|
||||
`ml_${riskScoreEntity}riskscore_ingest_pipeline_${spaceId}`;
|
||||
|
||||
export const getPivotTransformIndex = (riskScoreEntity: RiskScoreEntity, spaceId = 'default') =>
|
||||
`ml_${riskScoreEntity}_risk_score_${spaceId}`;
|
||||
|
||||
export const getLatestTransformIndex = (riskScoreEntity: RiskScoreEntity, spaceId = 'default') =>
|
||||
`ml_${riskScoreEntity}_risk_score_latest_${spaceId}`;
|
||||
|
||||
export const getAlertsIndex = (spaceId = 'default') => `${DEFAULT_ALERTS_INDEX}-${spaceId}`;
|
||||
|
||||
export const getRiskScoreLevelScriptId = (riskScoreEntity: RiskScoreEntity, spaceId = 'default') =>
|
||||
`ml_${riskScoreEntity}riskscore_levels_script_${spaceId}`;
|
||||
export const getRiskScoreInitScriptId = (riskScoreEntity: RiskScoreEntity, spaceId = 'default') =>
|
||||
`ml_${riskScoreEntity}riskscore_init_script_${spaceId}`;
|
||||
export const getRiskScoreMapScriptId = (riskScoreEntity: RiskScoreEntity, spaceId = 'default') =>
|
||||
`ml_${riskScoreEntity}riskscore_map_script_${spaceId}`;
|
||||
export const getRiskScoreReduceScriptId = (riskScoreEntity: RiskScoreEntity, spaceId = 'default') =>
|
||||
`ml_${riskScoreEntity}riskscore_reduce_script_${spaceId}`;
|
||||
|
||||
/**
|
||||
* These scripts and Ingest pipeline were not space aware in 8.4
|
||||
* They were shared across spaces and therefore affected each other.
|
||||
* New scripts and ingest pipeline are all independent in each space, so these ids
|
||||
* are Deprecated.
|
||||
* But We still need to keep track of the old ids, so we can delete them during upgrade.
|
||||
*/
|
||||
export const getLegacyIngestPipelineName = (riskScoreEntity: RiskScoreEntity) =>
|
||||
`ml_${riskScoreEntity}riskscore_ingest_pipeline`;
|
||||
export const getLegacyRiskScoreLevelScriptId = (riskScoreEntity: RiskScoreEntity) =>
|
||||
`ml_${riskScoreEntity}riskscore_levels_script`;
|
||||
export const getLegacyRiskScoreInitScriptId = (riskScoreEntity: RiskScoreEntity) =>
|
||||
`ml_${riskScoreEntity}riskscore_init_script`;
|
||||
export const getLegacyRiskScoreMapScriptId = (riskScoreEntity: RiskScoreEntity) =>
|
||||
`ml_${riskScoreEntity}riskscore_map_script`;
|
||||
export const getLegacyRiskScoreReduceScriptId = (riskScoreEntity: RiskScoreEntity) =>
|
||||
`ml_${riskScoreEntity}riskscore_reduce_script`;
|
||||
|
||||
/**
|
||||
* This should be aligned with
|
||||
* console_templates/enable_host_risk_score.console step 1
|
||||
*/
|
||||
export const getRiskHostCreateLevelScriptOptions = (
|
||||
spaceId = 'default',
|
||||
stringifyScript?: boolean
|
||||
) => {
|
||||
const source =
|
||||
"double risk_score = (def)ctx.getByPath(params.risk_score);\nif (risk_score < 20) {\n ctx['host']['risk']['calculated_level'] = 'Unknown'\n}\nelse if (risk_score >= 20 && risk_score < 40) {\n ctx['host']['risk']['calculated_level'] = 'Low'\n}\nelse if (risk_score >= 40 && risk_score < 70) {\n ctx['host']['risk']['calculated_level'] = 'Moderate'\n}\nelse if (risk_score >= 70 && risk_score < 90) {\n ctx['host']['risk']['calculated_level'] = 'High'\n}\nelse if (risk_score >= 90) {\n ctx['host']['risk']['calculated_level'] = 'Critical'\n}";
|
||||
return {
|
||||
id: getRiskScoreLevelScriptId(RiskScoreEntity.host, spaceId),
|
||||
script: {
|
||||
lang: 'painless',
|
||||
source: stringifyScript ? JSON.stringify(source) : source,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This should be aligned with
|
||||
* console_templates/enable_host_risk_score.console step 2
|
||||
*/
|
||||
export const getRiskHostCreateInitScriptOptions = (
|
||||
spaceId = 'default',
|
||||
stringifyScript?: boolean
|
||||
) => {
|
||||
const source =
|
||||
'state.rule_risk_stats = new HashMap();\nstate.host_variant_set = false;\nstate.host_variant = new String();\nstate.tactic_ids = new HashSet();';
|
||||
return {
|
||||
id: getRiskScoreInitScriptId(RiskScoreEntity.host, spaceId),
|
||||
script: {
|
||||
lang: 'painless',
|
||||
source: stringifyScript ? JSON.stringify(source) : source,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This should be aligned with
|
||||
* console_templates/enable_host_risk_score.console step 3
|
||||
*/
|
||||
export const getRiskHostCreateMapScriptOptions = (
|
||||
spaceId = 'default',
|
||||
stringifyScript?: boolean
|
||||
) => {
|
||||
const source =
|
||||
'// Get the host variant\nif (state.host_variant_set == false) {\n if (doc.containsKey("host.os.full") && doc["host.os.full"].size() != 0) {\n state.host_variant = doc["host.os.full"].value;\n state.host_variant_set = true;\n }\n}\n// Aggregate all the tactics seen on the host\nif (doc.containsKey("signal.rule.threat.tactic.id") && doc["signal.rule.threat.tactic.id"].size() != 0) {\n state.tactic_ids.add(doc["signal.rule.threat.tactic.id"].value);\n}\n// Get running sum of time-decayed risk score per rule name per shard\nString rule_name = doc["signal.rule.name"].value;\ndef stats = state.rule_risk_stats.getOrDefault(rule_name, [0.0,"",false]);\nint time_diff = (int)((System.currentTimeMillis() - doc["@timestamp"].value.toInstant().toEpochMilli()) / (1000.0 * 60.0 * 60.0));\ndouble risk_derate = Math.min(1, Math.exp((params.lookback_time - time_diff) / params.time_decay_constant));\nstats[0] = Math.max(stats[0], doc["signal.rule.risk_score"].value * risk_derate);\nif (stats[2] == false) {\n stats[1] = doc["kibana.alert.rule.uuid"].value;\n stats[2] = true;\n}\nstate.rule_risk_stats.put(rule_name, stats);';
|
||||
return {
|
||||
id: getRiskScoreMapScriptId(RiskScoreEntity.host, spaceId),
|
||||
script: {
|
||||
lang: 'painless',
|
||||
source: stringifyScript ? JSON.stringify(source) : source,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This should be aligned with
|
||||
* console_templates/enable_host_risk_score.console step 4
|
||||
*/
|
||||
export const getRiskHostCreateReduceScriptOptions = (
|
||||
spaceId = 'default',
|
||||
stringifyScript?: boolean
|
||||
) => {
|
||||
const source =
|
||||
'// Consolidating time decayed risks and tactics from across all shards\nMap total_risk_stats = new HashMap();\nString host_variant = new String();\ndef tactic_ids = new HashSet();\nfor (state in states) {\n for (key in state.rule_risk_stats.keySet()) {\n def rule_stats = state.rule_risk_stats.get(key);\n def stats = total_risk_stats.getOrDefault(key, [0.0,"",false]);\n stats[0] = Math.max(stats[0], rule_stats[0]);\n if (stats[2] == false) {\n stats[1] = rule_stats[1];\n stats[2] = true;\n } \n total_risk_stats.put(key, stats);\n }\n if (host_variant.length() == 0) {\n host_variant = state.host_variant;\n }\n tactic_ids.addAll(state.tactic_ids);\n}\n// Consolidating individual rule risks and arranging them in decreasing order\nList risks = new ArrayList();\nfor (key in total_risk_stats.keySet()) {\n risks.add(total_risk_stats[key][0])\n}\nCollections.sort(risks, Collections.reverseOrder());\n// Calculating total host risk score\ndouble total_risk = 0.0;\ndouble risk_cap = params.max_risk * params.zeta_constant;\nfor (int i=0;i<risks.length;i++) {\n total_risk += risks[i] / Math.pow((1+i), params.p);\n}\n// Normalizing the host risk score\ndouble total_norm_risk = 100 * total_risk / risk_cap;\nif (total_norm_risk < 40) {\n total_norm_risk = 2.125 * total_norm_risk;\n}\nelse if (total_norm_risk >= 40 && total_norm_risk < 50) {\n total_norm_risk = 85 + (total_norm_risk - 40);\n}\nelse {\n total_norm_risk = 95 + (total_norm_risk - 50) / 10;\n}\n// Calculating multipliers to the host risk score\ndouble risk_multiplier = 1.0;\nList multipliers = new ArrayList();\n// Add a multiplier if host is a server\nif (host_variant.toLowerCase().contains("server")) {\n risk_multiplier *= params.server_multiplier;\n multipliers.add("Host is a server");\n}\n// Add multipliers based on number and diversity of tactics seen on the host\nfor (String tactic : tactic_ids) {\n multipliers.add("Tactic "+tactic);\n risk_multiplier *= 1 + params.tactic_base_multiplier * params.tactic_weights.getOrDefault(tactic, 0);\n}\n// Calculating final risk\ndouble final_risk = total_norm_risk;\nif (risk_multiplier > 1.0) {\n double prior_odds = (total_norm_risk) / (100 - total_norm_risk);\n double updated_odds = prior_odds * risk_multiplier; \n final_risk = 100 * updated_odds / (1 + updated_odds);\n}\n// Adding additional metadata\nList rule_stats = new ArrayList();\nfor (key in total_risk_stats.keySet()) {\n Map temp = new HashMap();\n temp["rule_name"] = key;\n temp["rule_risk"] = total_risk_stats[key][0];\n temp["rule_id"] = total_risk_stats[key][1];\n rule_stats.add(temp);\n}\n\nreturn ["calculated_score_norm": final_risk, "rule_risks": rule_stats, "multipliers": multipliers];';
|
||||
return {
|
||||
id: getRiskScoreReduceScriptId(RiskScoreEntity.host, spaceId),
|
||||
script: {
|
||||
lang: 'painless',
|
||||
source: stringifyScript ? JSON.stringify(source) : source,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This should be aligned with
|
||||
* console_templates/enable_user_risk_score.console step 1
|
||||
*/
|
||||
export const getRiskUserCreateLevelScriptOptions = (
|
||||
spaceId = 'default',
|
||||
stringifyScript?: boolean
|
||||
) => {
|
||||
const source =
|
||||
"double risk_score = (def)ctx.getByPath(params.risk_score);\nif (risk_score < 20) {\n ctx['user']['risk']['calculated_level'] = 'Unknown'\n}\nelse if (risk_score >= 20 && risk_score < 40) {\n ctx['user']['risk']['calculated_level'] = 'Low'\n}\nelse if (risk_score >= 40 && risk_score < 70) {\n ctx['user']['risk']['calculated_level'] = 'Moderate'\n}\nelse if (risk_score >= 70 && risk_score < 90) {\n ctx['user']['risk']['calculated_level'] = 'High'\n}\nelse if (risk_score >= 90) {\n ctx['user']['risk']['calculated_level'] = 'Critical'\n}";
|
||||
return {
|
||||
id: getRiskScoreLevelScriptId(RiskScoreEntity.user, spaceId),
|
||||
script: {
|
||||
lang: 'painless',
|
||||
source: stringifyScript ? JSON.stringify(source) : source,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This should be aligned with
|
||||
* console_templates/enable_user_risk_score.console step 2
|
||||
*/
|
||||
export const getRiskUserCreateMapScriptOptions = (
|
||||
spaceId = 'default',
|
||||
stringifyScript?: boolean
|
||||
) => {
|
||||
const source =
|
||||
'// Get running sum of risk score per rule name per shard\\\\\nString rule_name = doc["signal.rule.name"].value;\ndef stats = state.rule_risk_stats.getOrDefault(rule_name, 0.0);\nstats = doc["signal.rule.risk_score"].value;\nstate.rule_risk_stats.put(rule_name, stats);';
|
||||
return {
|
||||
id: getRiskScoreMapScriptId(RiskScoreEntity.user, spaceId),
|
||||
script: {
|
||||
lang: 'painless',
|
||||
source: stringifyScript ? JSON.stringify(source) : source,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This should be aligned with
|
||||
* console_templates/enable_user_risk_score.console step 3
|
||||
*/
|
||||
export const getRiskUserCreateReduceScriptOptions = (
|
||||
spaceId = 'default',
|
||||
stringifyScript?: boolean
|
||||
) => {
|
||||
const source =
|
||||
'// Consolidating time decayed risks from across all shards\nMap total_risk_stats = new HashMap();\nfor (state in states) {\n for (key in state.rule_risk_stats.keySet()) {\n def rule_stats = state.rule_risk_stats.get(key);\n def stats = total_risk_stats.getOrDefault(key, 0.0);\n stats = rule_stats;\n total_risk_stats.put(key, stats);\n }\n}\n// Consolidating individual rule risks and arranging them in decreasing order\nList risks = new ArrayList();\nfor (key in total_risk_stats.keySet()) {\n risks.add(total_risk_stats[key])\n}\nCollections.sort(risks, Collections.reverseOrder());\n// Calculating total risk and normalizing it to a range\ndouble total_risk = 0.0;\ndouble risk_cap = params.max_risk * params.zeta_constant;\nfor (int i=0;i<risks.length;i++) {\n total_risk += risks[i] / Math.pow((1+i), params.p);\n}\ndouble total_norm_risk = 100 * total_risk / risk_cap;\nif (total_norm_risk < 40) {\n total_norm_risk = 2.125 * total_norm_risk;\n}\nelse if (total_norm_risk >= 40 && total_norm_risk < 50) {\n total_norm_risk = 85 + (total_norm_risk - 40);\n}\nelse {\n total_norm_risk = 95 + (total_norm_risk - 50) / 10;\n}\n\nList rule_stats = new ArrayList();\nfor (key in total_risk_stats.keySet()) {\n Map temp = new HashMap();\n temp["rule_name"] = key;\n temp["rule_risk"] = total_risk_stats[key];\n rule_stats.add(temp);\n}\n\nreturn ["calculated_score_norm": total_norm_risk, "rule_risks": rule_stats];';
|
||||
return {
|
||||
id: getRiskScoreReduceScriptId(RiskScoreEntity.user, spaceId),
|
||||
script: {
|
||||
lang: 'painless',
|
||||
source: stringifyScript ? JSON.stringify(source) : source,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This should be aligned with
|
||||
* console_templates/enable_user_risk_score.console step 4
|
||||
* console_templates/enable_host_risk_score.console step 5
|
||||
*/
|
||||
export const getRiskScoreIngestPipelineOptions = (
|
||||
riskScoreEntity: RiskScoreEntity,
|
||||
spaceId = 'default',
|
||||
stringifyScript?: boolean
|
||||
): Pipeline => {
|
||||
const processors: Processor[] = [
|
||||
{
|
||||
set: {
|
||||
field: 'ingest_timestamp',
|
||||
value: '{{_ingest.timestamp}}',
|
||||
},
|
||||
},
|
||||
{
|
||||
fingerprint: {
|
||||
fields: ['@timestamp', '_id'],
|
||||
method: 'SHA-256',
|
||||
target_field: '_id',
|
||||
},
|
||||
},
|
||||
{
|
||||
script: {
|
||||
id: getRiskScoreLevelScriptId(riskScoreEntity, spaceId),
|
||||
params: {
|
||||
risk_score: `${riskScoreEntity}.risk.calculated_score_norm`,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
return {
|
||||
name: getIngestPipelineName(riskScoreEntity, spaceId),
|
||||
processors: stringifyScript ? JSON.stringify(processors) : processors,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This should be aligned with
|
||||
* console_templates/enable_user_risk_score.console step 5
|
||||
* console_templates/enable_host_risk_score.console step 6
|
||||
*/
|
||||
export const getCreateRiskScoreIndicesOptions = ({
|
||||
spaceId = 'default',
|
||||
riskScoreEntity,
|
||||
stringifyScript,
|
||||
}: {
|
||||
spaceId?: string;
|
||||
riskScoreEntity: RiskScoreEntity;
|
||||
stringifyScript?: boolean;
|
||||
}) => {
|
||||
const mappings = {
|
||||
properties: {
|
||||
[riskScoreEntity]: {
|
||||
properties: {
|
||||
name: {
|
||||
type: 'keyword',
|
||||
},
|
||||
risk: {
|
||||
properties: {
|
||||
calculated_score_norm: {
|
||||
type: 'float',
|
||||
},
|
||||
calculated_level: {
|
||||
type: 'keyword',
|
||||
},
|
||||
multipliers: {
|
||||
type: 'keyword',
|
||||
},
|
||||
rule_risks: {
|
||||
properties: {
|
||||
rule_name: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
keyword: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
rule_risk: {
|
||||
type: 'float',
|
||||
},
|
||||
rule_id: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ingest_timestamp: {
|
||||
type: 'date',
|
||||
},
|
||||
'@timestamp': {
|
||||
type: 'date',
|
||||
},
|
||||
},
|
||||
};
|
||||
return {
|
||||
index: getPivotTransformIndex(riskScoreEntity, spaceId),
|
||||
mappings: stringifyScript ? JSON.stringify(mappings) : mappings,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This should be aligned with
|
||||
* console_templates/enable_user_risk_score.console step 8
|
||||
* console_templates/enable_host_risk_score.console step 9
|
||||
*/
|
||||
export const getCreateRiskScoreLatestIndicesOptions = ({
|
||||
spaceId = 'default',
|
||||
riskScoreEntity,
|
||||
stringifyScript,
|
||||
}: {
|
||||
spaceId?: string;
|
||||
riskScoreEntity: RiskScoreEntity;
|
||||
stringifyScript?: boolean;
|
||||
}) => {
|
||||
const mappings = {
|
||||
properties: {
|
||||
[riskScoreEntity]: {
|
||||
properties: {
|
||||
name: {
|
||||
type: 'keyword',
|
||||
},
|
||||
risk: {
|
||||
properties: {
|
||||
calculated_score_norm: {
|
||||
type: 'float',
|
||||
},
|
||||
calculated_level: {
|
||||
type: 'keyword',
|
||||
},
|
||||
multipliers: {
|
||||
type: 'keyword',
|
||||
},
|
||||
rule_risks: {
|
||||
properties: {
|
||||
rule_name: {
|
||||
type: 'text',
|
||||
fields: {
|
||||
keyword: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
rule_risk: {
|
||||
type: 'float',
|
||||
},
|
||||
rule_id: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ingest_timestamp: {
|
||||
type: 'date',
|
||||
},
|
||||
'@timestamp': {
|
||||
type: 'date',
|
||||
},
|
||||
},
|
||||
};
|
||||
return {
|
||||
index: getLatestTransformIndex(riskScoreEntity, spaceId),
|
||||
mappings: stringifyScript ? JSON.stringify(mappings) : mappings,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This should be aligned with
|
||||
* console_templates/enable_host_risk_score.console step 7
|
||||
*/
|
||||
export const getCreateMLHostPivotTransformOptions = ({
|
||||
spaceId = 'default',
|
||||
stringifyScript,
|
||||
}: {
|
||||
spaceId?: string;
|
||||
stringifyScript?: boolean;
|
||||
}) => {
|
||||
const options = {
|
||||
dest: {
|
||||
index: getPivotTransformIndex(RiskScoreEntity.host, spaceId),
|
||||
pipeline: getIngestPipelineName(RiskScoreEntity.host, spaceId),
|
||||
},
|
||||
frequency: '1h',
|
||||
pivot: {
|
||||
aggregations: {
|
||||
'@timestamp': {
|
||||
max: {
|
||||
field: '@timestamp',
|
||||
},
|
||||
},
|
||||
'host.risk': {
|
||||
scripted_metric: {
|
||||
combine_script: 'return state',
|
||||
init_script: {
|
||||
id: getRiskScoreInitScriptId(RiskScoreEntity.host, spaceId),
|
||||
},
|
||||
map_script: {
|
||||
id: getRiskScoreMapScriptId(RiskScoreEntity.host, spaceId),
|
||||
},
|
||||
params: {
|
||||
lookback_time: 72,
|
||||
max_risk: 100,
|
||||
p: 1.5,
|
||||
server_multiplier: 1.5,
|
||||
tactic_base_multiplier: 0.25,
|
||||
tactic_weights: {
|
||||
TA0001: 1,
|
||||
TA0002: 2,
|
||||
TA0003: 3,
|
||||
TA0004: 4,
|
||||
TA0005: 4,
|
||||
TA0006: 4,
|
||||
TA0007: 4,
|
||||
TA0008: 5,
|
||||
TA0009: 6,
|
||||
TA0010: 7,
|
||||
TA0011: 6,
|
||||
TA0040: 8,
|
||||
TA0042: 1,
|
||||
TA0043: 1,
|
||||
},
|
||||
time_decay_constant: 6,
|
||||
zeta_constant: 2.612,
|
||||
},
|
||||
reduce_script: {
|
||||
id: getRiskScoreReduceScriptId(RiskScoreEntity.host, spaceId),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
group_by: {
|
||||
[RiskScoreFields.hostName]: {
|
||||
terms: {
|
||||
field: RiskScoreFields.hostName,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
source: {
|
||||
index: [getAlertsIndex(spaceId)],
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: 'now-5d',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
sync: {
|
||||
time: {
|
||||
delay: '120s',
|
||||
field: '@timestamp',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return stringifyScript ? JSON.stringify(options) : options;
|
||||
};
|
||||
|
||||
/**
|
||||
* This should be aligned with
|
||||
* console_templates/enable_user_risk_score.console step 6
|
||||
*/
|
||||
export const getCreateMLUserPivotTransformOptions = ({
|
||||
spaceId = 'default',
|
||||
stringifyScript,
|
||||
}: {
|
||||
spaceId?: string;
|
||||
stringifyScript?: boolean;
|
||||
}) => {
|
||||
const options = {
|
||||
dest: {
|
||||
index: getPivotTransformIndex(RiskScoreEntity.user, spaceId),
|
||||
pipeline: getIngestPipelineName(RiskScoreEntity.user, spaceId),
|
||||
},
|
||||
frequency: '1h',
|
||||
pivot: {
|
||||
aggregations: {
|
||||
'@timestamp': {
|
||||
max: {
|
||||
field: '@timestamp',
|
||||
},
|
||||
},
|
||||
'user.risk': {
|
||||
scripted_metric: {
|
||||
combine_script: 'return state',
|
||||
init_script: 'state.rule_risk_stats = new HashMap();',
|
||||
map_script: {
|
||||
id: getRiskScoreMapScriptId(RiskScoreEntity.user, spaceId),
|
||||
},
|
||||
params: {
|
||||
max_risk: 100,
|
||||
p: 1.5,
|
||||
zeta_constant: 2.612,
|
||||
},
|
||||
reduce_script: {
|
||||
id: getRiskScoreReduceScriptId(RiskScoreEntity.user, spaceId),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
group_by: {
|
||||
'user.name': {
|
||||
terms: {
|
||||
field: 'user.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
source: {
|
||||
index: [getAlertsIndex(spaceId)],
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: 'now-90d',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
match: {
|
||||
'signal.status': 'open',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
sync: {
|
||||
time: {
|
||||
delay: '120s',
|
||||
field: '@timestamp',
|
||||
},
|
||||
},
|
||||
};
|
||||
return stringifyScript ? JSON.stringify(options) : options;
|
||||
};
|
||||
|
||||
/**
|
||||
* This should be aligned with
|
||||
* console_templates/enable_user_risk_score.console step 9
|
||||
* console_templates/enable_host_risk_score.console step 10
|
||||
*/
|
||||
export const getCreateLatestTransformOptions = ({
|
||||
spaceId = 'default',
|
||||
riskScoreEntity,
|
||||
stringifyScript,
|
||||
}: {
|
||||
spaceId?: string;
|
||||
riskScoreEntity: RiskScoreEntity;
|
||||
stringifyScript?: boolean;
|
||||
}) => {
|
||||
const options = {
|
||||
dest: {
|
||||
index: getLatestTransformIndex(riskScoreEntity, spaceId),
|
||||
},
|
||||
frequency: '1h',
|
||||
latest: {
|
||||
sort: '@timestamp',
|
||||
unique_key: [`${riskScoreEntity}.name`],
|
||||
},
|
||||
source: {
|
||||
index: [getPivotTransformIndex(riskScoreEntity, spaceId)],
|
||||
},
|
||||
sync: {
|
||||
time: {
|
||||
delay: '2s',
|
||||
field: 'ingest_timestamp',
|
||||
},
|
||||
},
|
||||
};
|
||||
return stringifyScript ? JSON.stringify(options) : options;
|
||||
};
|
|
@ -14,7 +14,6 @@ import * as inputActions from '../../store/inputs/actions';
|
|||
import { InputsModelId } from '../../store/inputs/constants';
|
||||
import { createMockStore, mockGlobalState, TestProviders } from '../../mock';
|
||||
import { useRefetchByRestartingSession } from '../page/use_refetch_by_session';
|
||||
import { getRiskScoreDonutAttributes } from '../../../entity_analytics/lens_attributes/risk_score_donut';
|
||||
|
||||
jest.mock('./lens_embeddable');
|
||||
jest.mock('../page/use_refetch_by_session', () => ({
|
||||
|
@ -148,36 +147,4 @@ describe('VisualizationEmbeddable', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when isDonut = true', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useRefetchByRestartingSession as jest.Mock).mockReturnValue({
|
||||
session: {
|
||||
current: {
|
||||
start: jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(mockSearchSessionId)
|
||||
.mockReturnValue(mockSearchSessionIdDefault),
|
||||
},
|
||||
},
|
||||
refetchByRestartingSession: mockRefetchByRestartingSession,
|
||||
});
|
||||
res = render(
|
||||
<TestProviders>
|
||||
<VisualizationEmbeddable
|
||||
getLensAttributes={getRiskScoreDonutAttributes}
|
||||
id="testId"
|
||||
isDonut={true}
|
||||
label={'Total'}
|
||||
timerange={{ from: '2022-10-27T23:00:00.000Z', to: '2022-11-04T10:46:16.204Z' }}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
});
|
||||
|
||||
it('should render donut wrapper ', () => {
|
||||
expect(res.getByTestId('donut-chart')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,11 +14,6 @@ jest.mock('../../use_search_strategy', () => ({
|
|||
useSearchStrategy: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../entity_analytics/api/hooks/use_risk_engine_status', () => ({
|
||||
useIsNewRiskScoreModuleInstalled: jest
|
||||
.fn()
|
||||
.mockReturnValue({ isLoading: false, installed: true }),
|
||||
}));
|
||||
const mockUseSearchStrategy = useSearchStrategy as jest.Mock;
|
||||
const mockSearch = jest.fn();
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@ import { RelatedEntitiesQueries } from '../../../../../common/search_strategy/se
|
|||
import type { RelatedHost } from '../../../../../common/search_strategy/security_solution/related_entities/related_hosts';
|
||||
import { useSearchStrategy } from '../../use_search_strategy';
|
||||
import { FAIL_RELATED_HOSTS } from './translations';
|
||||
import { useIsNewRiskScoreModuleInstalled } from '../../../../entity_analytics/api/hooks/use_risk_engine_status';
|
||||
|
||||
export interface UseUserRelatedHostsResult {
|
||||
inspect: InspectResponse;
|
||||
|
@ -51,9 +50,6 @@ export const useUserRelatedHosts = ({
|
|||
abort: skip,
|
||||
});
|
||||
|
||||
const { installed: isNewRiskScoreModuleInstalled, isLoading: riskScoreStatusLoading } =
|
||||
useIsNewRiskScoreModuleInstalled();
|
||||
|
||||
const userRelatedHostsResponse = useMemo(
|
||||
() => ({
|
||||
inspect,
|
||||
|
@ -71,16 +67,15 @@ export const useUserRelatedHosts = ({
|
|||
factoryQueryType: RelatedEntitiesQueries.relatedHosts,
|
||||
userName,
|
||||
from,
|
||||
isNewRiskScoreModuleInstalled,
|
||||
}),
|
||||
[indexNames, from, userName, isNewRiskScoreModuleInstalled]
|
||||
[indexNames, from, userName]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!skip && !riskScoreStatusLoading) {
|
||||
if (!skip) {
|
||||
search(userRelatedHostsRequest);
|
||||
}
|
||||
}, [userRelatedHostsRequest, search, skip, riskScoreStatusLoading]);
|
||||
}, [userRelatedHostsRequest, search, skip]);
|
||||
|
||||
return userRelatedHostsResponse;
|
||||
};
|
||||
|
|
|
@ -14,12 +14,6 @@ jest.mock('../../use_search_strategy', () => ({
|
|||
useSearchStrategy: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../entity_analytics/api/hooks/use_risk_engine_status', () => ({
|
||||
useIsNewRiskScoreModuleInstalled: jest
|
||||
.fn()
|
||||
.mockReturnValue({ isLoading: false, installed: true }),
|
||||
}));
|
||||
|
||||
const mockUseSearchStrategy = useSearchStrategy as jest.Mock;
|
||||
const mockSearch = jest.fn();
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@ import { RelatedEntitiesQueries } from '../../../../../common/search_strategy/se
|
|||
import type { RelatedUser } from '../../../../../common/search_strategy/security_solution/related_entities/related_users';
|
||||
import { useSearchStrategy } from '../../use_search_strategy';
|
||||
import { FAIL_RELATED_USERS } from './translations';
|
||||
import { useIsNewRiskScoreModuleInstalled } from '../../../../entity_analytics/api/hooks/use_risk_engine_status';
|
||||
|
||||
export interface UseHostRelatedUsersResult {
|
||||
inspect: InspectResponse;
|
||||
|
@ -35,8 +34,6 @@ export const useHostRelatedUsers = ({
|
|||
from,
|
||||
skip = false,
|
||||
}: UseHostRelatedUsersParam): UseHostRelatedUsersResult => {
|
||||
const { installed: isNewRiskScoreModuleInstalled, isLoading: riskScoreStatusLoading } =
|
||||
useIsNewRiskScoreModuleInstalled();
|
||||
const {
|
||||
loading,
|
||||
result: response,
|
||||
|
@ -70,16 +67,15 @@ export const useHostRelatedUsers = ({
|
|||
factoryQueryType: RelatedEntitiesQueries.relatedUsers,
|
||||
hostName,
|
||||
from,
|
||||
isNewRiskScoreModuleInstalled,
|
||||
}),
|
||||
[indexNames, from, hostName, isNewRiskScoreModuleInstalled]
|
||||
[indexNames, from, hostName]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!skip && !riskScoreStatusLoading) {
|
||||
if (!skip) {
|
||||
search(hostRelatedUsersRequest);
|
||||
}
|
||||
}, [hostRelatedUsersRequest, riskScoreStatusLoading, search, skip]);
|
||||
}, [hostRelatedUsersRequest, search, skip]);
|
||||
|
||||
return hostRelatedUsersResponse;
|
||||
};
|
||||
|
|
|
@ -12,10 +12,6 @@ export const REQUEST_NAMES = {
|
|||
SECURITY_CREATE_TAG: `${APP_UI_ID} fetch security create tag`,
|
||||
CTI_TAGS: `${APP_UI_ID} fetch cti tags`,
|
||||
ANOMALIES_TABLE: `${APP_UI_ID} fetch anomalies table data`,
|
||||
GET_RISK_SCORE_DEPRECATED: `${APP_UI_ID} fetch is risk score deprecated`,
|
||||
ENABLE_RISK_SCORE: `${APP_UI_ID} fetch enable risk score`,
|
||||
REFRESH_RISK_SCORE: `${APP_UI_ID} fetch refresh risk score`,
|
||||
UPGRADE_RISK_SCORE: `${APP_UI_ID} fetch upgrade risk score`,
|
||||
} as const;
|
||||
|
||||
export type RequestName = (typeof REQUEST_NAMES)[keyof typeof REQUEST_NAMES];
|
||||
|
|
|
@ -5,9 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export const isIndexNotFoundError = (error: unknown): boolean =>
|
||||
(
|
||||
error as {
|
||||
attributes?: { caused_by?: { type?: string } };
|
||||
}
|
||||
).attributes?.caused_by?.type === 'index_not_found_exception';
|
||||
export const isIndexNotFoundError = (error: unknown): boolean => {
|
||||
const castError = error as {
|
||||
attributes?: {
|
||||
caused_by?: { type?: string };
|
||||
error?: { caused_by?: { type?: string } };
|
||||
};
|
||||
};
|
||||
return (
|
||||
castError.attributes?.caused_by?.type === 'index_not_found_exception' ||
|
||||
castError.attributes?.error?.caused_by?.type === 'index_not_found_exception'
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isSecurityAppError } from '@kbn/securitysolution-t-grid';
|
||||
|
||||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
import { checkSignalIndex } from './api';
|
||||
import * as i18n from './translations';
|
||||
import { useAlertsPrivileges } from './use_alerts_privileges';
|
||||
|
||||
interface ReturnSignalIndex {
|
||||
loading: boolean;
|
||||
signalIndexExists: boolean | null;
|
||||
signalIndexName: string | null;
|
||||
signalIndexMappingOutdated: boolean | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for managing signal index
|
||||
*
|
||||
*
|
||||
*/
|
||||
export const useCheckSignalIndex = (): ReturnSignalIndex => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [signalIndex, setSignalIndex] = useState<Omit<ReturnSignalIndex, 'loading'>>({
|
||||
signalIndexExists: null,
|
||||
signalIndexName: null,
|
||||
signalIndexMappingOutdated: null,
|
||||
});
|
||||
const { addError } = useAppToasts();
|
||||
const { hasIndexRead } = useAlertsPrivileges();
|
||||
|
||||
useEffect(() => {
|
||||
let isSubscribed = true;
|
||||
const abortCtrl = new AbortController();
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const signal = await checkSignalIndex({ signal: abortCtrl.signal });
|
||||
|
||||
if (isSubscribed && signal != null) {
|
||||
setSignalIndex({
|
||||
signalIndexExists: signal?.indexExists,
|
||||
signalIndexName: signal.name,
|
||||
signalIndexMappingOutdated: signal.index_mapping_outdated,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (isSubscribed) {
|
||||
setSignalIndex({
|
||||
signalIndexExists: false,
|
||||
signalIndexName: null,
|
||||
signalIndexMappingOutdated: null,
|
||||
});
|
||||
if (isSecurityAppError(error) && error.body.status_code !== 404) {
|
||||
addError(error, { title: i18n.SIGNAL_GET_NAME_FAILURE });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isSubscribed) {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (hasIndexRead) {
|
||||
fetchData();
|
||||
} else {
|
||||
// Skip data fetching as the current user doesn't have enough priviliges.
|
||||
// Attempt to get the signal index will result in 500 error.
|
||||
setLoading(false);
|
||||
}
|
||||
return () => {
|
||||
isSubscribed = false;
|
||||
abortCtrl.abort();
|
||||
};
|
||||
}, [addError, hasIndexRead]);
|
||||
|
||||
return { loading, ...signalIndex };
|
||||
};
|
|
@ -28,7 +28,6 @@ import type {
|
|||
AssetCriticalityRecord,
|
||||
EntityAnalyticsPrivileges,
|
||||
} from '../../../common/api/entity_analytics';
|
||||
import type { RiskScoreEntity } from '../../../common/search_strategy';
|
||||
import {
|
||||
RISK_ENGINE_STATUS_URL,
|
||||
RISK_SCORE_PREVIEW_URL,
|
||||
|
@ -38,7 +37,6 @@ import {
|
|||
RISK_ENGINE_PRIVILEGES_URL,
|
||||
ASSET_CRITICALITY_INTERNAL_PRIVILEGES_URL,
|
||||
ASSET_CRITICALITY_PUBLIC_URL,
|
||||
RISK_SCORE_INDEX_STATUS_API_URL,
|
||||
RISK_ENGINE_SETTINGS_URL,
|
||||
ASSET_CRITICALITY_PUBLIC_CSV_UPLOAD_URL,
|
||||
RISK_SCORE_ENTITY_CALCULATION_URL,
|
||||
|
@ -259,27 +257,6 @@ export const useEntityAnalyticsRoutes = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const getRiskScoreIndexStatus = ({
|
||||
query,
|
||||
signal,
|
||||
}: {
|
||||
query: {
|
||||
indexName: string;
|
||||
entity: RiskScoreEntity;
|
||||
};
|
||||
signal?: AbortSignal;
|
||||
}): Promise<{
|
||||
isDeprecated: boolean;
|
||||
isEnabled: boolean;
|
||||
}> =>
|
||||
http.fetch<{ isDeprecated: boolean; isEnabled: boolean }>(RISK_SCORE_INDEX_STATUS_API_URL, {
|
||||
version: '1',
|
||||
method: 'GET',
|
||||
query,
|
||||
asSystemRequest: true,
|
||||
signal,
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetches risk engine settings
|
||||
*/
|
||||
|
@ -321,7 +298,6 @@ export const useEntityAnalyticsRoutes = () => {
|
|||
deleteAssetCriticality,
|
||||
fetchAssetCriticality,
|
||||
uploadAssetCriticalityFile,
|
||||
getRiskScoreIndexStatus,
|
||||
fetchRiskEngineSettings,
|
||||
calculateEntityRiskScore,
|
||||
cleanUpRiskEngine,
|
||||
|
|
|
@ -10,9 +10,7 @@ import { useCallback } from 'react';
|
|||
import moment from 'moment';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { RiskEngineStatusResponse } from '../../../../common/api/entity_analytics/risk_engine/engine_status_route.gen';
|
||||
import { RiskEngineStatusEnum } from '../../../../common/api/entity_analytics/risk_engine/engine_status_route.gen';
|
||||
import { useEntityAnalyticsRoutes } from '../api';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
|
||||
const FETCH_RISK_ENGINE_STATUS = ['GET', 'FETCH_RISK_ENGINE_STATUS'];
|
||||
|
||||
export const useInvalidateRiskEngineStatusQuery = () => {
|
||||
|
@ -25,24 +23,7 @@ export const useInvalidateRiskEngineStatusQuery = () => {
|
|||
}, [queryClient]);
|
||||
};
|
||||
|
||||
interface RiskScoreModuleStatus {
|
||||
isLoading: boolean;
|
||||
installed?: boolean;
|
||||
}
|
||||
|
||||
export const useIsNewRiskScoreModuleInstalled = (): RiskScoreModuleStatus => {
|
||||
const { data: riskEngineStatus, isLoading } = useRiskEngineStatus();
|
||||
|
||||
if (isLoading) {
|
||||
return { isLoading: true };
|
||||
}
|
||||
|
||||
return { isLoading: false, installed: !!riskEngineStatus?.isNewRiskScoreModuleInstalled };
|
||||
};
|
||||
|
||||
export const useRiskEngineCountdownTime = (
|
||||
riskEngineStatus: RiskEngineStatus | undefined
|
||||
): string => {
|
||||
export const useRiskEngineCountdownTime = (riskEngineStatus?: RiskEngineStatusResponse): string => {
|
||||
const { status, runAt } = riskEngineStatus?.risk_engine_task_status || {};
|
||||
const isRunning = status === 'running' || (!!runAt && new Date(runAt) < new Date());
|
||||
|
||||
|
@ -56,46 +37,16 @@ export const useRiskEngineCountdownTime = (
|
|||
: moment(runAt).fromNow(true);
|
||||
};
|
||||
|
||||
export interface RiskEngineStatus extends RiskEngineStatusResponse {
|
||||
isUpdateAvailable: boolean;
|
||||
isNewRiskScoreModuleInstalled: boolean;
|
||||
isNewRiskScoreModuleAvailable: boolean;
|
||||
}
|
||||
|
||||
export const useRiskEngineStatus = (
|
||||
queryOptions: Pick<
|
||||
UseQueryOptions<unknown, unknown, RiskEngineStatus, string[]>,
|
||||
UseQueryOptions<unknown, unknown, RiskEngineStatusResponse, string[]>,
|
||||
'refetchInterval' | 'structuralSharing'
|
||||
> = {}
|
||||
) => {
|
||||
const isNewRiskScoreModuleAvailable = useIsExperimentalFeatureEnabled('riskScoringRoutesEnabled');
|
||||
const { fetchRiskEngineStatus } = useEntityAnalyticsRoutes();
|
||||
return useQuery(
|
||||
FETCH_RISK_ENGINE_STATUS,
|
||||
async ({ signal }) => {
|
||||
if (!isNewRiskScoreModuleAvailable) {
|
||||
return {
|
||||
isUpdateAvailable: false,
|
||||
isNewRiskScoreModuleInstalled: false,
|
||||
isNewRiskScoreModuleAvailable,
|
||||
risk_engine_status: null,
|
||||
legacy_risk_engine_status: null,
|
||||
risk_engine_task_status: null,
|
||||
};
|
||||
}
|
||||
const response = await fetchRiskEngineStatus({ signal });
|
||||
const isUpdateAvailable =
|
||||
response?.legacy_risk_engine_status === RiskEngineStatusEnum.ENABLED &&
|
||||
response.risk_engine_status === RiskEngineStatusEnum.NOT_INSTALLED;
|
||||
const isNewRiskScoreModuleInstalled =
|
||||
response.risk_engine_status !== RiskEngineStatusEnum.NOT_INSTALLED;
|
||||
return {
|
||||
isUpdateAvailable,
|
||||
isNewRiskScoreModuleInstalled,
|
||||
isNewRiskScoreModuleAvailable,
|
||||
...response,
|
||||
};
|
||||
},
|
||||
async ({ signal }) => fetchRiskEngineStatus({ signal }),
|
||||
queryOptions
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,13 +8,21 @@
|
|||
import { waitFor, renderHook } from '@testing-library/react';
|
||||
import { useRiskScore } from './use_risk_score';
|
||||
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 { useRiskScoreFeatureStatus } from './use_risk_score_feature_status';
|
||||
import { useIsNewRiskScoreModuleInstalled } from './use_risk_engine_status';
|
||||
import { RiskScoreEntity } 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';
|
||||
jest.mock('../../../common/components/ml/hooks/use_ml_capabilities', () => ({
|
||||
useMlCapabilities: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../helper_hooks', () => ({
|
||||
useHasSecurityCapability: jest.fn().mockReturnValue(true),
|
||||
}));
|
||||
|
||||
jest.mock('../../../common/containers/use_search_strategy', () => ({
|
||||
useSearchStrategy: jest.fn(),
|
||||
}));
|
||||
|
@ -24,38 +32,27 @@ jest.mock('../../../common/hooks/use_space_id', () => ({
|
|||
}));
|
||||
|
||||
jest.mock('../../../common/hooks/use_app_toasts');
|
||||
jest.mock('./use_risk_score_feature_status');
|
||||
jest.mock('./use_risk_engine_status', () => ({
|
||||
useRiskEngineStatus: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('./use_risk_engine_status');
|
||||
|
||||
const mockUseIsNewRiskScoreModuleInstalled = useIsNewRiskScoreModuleInstalled as jest.Mock;
|
||||
const mockUseRiskScoreFeatureStatus = useRiskScoreFeatureStatus as jest.Mock;
|
||||
const mockUseMlCapabilities = useMlCapabilities as jest.Mock;
|
||||
const mockUseSearchStrategy = useSearchStrategy as jest.Mock;
|
||||
const mockUseRiskEngineStatus = useRiskEngineStatus as jest.Mock;
|
||||
const mockSearch = jest.fn();
|
||||
|
||||
let appToastsMock: jest.Mocked<ReturnType<typeof useAppToastsMock.create>>;
|
||||
|
||||
const defaultRiskScoreModuleStatus = {
|
||||
isLoading: false,
|
||||
installed: false,
|
||||
};
|
||||
|
||||
const defaultFeatureStatus = {
|
||||
isLoading: false,
|
||||
isDeprecated: false,
|
||||
isAuthorized: true,
|
||||
isEnabled: true,
|
||||
refetch: () => {},
|
||||
};
|
||||
const defaultRisk = {
|
||||
data: undefined,
|
||||
error: undefined,
|
||||
inspect: {},
|
||||
isInspected: false,
|
||||
isAuthorized: true,
|
||||
isModuleEnabled: true,
|
||||
isDeprecated: false,
|
||||
hasEngineBeenInstalled: false,
|
||||
totalCount: 0,
|
||||
};
|
||||
|
||||
const defaultSearchResponse = {
|
||||
loading: false,
|
||||
result: {
|
||||
|
@ -67,6 +64,25 @@ const defaultSearchResponse = {
|
|||
inspect: {},
|
||||
error: undefined,
|
||||
};
|
||||
|
||||
const makeLicenseInvalid = () => {
|
||||
mockUseMlCapabilities.mockClear();
|
||||
mockUseMlCapabilities.mockReturnValue({
|
||||
isPlatinumOrTrialLicense: false,
|
||||
});
|
||||
};
|
||||
|
||||
const mockRiskEngineStatus = (status: RiskEngineStatusResponse['risk_engine_status']) => {
|
||||
mockUseRiskEngineStatus.mockClear();
|
||||
mockUseRiskEngineStatus.mockReturnValue({
|
||||
data: {
|
||||
risk_engine_status: status,
|
||||
risk_engine_task_status: { status: 'idle', runAt: '2021-09-29T15:00:00Z' },
|
||||
} as RiskEngineStatusResponse,
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
});
|
||||
};
|
||||
describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
|
||||
'useRiskScore entityType: %s',
|
||||
(riskEntity) => {
|
||||
|
@ -74,32 +90,15 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
|
|||
jest.clearAllMocks();
|
||||
appToastsMock = useAppToastsMock.create();
|
||||
(useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
|
||||
mockUseRiskScoreFeatureStatus.mockReturnValue(defaultFeatureStatus);
|
||||
mockUseSearchStrategy.mockReturnValue(defaultSearchResponse);
|
||||
mockUseIsNewRiskScoreModuleInstalled.mockReturnValue(defaultRiskScoreModuleStatus);
|
||||
mockUseMlCapabilities.mockReturnValue({
|
||||
isPlatinumOrTrialLicense: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('does not search if license is not valid', () => {
|
||||
mockUseRiskScoreFeatureStatus.mockReturnValue({
|
||||
...defaultFeatureStatus,
|
||||
isAuthorized: false,
|
||||
});
|
||||
const { result } = renderHook(() => useRiskScore({ riskEntity }), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
expect(mockSearch).not.toHaveBeenCalled();
|
||||
expect(result.current).toEqual({
|
||||
loading: false,
|
||||
...defaultRisk,
|
||||
isAuthorized: false,
|
||||
refetch: result.current.refetch,
|
||||
});
|
||||
});
|
||||
test('does not search if feature is not enabled', () => {
|
||||
mockUseRiskScoreFeatureStatus.mockReturnValue({
|
||||
...defaultFeatureStatus,
|
||||
isEnabled: false,
|
||||
});
|
||||
makeLicenseInvalid();
|
||||
mockRiskEngineStatus('ENABLED');
|
||||
|
||||
const { result } = renderHook(() => useRiskScore({ riskEntity }), {
|
||||
wrapper: TestProviders,
|
||||
|
@ -108,34 +107,26 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
|
|||
expect(result.current).toEqual({
|
||||
loading: false,
|
||||
...defaultRisk,
|
||||
isModuleEnabled: false,
|
||||
hasEngineBeenInstalled: true,
|
||||
isAuthorized: false,
|
||||
refetch: result.current.refetch,
|
||||
});
|
||||
});
|
||||
|
||||
test('does not search if index is deprecated ', () => {
|
||||
mockUseRiskScoreFeatureStatus.mockReturnValue({
|
||||
...defaultFeatureStatus,
|
||||
isDeprecated: true,
|
||||
});
|
||||
const { result } = renderHook(() => useRiskScore({ riskEntity, skip: true }), {
|
||||
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,
|
||||
isDeprecated: true,
|
||||
refetch: result.current.refetch,
|
||||
});
|
||||
});
|
||||
|
||||
test('handle index not found error', () => {
|
||||
mockUseRiskScoreFeatureStatus.mockReturnValue({
|
||||
...defaultFeatureStatus,
|
||||
isDeprecated: false,
|
||||
isEnabled: false,
|
||||
});
|
||||
mockRiskEngineStatus('ENABLED');
|
||||
mockUseSearchStrategy.mockReturnValue({
|
||||
...defaultSearchResponse,
|
||||
error: {
|
||||
|
@ -152,7 +143,7 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
|
|||
expect(result.current).toEqual({
|
||||
loading: false,
|
||||
...defaultRisk,
|
||||
isModuleEnabled: false,
|
||||
hasEngineBeenInstalled: true,
|
||||
refetch: result.current.refetch,
|
||||
error: {
|
||||
attributes: {
|
||||
|
@ -165,6 +156,7 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
|
|||
});
|
||||
|
||||
test('show error toast', () => {
|
||||
mockRiskEngineStatus('ENABLED');
|
||||
const error = new Error();
|
||||
mockUseSearchStrategy.mockReturnValue({
|
||||
...defaultSearchResponse,
|
||||
|
@ -178,40 +170,23 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
|
|||
});
|
||||
});
|
||||
|
||||
test('runs search if feature is enabled and not deprecated', () => {
|
||||
test('runs search if engine is enabled', () => {
|
||||
mockRiskEngineStatus('ENABLED');
|
||||
renderHook(() => useRiskScore({ riskEntity }), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
|
||||
expect(mockSearch).toHaveBeenCalledTimes(1);
|
||||
expect(mockSearch).toHaveBeenCalledWith({
|
||||
defaultIndex: [`ml_${riskEntity}_risk_score_latest_default`],
|
||||
defaultIndex: [`risk-score.risk-score-latest-default`],
|
||||
factoryQueryType: `${riskEntity}sRiskScore`,
|
||||
riskScoreEntity: riskEntity,
|
||||
includeAlertsCount: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('runs search with new index if feature is enabled and not deprecated and new module installed', () => {
|
||||
mockUseIsNewRiskScoreModuleInstalled.mockReturnValue({
|
||||
...defaultRiskScoreModuleStatus,
|
||||
installed: true,
|
||||
});
|
||||
test('returns result', async () => {
|
||||
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('return result', async () => {
|
||||
mockUseSearchStrategy.mockReturnValue({
|
||||
...defaultSearchResponse,
|
||||
result: {
|
||||
|
@ -226,6 +201,7 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
|
|||
expect(result.current).toEqual({
|
||||
loading: false,
|
||||
...defaultRisk,
|
||||
hasEngineBeenInstalled: true,
|
||||
data: [],
|
||||
refetch: result.current.refetch,
|
||||
});
|
||||
|
|
|
@ -8,24 +8,20 @@
|
|||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useRiskScoreFeatureStatus } from './use_risk_score_feature_status';
|
||||
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,
|
||||
getUserRiskIndex,
|
||||
RiskScoreEntity,
|
||||
getHostRiskIndex,
|
||||
} from '../../../../common/search_strategy';
|
||||
import { RiskQueries, RiskScoreEntity } 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 { useSpaceId } from '../../../common/hooks/use_space_id';
|
||||
import { useSearchStrategy } from '../../../common/containers/use_search_strategy';
|
||||
import { useIsNewRiskScoreModuleInstalled } from './use_risk_engine_status';
|
||||
import { useGetDefaulRiskIndex } 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:
|
||||
|
@ -37,9 +33,8 @@ export interface RiskScoreState<T extends RiskScoreEntity.host | RiskScoreEntity
|
|||
isInspected: boolean;
|
||||
refetch: inputsModel.Refetch;
|
||||
totalCount: number;
|
||||
isModuleEnabled: boolean;
|
||||
isAuthorized: boolean;
|
||||
isDeprecated: boolean;
|
||||
hasEngineBeenInstalled: boolean;
|
||||
loading: boolean;
|
||||
error: unknown;
|
||||
}
|
||||
|
@ -81,30 +76,16 @@ export const useRiskScore = <T extends RiskScoreEntity.host | RiskScoreEntity.us
|
|||
riskEntity,
|
||||
includeAlertsCount = false,
|
||||
}: UseRiskScore<T>): RiskScoreState<T> => {
|
||||
const spaceId = useSpaceId();
|
||||
const { installed: isNewRiskScoreModuleInstalled, isLoading: riskScoreStatusLoading } =
|
||||
useIsNewRiskScoreModuleInstalled();
|
||||
const defaultIndex =
|
||||
spaceId && !riskScoreStatusLoading && isNewRiskScoreModuleInstalled !== undefined
|
||||
? riskEntity === RiskScoreEntity.host
|
||||
? getHostRiskIndex(spaceId, onlyLatest, isNewRiskScoreModuleInstalled)
|
||||
: getUserRiskIndex(spaceId, onlyLatest, isNewRiskScoreModuleInstalled)
|
||||
: undefined;
|
||||
const defaultIndex = useGetDefaulRiskIndex(riskEntity, onlyLatest);
|
||||
const factoryQueryType =
|
||||
riskEntity === RiskScoreEntity.host ? RiskQueries.hostsRiskScore : RiskQueries.usersRiskScore;
|
||||
|
||||
const { querySize, cursorStart } = pagination || {};
|
||||
|
||||
const { addError } = useAppToasts();
|
||||
|
||||
const {
|
||||
isDeprecated,
|
||||
isEnabled,
|
||||
isAuthorized,
|
||||
isLoading: isDeprecatedLoading,
|
||||
refetch: refetchDeprecated,
|
||||
} = useRiskScoreFeatureStatus(riskEntity, defaultIndex);
|
||||
|
||||
const { isPlatinumOrTrialLicense } = useMlCapabilities();
|
||||
const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics');
|
||||
const isAuthorized = isPlatinumOrTrialLicense && hasEntityAnalyticsCapability;
|
||||
const { data: riskEngineStatus, isFetching: isStatusLoading } = useRiskEngineStatus();
|
||||
const hasEngineBeenInstalled = riskEngineStatus?.risk_engine_status !== 'NOT_INSTALLED';
|
||||
const {
|
||||
loading,
|
||||
result: response,
|
||||
|
@ -115,15 +96,14 @@ export const useRiskScore = <T extends RiskScoreEntity.host | RiskScoreEntity.us
|
|||
} = useSearchStrategy<RiskQueries.hostsRiskScore | RiskQueries.usersRiskScore>({
|
||||
factoryQueryType,
|
||||
initialResult,
|
||||
abort: skip,
|
||||
abort: skip || !hasEngineBeenInstalled || isStatusLoading || !isAuthorized,
|
||||
showErrorToast: false,
|
||||
});
|
||||
const refetchAll = useCallback(() => {
|
||||
if (defaultIndex) {
|
||||
refetchDeprecated(defaultIndex);
|
||||
refetch();
|
||||
}
|
||||
}, [defaultIndex, refetch, refetchDeprecated]);
|
||||
}, [defaultIndex, refetch]);
|
||||
|
||||
const riskScoreResponse = useMemo(
|
||||
() => ({
|
||||
|
@ -132,19 +112,17 @@ export const useRiskScore = <T extends RiskScoreEntity.host | RiskScoreEntity.us
|
|||
refetch: refetchAll,
|
||||
totalCount: response.totalCount,
|
||||
isAuthorized,
|
||||
isDeprecated,
|
||||
isModuleEnabled: isEnabled,
|
||||
isInspected: false,
|
||||
hasEngineBeenInstalled,
|
||||
error,
|
||||
}),
|
||||
[
|
||||
inspect,
|
||||
isDeprecated,
|
||||
isEnabled,
|
||||
isAuthorized,
|
||||
refetchAll,
|
||||
response.data,
|
||||
response.totalCount,
|
||||
inspect,
|
||||
refetchAll,
|
||||
isAuthorized,
|
||||
hasEngineBeenInstalled,
|
||||
error,
|
||||
]
|
||||
);
|
||||
|
@ -201,19 +179,12 @@ export const useRiskScore = <T extends RiskScoreEntity.host | RiskScoreEntity.us
|
|||
}, [addError, error]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!skip &&
|
||||
!isDeprecatedLoading &&
|
||||
riskScoreRequest != null &&
|
||||
isAuthorized &&
|
||||
isEnabled &&
|
||||
!isDeprecated
|
||||
) {
|
||||
if (!skip && riskScoreRequest != null && isAuthorized && hasEngineBeenInstalled) {
|
||||
search(riskScoreRequest);
|
||||
}
|
||||
}, [isEnabled, isDeprecated, isAuthorized, isDeprecatedLoading, riskScoreRequest, search, skip]);
|
||||
}, [hasEngineBeenInstalled, isAuthorized, riskScoreRequest, search, skip]);
|
||||
|
||||
const result = { ...riskScoreResponse, loading: loading || isDeprecatedLoading };
|
||||
const result = { ...riskScoreResponse, loading: loading || isStatusLoading };
|
||||
|
||||
return result;
|
||||
};
|
||||
|
|
|
@ -1,126 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
|
||||
import { useRiskScoreFeatureStatus } from './use_risk_score_feature_status';
|
||||
import { RiskScoreEntity } from '../../../../common/search_strategy';
|
||||
import { useFetch } from '../../../common/hooks/use_fetch';
|
||||
import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities';
|
||||
import { useHasSecurityCapability } from '../../../helper_hooks';
|
||||
|
||||
jest.mock('../../../common/hooks/use_fetch');
|
||||
jest.mock('../../../common/components/ml/hooks/use_ml_capabilities');
|
||||
jest.mock('../../../helper_hooks');
|
||||
|
||||
const mockFetch = jest.fn();
|
||||
const mockUseMlCapabilities = useMlCapabilities as jest.Mock;
|
||||
const mockUseFetch = useFetch as jest.Mock;
|
||||
const mockUseHasSecurityCapability = useHasSecurityCapability as jest.Mock;
|
||||
|
||||
describe(`risk score feature status`, () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockUseMlCapabilities.mockReturnValue({ isPlatinumOrTrialLicense: true });
|
||||
mockUseFetch.mockReturnValue(defaultFetch);
|
||||
mockUseHasSecurityCapability.mockReturnValue(true);
|
||||
});
|
||||
|
||||
const defaultFetch = {
|
||||
data: undefined,
|
||||
error: undefined,
|
||||
fetch: mockFetch,
|
||||
isLoading: false,
|
||||
refetch: () => {},
|
||||
};
|
||||
const defaultResult = {
|
||||
error: undefined,
|
||||
isDeprecated: true,
|
||||
isAuthorized: true,
|
||||
isEnabled: true,
|
||||
isLoading: true,
|
||||
};
|
||||
|
||||
test('does not search if license is not valid, and initial isDeprecated state is false', () => {
|
||||
mockUseMlCapabilities.mockReturnValue({ isPlatinumOrTrialLicense: false });
|
||||
const { result } = renderHook(
|
||||
() => useRiskScoreFeatureStatus(RiskScoreEntity.host, 'the_right_one'),
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
expect(result.current).toEqual({
|
||||
...defaultResult,
|
||||
isAuthorized: false,
|
||||
isDeprecated: false,
|
||||
isEnabled: false,
|
||||
refetch: result.current.refetch,
|
||||
});
|
||||
});
|
||||
|
||||
test("does not search if the user doesn't has entity analytics capability", () => {
|
||||
mockUseHasSecurityCapability.mockReturnValue(false);
|
||||
const { result } = renderHook(
|
||||
() => useRiskScoreFeatureStatus(RiskScoreEntity.host, 'the_right_one'),
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
expect(result.current).toEqual({
|
||||
...defaultResult,
|
||||
isAuthorized: false,
|
||||
isDeprecated: false,
|
||||
isEnabled: false,
|
||||
refetch: result.current.refetch,
|
||||
});
|
||||
});
|
||||
|
||||
test('runs search if feature is enabled, and initial isDeprecated state is true', () => {
|
||||
const { result } = renderHook(
|
||||
() => useRiskScoreFeatureStatus(RiskScoreEntity.host, 'the_right_one'),
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
expect(mockFetch).toHaveBeenCalledWith({
|
||||
query: { entity: RiskScoreEntity.host, indexName: 'the_right_one' },
|
||||
});
|
||||
expect(result.current).toEqual({
|
||||
...defaultResult,
|
||||
refetch: result.current.refetch,
|
||||
});
|
||||
});
|
||||
|
||||
test('updates state after search returns isDeprecated = false', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
() => useRiskScoreFeatureStatus(RiskScoreEntity.host, 'the_right_one'),
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
expect(result.current).toEqual({
|
||||
...defaultResult,
|
||||
refetch: result.current.refetch,
|
||||
});
|
||||
mockUseFetch.mockReturnValue({
|
||||
...defaultFetch,
|
||||
data: {
|
||||
isDeprecated: false,
|
||||
isEnabled: true,
|
||||
},
|
||||
});
|
||||
act(() => rerender());
|
||||
expect(result.current).toEqual({
|
||||
...defaultResult,
|
||||
isDeprecated: false,
|
||||
refetch: result.current.refetch,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,74 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities';
|
||||
import { REQUEST_NAMES, useFetch } from '../../../common/hooks/use_fetch';
|
||||
import type { RiskScoreEntity } from '../../../../common/search_strategy';
|
||||
import { useHasSecurityCapability } from '../../../helper_hooks';
|
||||
import { useEntityAnalyticsRoutes } from '../api';
|
||||
|
||||
interface RiskScoresFeatureStatus {
|
||||
error: unknown;
|
||||
// Is transform index an old version?
|
||||
isDeprecated: boolean;
|
||||
// Does the transform index exist?
|
||||
isEnabled: boolean;
|
||||
// Does the user has the authorization for the risk score feature?
|
||||
isAuthorized: boolean;
|
||||
isLoading: boolean;
|
||||
refetch: (indexName: string) => void;
|
||||
}
|
||||
|
||||
export const useRiskScoreFeatureStatus = (
|
||||
riskEntity: RiskScoreEntity.host | RiskScoreEntity.user,
|
||||
defaultIndex?: string
|
||||
): RiskScoresFeatureStatus => {
|
||||
const { isPlatinumOrTrialLicense, capabilitiesFetched } = useMlCapabilities();
|
||||
const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics');
|
||||
const isAuthorized = isPlatinumOrTrialLicense && hasEntityAnalyticsCapability;
|
||||
const { getRiskScoreIndexStatus } = useEntityAnalyticsRoutes();
|
||||
|
||||
const { fetch, data, isLoading, error } = useFetch(
|
||||
REQUEST_NAMES.GET_RISK_SCORE_DEPRECATED,
|
||||
getRiskScoreIndexStatus
|
||||
);
|
||||
|
||||
const response = useMemo(
|
||||
// if authorized is true, let isDeprecated = true so the actual
|
||||
// risk score fetch is not called until this check is complete
|
||||
() => (data ? data : { isDeprecated: isAuthorized, isEnabled: isAuthorized }),
|
||||
// isAuthorized is initial state, not update requirement
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[data]
|
||||
);
|
||||
|
||||
const searchIndexStatus = useCallback(
|
||||
(indexName: string) => {
|
||||
if (isAuthorized) {
|
||||
fetch({
|
||||
query: { indexName, entity: riskEntity },
|
||||
});
|
||||
}
|
||||
},
|
||||
[isAuthorized, fetch, riskEntity]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultIndex != null) {
|
||||
searchIndexStatus(defaultIndex);
|
||||
}
|
||||
}, [defaultIndex, searchIndexStatus]);
|
||||
|
||||
return {
|
||||
error,
|
||||
isLoading: isLoading || !capabilitiesFetched || defaultIndex == null,
|
||||
refetch: searchIndexStatus,
|
||||
isAuthorized,
|
||||
...response,
|
||||
};
|
||||
};
|
|
@ -8,24 +8,21 @@
|
|||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { RiskScoreEntity } from '../../../../common/search_strategy';
|
||||
import {
|
||||
getHostRiskIndex,
|
||||
getUserRiskIndex,
|
||||
RiskQueries,
|
||||
RiskSeverity,
|
||||
RiskScoreEntity,
|
||||
EMPTY_SEVERITY_COUNT,
|
||||
} from '../../../../common/search_strategy';
|
||||
import { isIndexNotFoundError } from '../../../common/utils/exceptions';
|
||||
import type { ESQuery } from '../../../../common/typed_json';
|
||||
import type { SeverityCount } from '../../components/severity/types';
|
||||
import { useSpaceId } from '../../../common/hooks/use_space_id';
|
||||
import { useSearchStrategy } from '../../../common/containers/use_search_strategy';
|
||||
import type { InspectResponse } from '../../../types';
|
||||
import type { inputsModel } from '../../../common/store';
|
||||
import { useAppToasts } from '../../../common/hooks/use_app_toasts';
|
||||
import { useIsNewRiskScoreModuleInstalled } from './use_risk_engine_status';
|
||||
import { useRiskScoreFeatureStatus } from './use_risk_score_feature_status';
|
||||
import { useGetDefaulRiskIndex } from '../../hooks/use_get_default_risk_index';
|
||||
import { useRiskEngineStatus } from './use_risk_engine_status';
|
||||
|
||||
interface RiskScoreKpi {
|
||||
error: unknown;
|
||||
|
@ -51,24 +48,13 @@ export const useRiskScoreKpi = ({
|
|||
timerange,
|
||||
}: UseRiskScoreKpiProps): RiskScoreKpi => {
|
||||
const { addError } = useAppToasts();
|
||||
const spaceId = useSpaceId();
|
||||
const { installed: isNewRiskScoreModuleInstalled, isLoading: riskScoreStatusLoading } =
|
||||
useIsNewRiskScoreModuleInstalled();
|
||||
const defaultIndex =
|
||||
spaceId && !riskScoreStatusLoading && isNewRiskScoreModuleInstalled !== undefined
|
||||
? riskEntity === RiskScoreEntity.host
|
||||
? getHostRiskIndex(spaceId, true, isNewRiskScoreModuleInstalled)
|
||||
: getUserRiskIndex(spaceId, true, isNewRiskScoreModuleInstalled)
|
||||
: undefined;
|
||||
|
||||
const defaultIndex = useGetDefaulRiskIndex(riskEntity);
|
||||
const {
|
||||
isDeprecated,
|
||||
isEnabled,
|
||||
isAuthorized,
|
||||
isLoading: isDeprecatedLoading,
|
||||
refetch: refetchFeatureStatus,
|
||||
} = useRiskScoreFeatureStatus(riskEntity, defaultIndex);
|
||||
|
||||
data: riskEngineStatus,
|
||||
isFetching: isStatusLoading,
|
||||
refetch: refetchEngineStatus,
|
||||
} = useRiskEngineStatus();
|
||||
const riskEngineHasBeenEnabled = riskEngineStatus?.risk_engine_status !== 'NOT_INSTALLED';
|
||||
const { loading, result, search, refetch, inspect, error } =
|
||||
useSearchStrategy<RiskQueries.kpiRiskScore>({
|
||||
factoryQueryType: RiskQueries.kpiRiskScore,
|
||||
|
@ -87,14 +73,7 @@ export const useRiskScoreKpi = ({
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!skip &&
|
||||
defaultIndex &&
|
||||
!isDeprecatedLoading &&
|
||||
isAuthorized &&
|
||||
isEnabled &&
|
||||
!isDeprecated
|
||||
) {
|
||||
if (!skip && defaultIndex && !isStatusLoading && riskEngineHasBeenEnabled) {
|
||||
search({
|
||||
filterQuery,
|
||||
defaultIndex: [defaultIndex],
|
||||
|
@ -109,18 +88,16 @@ export const useRiskScoreKpi = ({
|
|||
skip,
|
||||
riskEntity,
|
||||
requestTimerange,
|
||||
isEnabled,
|
||||
isDeprecated,
|
||||
isAuthorized,
|
||||
isDeprecatedLoading,
|
||||
isStatusLoading,
|
||||
riskEngineHasBeenEnabled,
|
||||
]);
|
||||
|
||||
const refetchAll = useCallback(() => {
|
||||
if (defaultIndex) {
|
||||
refetchFeatureStatus(defaultIndex);
|
||||
refetchEngineStatus();
|
||||
refetch();
|
||||
}
|
||||
}, [defaultIndex, refetch, refetchFeatureStatus]);
|
||||
}, [defaultIndex, refetch, refetchEngineStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { HostRiskScore, UserRiskScore } from '../../../common/search_strategy';
|
||||
|
||||
export interface HostRisk {
|
||||
loading: boolean;
|
||||
isModuleEnabled: boolean;
|
||||
result?: HostRiskScore[];
|
||||
}
|
||||
|
||||
export interface UserRisk {
|
||||
loading: boolean;
|
||||
isModuleEnabled: boolean;
|
||||
result?: UserRiskScore[];
|
||||
}
|
|
@ -48,7 +48,6 @@ describe('ScheduleRiskEngineCallout', () => {
|
|||
it('should show the remaining time for the next risk engine run', async () => {
|
||||
mockUseRiskEngineStatus.mockReturnValue({
|
||||
data: {
|
||||
isNewRiskScoreModuleInstalled: true,
|
||||
risk_engine_status: RiskEngineStatusEnum.ENABLED,
|
||||
risk_engine_task_status: {
|
||||
status: 'idle',
|
||||
|
@ -70,7 +69,6 @@ describe('ScheduleRiskEngineCallout', () => {
|
|||
it('should show "now running" status when the risk engine status is "running"', () => {
|
||||
mockUseRiskEngineStatus.mockReturnValue({
|
||||
data: {
|
||||
isNewRiskScoreModuleInstalled: true,
|
||||
risk_engine_status: RiskEngineStatusEnum.ENABLED,
|
||||
risk_engine_task_status: {
|
||||
status: 'running',
|
||||
|
@ -89,7 +87,6 @@ describe('ScheduleRiskEngineCallout', () => {
|
|||
it('should show "now running" status when the next schedule run is in the past', () => {
|
||||
mockUseRiskEngineStatus.mockReturnValue({
|
||||
data: {
|
||||
isNewRiskScoreModuleInstalled: true,
|
||||
risk_engine_status: RiskEngineStatusEnum.ENABLED,
|
||||
risk_engine_task_status: {
|
||||
status: 'idle',
|
||||
|
@ -110,7 +107,6 @@ describe('ScheduleRiskEngineCallout', () => {
|
|||
it('should update the count down time when time has passed', () => {
|
||||
mockUseRiskEngineStatus.mockReturnValueOnce({
|
||||
data: {
|
||||
isNewRiskScoreModuleInstalled: true,
|
||||
risk_engine_status: RiskEngineStatusEnum.ENABLED,
|
||||
risk_engine_task_status: {
|
||||
status: 'idle',
|
||||
|
@ -127,7 +123,6 @@ describe('ScheduleRiskEngineCallout', () => {
|
|||
// simulate useQuery re-render after fetching data
|
||||
mockUseRiskEngineStatus.mockReturnValueOnce({
|
||||
data: {
|
||||
isNewRiskScoreModuleInstalled: true,
|
||||
risk_engine_status: RiskEngineStatusEnum.ENABLED,
|
||||
risk_engine_task_status: {
|
||||
status: 'idle',
|
||||
|
@ -143,7 +138,6 @@ describe('ScheduleRiskEngineCallout', () => {
|
|||
it('should call the run risk engine api when button is clicked', () => {
|
||||
mockUseRiskEngineStatus.mockReturnValue({
|
||||
data: {
|
||||
isNewRiskScoreModuleInstalled: true,
|
||||
risk_engine_status: RiskEngineStatusEnum.ENABLED,
|
||||
risk_engine_task_status: {
|
||||
status: 'idle',
|
||||
|
@ -163,9 +157,7 @@ describe('ScheduleRiskEngineCallout', () => {
|
|||
|
||||
it('should not show the callout if the risk engine is not installed', () => {
|
||||
mockUseRiskEngineStatus.mockReturnValue({
|
||||
data: {
|
||||
isNewRiskScoreModuleInstalled: false,
|
||||
},
|
||||
data: {},
|
||||
});
|
||||
|
||||
const { queryByTestId } = render(<ScheduleRiskEngineCallout />, {
|
||||
|
@ -178,7 +170,6 @@ describe('ScheduleRiskEngineCallout', () => {
|
|||
it('should not show the callout if the risk engine is disabled', () => {
|
||||
mockUseRiskEngineStatus.mockReturnValue({
|
||||
data: {
|
||||
isNewRiskScoreModuleInstalled: true,
|
||||
risk_engine_status: RiskEngineStatusEnum.DISABLED,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -61,10 +61,7 @@ export const ScheduleRiskEngineCallout: React.FC = () => {
|
|||
scheduleRiskEngineMutation();
|
||||
}, [scheduleRiskEngineMutation]);
|
||||
|
||||
if (
|
||||
!riskEngineStatus?.isNewRiskScoreModuleInstalled ||
|
||||
riskEngineStatus?.risk_engine_status !== RiskEngineStatusEnum.ENABLED
|
||||
) {
|
||||
if (riskEngineStatus?.risk_engine_status !== RiskEngineStatusEnum.ENABLED) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,66 +6,51 @@
|
|||
*/
|
||||
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 { useCheckSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_check_signal_index';
|
||||
import type { inputsModel } from '../../../common/store';
|
||||
import { RiskScoreHeaderTitle } from '../risk_score_onboarding/risk_score_header_title';
|
||||
import { HeaderSection } from '../../../common/components/header_section';
|
||||
import { EntityAnalyticsLearnMoreLink } from '../risk_score_onboarding/entity_analytics_doc_link';
|
||||
import { RiskScoreEnableButton } from '../risk_score_onboarding/risk_score_enable_button';
|
||||
import * as i18n from './translations';
|
||||
import { EntityAnalyticsLearnMoreLink } from '../entity_analytics_learn_more_link';
|
||||
import { RiskScoreHeaderTitle } from '../risk_score_header_title';
|
||||
import { SecuritySolutionLinkButton } from '../../../common/components/links';
|
||||
import { SecurityPageName } from '../../../../common/constants';
|
||||
|
||||
const EnableRiskScoreComponent = ({
|
||||
isDeprecated,
|
||||
isDisabled,
|
||||
entityType,
|
||||
refetch,
|
||||
timerange,
|
||||
}: {
|
||||
isDeprecated: boolean;
|
||||
isDisabled: boolean;
|
||||
entityType: RiskScoreEntity;
|
||||
refetch: inputsModel.Refetch;
|
||||
timerange: {
|
||||
from: string;
|
||||
to: string;
|
||||
};
|
||||
}) => {
|
||||
const { signalIndexExists } = useCheckSignalIndex();
|
||||
if (!isDeprecated && !isDisabled) {
|
||||
if (!isDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = isDeprecated
|
||||
? {
|
||||
cta: i18n.UPGRADE_RISK_SCORE(entityType),
|
||||
body: i18n.UPGRADE_RISK_SCORE_DESCRIPTION,
|
||||
}
|
||||
: {
|
||||
cta: i18n.ENABLE_RISK_SCORE(entityType),
|
||||
body: i18n.ENABLE_RISK_SCORE_DESCRIPTION(entityType),
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPanel hasBorder>
|
||||
<HeaderSection title={<RiskScoreHeaderTitle riskScoreEntity={entityType} />} titleSize="s" />
|
||||
<EuiEmptyPrompt
|
||||
title={<h2>{text.cta}</h2>}
|
||||
title={<h2>{i18n.ENABLE_RISK_SCORE(entityType)}</h2>}
|
||||
body={
|
||||
<>
|
||||
{text.body}
|
||||
{i18n.ENABLE_RISK_SCORE_DESCRIPTION(entityType)}
|
||||
{` `}
|
||||
<EntityAnalyticsLearnMoreLink />
|
||||
</>
|
||||
}
|
||||
actions={
|
||||
<EuiToolTip content={!signalIndexExists ? i18n.ENABLE_RISK_SCORE_POPOVER : null}>
|
||||
<RiskScoreEnableButton
|
||||
disabled={!signalIndexExists}
|
||||
refetch={refetch}
|
||||
riskScoreEntity={entityType}
|
||||
timerange={timerange}
|
||||
/>
|
||||
<EuiToolTip content={i18n.ENABLE_RISK_SCORE_POPOVER}>
|
||||
<SecuritySolutionLinkButton
|
||||
color="primary"
|
||||
fill
|
||||
deepLinkId={SecurityPageName.entityAnalyticsManagement}
|
||||
data-test-subj={`enable_risk_score`}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.riskScore.enableButtonTitle"
|
||||
defaultMessage="Enable"
|
||||
/>
|
||||
</SecuritySolutionLinkButton>
|
||||
</EuiToolTip>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -15,22 +15,6 @@ export const ENABLE_RISK_SCORE_POPOVER = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const UPGRADE_RISK_SCORE = (riskEntity: RiskScoreEntity) =>
|
||||
i18n.translate('xpack.securitySolution.enableRiskScore.upgradeRiskScore', {
|
||||
defaultMessage: 'Upgrade {riskEntity} Risk Score',
|
||||
values: {
|
||||
riskEntity: getRiskEntityTranslation(riskEntity),
|
||||
},
|
||||
});
|
||||
|
||||
export const UPGRADE_RISK_SCORE_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.riskDeprecated.entity.upgradeRiskScoreDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Current data is no longer supported. Please migrate your data and upgrade the module. The data might need an hour to be generated after enabling the module.',
|
||||
}
|
||||
);
|
||||
|
||||
export const ENABLE_RISK_SCORE = (riskEntity: RiskScoreEntity) =>
|
||||
i18n.translate('xpack.securitySolution.enableRiskScore.enableRiskScore', {
|
||||
defaultMessage: 'Enable {riskEntity} Risk Score',
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { EuiLink } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React from 'react';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
|
||||
const EntityAnalyticsLearnMoreLinkComponent = ({ title }: { title?: string | React.ReactNode }) => {
|
||||
const { docLinks } = useKibana().services;
|
|
@ -54,7 +54,7 @@ const defaultProps = {
|
|||
data: undefined,
|
||||
inspect: null,
|
||||
refetch: () => {},
|
||||
isModuleEnabled: true,
|
||||
hasEngineBeenInstalled: true,
|
||||
isAuthorized: true,
|
||||
loading: false,
|
||||
};
|
||||
|
@ -89,14 +89,14 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
|
|||
});
|
||||
|
||||
it('renders enable button when module is disable', () => {
|
||||
mockUseRiskScore.mockReturnValue({ ...defaultProps, isModuleEnabled: false });
|
||||
mockUseRiskScore.mockReturnValue({ ...defaultProps, hasEngineBeenInstalled: false });
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<EntityAnalyticsRiskScores riskEntity={riskEntity} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId(`enable_${riskEntity}_risk_score`)).toBeInTheDocument();
|
||||
expect(getByTestId(`enable_risk_score`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("doesn't render enable button when module is enable", () => {
|
||||
|
@ -106,7 +106,7 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
|
|||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(queryByTestId(`enable_${riskEntity}_risk_score`)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(`enable_risk_score`)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('queries when toggleStatus is true', () => {
|
||||
|
|
|
@ -21,9 +21,6 @@ import { useGlobalTime } from '../../../common/containers/use_global_time';
|
|||
import { InspectButtonContainer } from '../../../common/components/inspect';
|
||||
import { useQueryToggle } from '../../../common/containers/query_toggle';
|
||||
import { StyledBasicTable } from '../styled_basic_table';
|
||||
import { RiskScoreHeaderTitle } from '../risk_score_onboarding/risk_score_header_title';
|
||||
import { RiskScoresNoDataDetected } from '../risk_score_onboarding/risk_score_no_data_detected';
|
||||
import { useRefetchQueries } from '../../../common/hooks/use_refetch_queries';
|
||||
import { Loader } from '../../../common/components/loader';
|
||||
import { Panel } from '../../../common/components/panel';
|
||||
import { useEntityInfo } from './use_entity';
|
||||
|
@ -39,6 +36,8 @@ 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';
|
||||
import { RiskScoresNoDataDetected } from '../risk_score_no_data_detected';
|
||||
import { RiskScoreHeaderTitle } from '../risk_score_header_title';
|
||||
|
||||
export const ENTITY_RISK_SCORE_TABLE_ID = 'entity-risk-score-table';
|
||||
|
||||
|
@ -131,9 +130,8 @@ const EntityAnalyticsRiskScoresComponent = ({ riskEntity }: { riskEntity: RiskSc
|
|||
loading: isTableLoading,
|
||||
inspect,
|
||||
refetch,
|
||||
isDeprecated,
|
||||
isAuthorized,
|
||||
isModuleEnabled,
|
||||
hasEngineBeenInstalled,
|
||||
} = useRiskScore({
|
||||
filterQuery,
|
||||
skip: !toggleStatus,
|
||||
|
@ -159,18 +157,13 @@ const EntityAnalyticsRiskScoresComponent = ({ riskEntity }: { riskEntity: RiskSc
|
|||
setUpdatedAt(Date.now());
|
||||
}, [isTableLoading, isKpiLoading]); // Update the time when data loads
|
||||
|
||||
const refreshPage = useRefetchQueries();
|
||||
|
||||
const privileges = useMissingRiskEnginePrivileges(['read']);
|
||||
|
||||
if (!isAuthorized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const status = {
|
||||
isDisabled: !isModuleEnabled && !isTableLoading,
|
||||
isDeprecated: isDeprecated && !isTableLoading,
|
||||
};
|
||||
const isDisabled = !hasEngineBeenInstalled && !isTableLoading;
|
||||
|
||||
if (!privileges.isLoading && !privileges.hasAllRequiredPrivileges) {
|
||||
return (
|
||||
|
@ -180,19 +173,12 @@ const EntityAnalyticsRiskScoresComponent = ({ riskEntity }: { riskEntity: RiskSc
|
|||
);
|
||||
}
|
||||
|
||||
if (status.isDisabled || status.isDeprecated) {
|
||||
return (
|
||||
<EnableRiskScore
|
||||
{...status}
|
||||
entityType={riskEntity}
|
||||
refetch={refreshPage}
|
||||
timerange={timerange}
|
||||
/>
|
||||
);
|
||||
if (isDisabled) {
|
||||
return <EnableRiskScore isDisabled={isDisabled} entityType={riskEntity} />;
|
||||
}
|
||||
|
||||
if (isModuleEnabled && selectedSeverity.length === 0 && data && data.length === 0) {
|
||||
return <RiskScoresNoDataDetected entityType={riskEntity} refetch={refreshPage} />;
|
||||
if (hasEngineBeenInstalled && selectedSeverity.length === 0 && data && data.length === 0) {
|
||||
return <RiskScoresNoDataDetected entityType={riskEntity} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -17,7 +17,10 @@ import {
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { UseQueryResult } from '@tanstack/react-query';
|
||||
import type { GetEntityStoreStatusResponse } from '../../../../../common/api/entity_analytics/entity_store/status.gen';
|
||||
import type { StoreStatus } from '../../../../../common/api/entity_analytics';
|
||||
import type {
|
||||
RiskEngineStatusResponse,
|
||||
StoreStatus,
|
||||
} from '../../../../../common/api/entity_analytics';
|
||||
import { RiskEngineStatusEnum } from '../../../../../common/api/entity_analytics';
|
||||
import { useInitRiskEngineMutation } from '../../../api/hooks/use_init_risk_engine_mutation';
|
||||
import { useEnableEntityStoreMutation } from '../hooks/use_entity_store';
|
||||
|
@ -34,11 +37,10 @@ import {
|
|||
import type { Enablements } from './enablement_modal';
|
||||
import { EntityStoreEnablementModal } from './enablement_modal';
|
||||
import dashboardEnableImg from '../../../images/entity_store_dashboard.png';
|
||||
import type { RiskEngineStatus } from '../../../api/hooks/use_risk_engine_status';
|
||||
|
||||
interface EnableEntityStorePanelProps {
|
||||
state: {
|
||||
riskEngine: UseQueryResult<RiskEngineStatus>;
|
||||
riskEngine: UseQueryResult<RiskEngineStatusResponse>;
|
||||
entityStore: UseQueryResult<GetEntityStoreStatusResponse>;
|
||||
};
|
||||
}
|
||||
|
@ -181,7 +183,7 @@ export const EnablementPanel: React.FC<EnableEntityStorePanelProps> = ({ state }
|
|||
|
||||
const getEnablementTexts = (
|
||||
entityStoreStatus?: StoreStatus,
|
||||
riskEngineStatus?: RiskEngineStatus['risk_engine_status']
|
||||
riskEngineStatus?: RiskEngineStatusResponse['risk_engine_status']
|
||||
): [string, string] => {
|
||||
if (
|
||||
(entityStoreStatus === 'not_installed' || entityStoreStatus === 'stopped') &&
|
||||
|
|
|
@ -47,7 +47,7 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
|
|||
isInspected: false,
|
||||
totalCount: 0,
|
||||
refetch: jest.fn(),
|
||||
isModuleEnabled: true,
|
||||
hasEngineBeenInstalled: true,
|
||||
});
|
||||
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() });
|
||||
});
|
||||
|
|
|
@ -5,37 +5,27 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useUpsellingComponent } from '../../../common/hooks/use_upselling';
|
||||
import { RISKY_HOSTS_DASHBOARD_TITLE, RISKY_USERS_DASHBOARD_TITLE } from '../risk_score/constants';
|
||||
import { EnableRiskScore } from '../enable_risk_score';
|
||||
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import type { State } from '../../../common/store';
|
||||
import { hostsModel, hostsSelectors } from '../../../explore/hosts/store';
|
||||
import { usersSelectors } from '../../../explore/users/store';
|
||||
import { RiskInformationButtonEmpty } from '../risk_information';
|
||||
import * as i18n from './translations';
|
||||
|
||||
import { useQueryInspector } from '../../../common/components/page/manage_query';
|
||||
import { RiskScoreOverTime } from '../risk_score_over_time';
|
||||
import { TopRiskScoreContributors } from '../top_risk_score_contributors';
|
||||
import { TopRiskScoreContributorsAlerts } from '../top_risk_score_contributors_alerts';
|
||||
import { useQueryToggle } from '../../../common/containers/query_toggle';
|
||||
import type { HostRiskScore, UserRiskScore } from '../../../../common/search_strategy';
|
||||
import { buildEntityNameFilter, RiskScoreEntity } from '../../../../common/search_strategy';
|
||||
import type { UsersComponentsQueryProps } from '../../../explore/users/pages/navigation/types';
|
||||
import type { HostsComponentsQueryProps } from '../../../explore/hosts/pages/navigation/types';
|
||||
import { useDashboardHref } from '../../../common/hooks/use_dashboard_href';
|
||||
import { RiskScoresNoDataDetected } from '../risk_score_onboarding/risk_score_no_data_detected';
|
||||
import { useRiskEngineStatus } from '../../api/hooks/use_risk_engine_status';
|
||||
import { RiskScoreUpdatePanel } from '../risk_score_update_panel';
|
||||
import { HostRiskScoreQueryId, UserRiskScoreQueryId } from '../../common/utils';
|
||||
import { useRiskScore } from '../../api/hooks/use_risk_score';
|
||||
import { useMissingRiskEnginePrivileges } from '../../hooks/use_missing_risk_engine_privileges';
|
||||
import { RiskEnginePrivilegesCallOut } from '../risk_engine_privileges_callout';
|
||||
import { RiskScoresNoDataDetected } from '../risk_score_no_data_detected';
|
||||
|
||||
const StyledEuiFlexGroup = styled(EuiFlexGroup)`
|
||||
margin-top: ${({ theme }) => theme.eui.euiSizeL};
|
||||
|
@ -43,9 +33,6 @@ const StyledEuiFlexGroup = styled(EuiFlexGroup)`
|
|||
|
||||
type ComponentsQueryProps = HostsComponentsQueryProps | UsersComponentsQueryProps;
|
||||
|
||||
const getDashboardTitle = (riskEntity: RiskScoreEntity) =>
|
||||
riskEntity === RiskScoreEntity.host ? RISKY_HOSTS_DASHBOARD_TITLE : RISKY_USERS_DASHBOARD_TITLE;
|
||||
|
||||
const RiskDetailsTabBodyComponent: React.FC<
|
||||
Pick<ComponentsQueryProps, 'startDate' | 'endDate' | 'setQuery' | 'deleteQuery'> & {
|
||||
entityName: string;
|
||||
|
@ -66,8 +53,6 @@ const RiskDetailsTabBodyComponent: React.FC<
|
|||
: usersSelectors.userRiskScoreSeverityFilterSelector()(state)
|
||||
);
|
||||
|
||||
const buttonHref = useDashboardHref({ title: getDashboardTitle(riskEntity) });
|
||||
|
||||
const timerange = useMemo(
|
||||
() => ({
|
||||
from: startDate,
|
||||
|
@ -76,8 +61,6 @@ const RiskDetailsTabBodyComponent: React.FC<
|
|||
[startDate, endDate]
|
||||
);
|
||||
|
||||
const { toggleStatus: overTimeToggleStatus, setToggleStatus: setOverTimeToggleStatus } =
|
||||
useQueryToggle(`${queryId} overTime`);
|
||||
const { toggleStatus: contributorsToggleStatus, setToggleStatus: setContributorsToggleStatus } =
|
||||
useQueryToggle(`${queryId} contributors`);
|
||||
|
||||
|
@ -86,26 +69,14 @@ const RiskDetailsTabBodyComponent: React.FC<
|
|||
[entityName, riskEntity]
|
||||
);
|
||||
|
||||
const { data, loading, refetch, inspect, isDeprecated, isModuleEnabled } = useRiskScore({
|
||||
const { data, loading, refetch, inspect, hasEngineBeenInstalled } = useRiskScore({
|
||||
filterQuery,
|
||||
onlyLatest: false,
|
||||
riskEntity,
|
||||
skip: !overTimeToggleStatus && !contributorsToggleStatus,
|
||||
skip: !contributorsToggleStatus,
|
||||
timerange,
|
||||
});
|
||||
|
||||
const { data: riskScoreEngineStatus } = useRiskEngineStatus();
|
||||
|
||||
const rules = useMemo(() => {
|
||||
const lastRiskItem = data && data.length > 0 ? data[data.length - 1] : null;
|
||||
if (lastRiskItem) {
|
||||
return riskEntity === RiskScoreEntity.host
|
||||
? (lastRiskItem as HostRiskScore).host.risk.rule_risks
|
||||
: (lastRiskItem as UserRiskScore).user.risk.rule_risks;
|
||||
}
|
||||
return [];
|
||||
}, [data, riskEntity]);
|
||||
|
||||
useQueryInspector({
|
||||
queryId,
|
||||
loading,
|
||||
|
@ -122,13 +93,6 @@ const RiskDetailsTabBodyComponent: React.FC<
|
|||
[setContributorsToggleStatus]
|
||||
);
|
||||
|
||||
const toggleOverTimeQuery = useCallback(
|
||||
(status: boolean) => {
|
||||
setOverTimeToggleStatus(status);
|
||||
},
|
||||
[setOverTimeToggleStatus]
|
||||
);
|
||||
|
||||
const privileges = useMissingRiskEnginePrivileges();
|
||||
|
||||
const RiskScoreUpsell = useUpsellingComponent('entity_analytics_panel');
|
||||
|
@ -145,89 +109,32 @@ const RiskDetailsTabBodyComponent: React.FC<
|
|||
}
|
||||
|
||||
const status = {
|
||||
isDisabled: !isModuleEnabled && !loading,
|
||||
isDeprecated: isDeprecated && !loading,
|
||||
isDisabled: !hasEngineBeenInstalled && !loading,
|
||||
};
|
||||
|
||||
if (status.isDisabled || status.isDeprecated) {
|
||||
return (
|
||||
<EnableRiskScore
|
||||
{...status}
|
||||
entityType={riskEntity}
|
||||
refetch={refetch}
|
||||
timerange={timerange}
|
||||
/>
|
||||
);
|
||||
if (status.isDisabled) {
|
||||
return <EnableRiskScore {...status} entityType={riskEntity} />;
|
||||
}
|
||||
|
||||
if (isModuleEnabled && severitySelectionRedux.length === 0 && data && data.length === 0) {
|
||||
return <RiskScoresNoDataDetected entityType={riskEntity} refetch={refetch} />;
|
||||
if (hasEngineBeenInstalled && severitySelectionRedux.length === 0 && data && data.length === 0) {
|
||||
return <RiskScoresNoDataDetected entityType={riskEntity} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{riskScoreEngineStatus?.isUpdateAvailable && <RiskScoreUpdatePanel />}
|
||||
{riskScoreEngineStatus?.isNewRiskScoreModuleInstalled ? (
|
||||
<StyledEuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
{data?.[0] && (
|
||||
<TopRiskScoreContributorsAlerts
|
||||
toggleStatus={contributorsToggleStatus}
|
||||
toggleQuery={toggleContributorsQuery}
|
||||
riskScore={data[0]}
|
||||
riskEntity={riskEntity}
|
||||
loading={loading}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</StyledEuiFlexGroup>
|
||||
) : (
|
||||
<>
|
||||
<EuiFlexGroup direction="row">
|
||||
<EuiFlexItem grow={2}>
|
||||
<RiskScoreOverTime
|
||||
from={startDate}
|
||||
loading={loading}
|
||||
queryId={queryId}
|
||||
riskEntity={riskEntity}
|
||||
riskScore={data}
|
||||
title={i18n.RISK_SCORE_OVER_TIME(riskEntity)}
|
||||
to={endDate}
|
||||
toggleQuery={toggleOverTimeQuery}
|
||||
toggleStatus={overTimeToggleStatus}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={1}>
|
||||
<TopRiskScoreContributors
|
||||
loading={loading}
|
||||
queryId={queryId}
|
||||
toggleStatus={contributorsToggleStatus}
|
||||
toggleQuery={toggleContributorsQuery}
|
||||
rules={rules}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<StyledEuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
href={buttonHref}
|
||||
isDisabled={!buttonHref}
|
||||
data-test-subj={`risky-${riskEntity}s-view-dashboard-button`}
|
||||
target="_blank"
|
||||
iconType="popout"
|
||||
iconSide="right"
|
||||
>
|
||||
{i18n.VIEW_DASHBOARD_BUTTON}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<RiskInformationButtonEmpty riskEntity={riskEntity} />
|
||||
</EuiFlexItem>
|
||||
</StyledEuiFlexGroup>
|
||||
</>
|
||||
)}
|
||||
<StyledEuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
{data?.[0] && (
|
||||
<TopRiskScoreContributorsAlerts
|
||||
toggleStatus={contributorsToggleStatus}
|
||||
toggleQuery={toggleContributorsQuery}
|
||||
riskScore={data[0]}
|
||||
riskEntity={riskEntity}
|
||||
loading={loading}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</StyledEuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -28,6 +28,7 @@ import React from 'react';
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import { BETA } from '../../../common/translations';
|
||||
import * as i18n from './translations';
|
||||
import { useOnOpenCloseHandler } from '../../../helper_hooks';
|
||||
import { RiskScoreLevel } from '../severity/common';
|
||||
|
@ -37,9 +38,8 @@ import {
|
|||
CriticalityLevels,
|
||||
CriticalityModifiers,
|
||||
} from '../../../../common/entity_analytics/asset_criticality';
|
||||
import { EntityAnalyticsLearnMoreLink } from '../risk_score_onboarding/entity_analytics_doc_link';
|
||||
import { BETA } from '../risk_score_onboarding/translations';
|
||||
import { AssetCriticalityBadge } from '../asset_criticality';
|
||||
import { EntityAnalyticsLearnMoreLink } from '../entity_analytics_learn_more_link';
|
||||
|
||||
const SpacedOrderedList = styled.ol`
|
||||
li {
|
||||
|
|
|
@ -5,9 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export const RISKY_HOSTS_DASHBOARD_TITLE = 'Current Risk Score for Hosts';
|
||||
export const RISKY_USERS_DASHBOARD_TITLE = 'Current Risk Score for Users';
|
||||
|
||||
export const CELL_ACTIONS_TELEMETRY = {
|
||||
component: 'RiskScoreTable',
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
|
@ -13,14 +13,6 @@ import {
|
|||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiLoadingSpinner,
|
||||
EuiBadge,
|
||||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiText,
|
||||
EuiCallOut,
|
||||
EuiAccordion,
|
||||
|
@ -65,74 +57,6 @@ const RiskScoreErrorPanel = ({ errors }: { errors: string[] }) => (
|
|||
</>
|
||||
);
|
||||
|
||||
interface RiskScoreUpdateModalParams {
|
||||
isLoading: boolean;
|
||||
isVisible: boolean;
|
||||
closeModal: () => void;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
const RiskScoreUpdateModal = ({
|
||||
closeModal,
|
||||
isLoading,
|
||||
onConfirm,
|
||||
isVisible,
|
||||
}: RiskScoreUpdateModalParams) => {
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<EuiModal onClose={closeModal}>
|
||||
{isLoading ? (
|
||||
<EuiModalHeader>
|
||||
<EuiFlexGroup gutterSize="m" alignItems="center">
|
||||
<EuiLoadingSpinner size="m" />
|
||||
<EuiModalHeaderTitle>{i18n.UPDATING_RISK_ENGINE}</EuiModalHeaderTitle>
|
||||
</EuiFlexGroup>
|
||||
</EuiModalHeader>
|
||||
) : (
|
||||
<>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>{i18n.UPDATE_RISK_ENGINE_MODAL_TITLE}</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
<EuiText>
|
||||
<p>
|
||||
<b>{i18n.UPDATE_RISK_ENGINE_MODAL_EXISTING_USER_HOST_1}</b>
|
||||
{i18n.UPDATE_RISK_ENGINE_MODAL_EXISTING_USER_HOST_2}
|
||||
</p>
|
||||
<EuiSpacer size="s" />
|
||||
<p>
|
||||
<b>{i18n.UPDATE_RISK_ENGINE_MODAL_EXISTING_DATA_1}</b>
|
||||
{i18n.UPDATE_RISK_ENGINE_MODAL_EXISTING_DATA_2}
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer />
|
||||
</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty
|
||||
color="primary"
|
||||
data-test-subj="risk-score-update-cancel"
|
||||
onClick={closeModal}
|
||||
>
|
||||
{i18n.UPDATE_RISK_ENGINE_MODAL_BUTTON_NO}
|
||||
</EuiButtonEmpty>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
data-test-subj="risk-score-update-confirm"
|
||||
onClick={onConfirm}
|
||||
fill
|
||||
>
|
||||
{i18n.UPDATE_RISK_ENGINE_MODAL_BUTTON_YES}
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</>
|
||||
)}
|
||||
</EuiModal>
|
||||
);
|
||||
};
|
||||
|
||||
const RiskEngineHealth: React.FC<{ currentRiskEngineStatus?: RiskEngineStatus | null }> = ({
|
||||
currentRiskEngineStatus,
|
||||
}) => {
|
||||
|
@ -140,9 +64,9 @@ const RiskEngineHealth: React.FC<{ currentRiskEngineStatus?: RiskEngineStatus |
|
|||
return <EuiHealth color="danger">{'-'}</EuiHealth>;
|
||||
}
|
||||
if (currentRiskEngineStatus === RiskEngineStatusEnum.ENABLED) {
|
||||
return <EuiHealth color="success">{i18n.RISK_SCORE_MODULE_STATUS_ON}</EuiHealth>;
|
||||
return <EuiHealth color="success">{i18n.RISK_ENGINE_STATUS_ON}</EuiHealth>;
|
||||
}
|
||||
return <EuiHealth color="danger">{i18n.RISK_SCORE_MODULE_STATUS_OFF}</EuiHealth>;
|
||||
return <EuiHealth color="danger">{i18n.RISK_ENGINE_STATUS_OFF}</EuiHealth>;
|
||||
};
|
||||
|
||||
const RiskEngineStatusRow: React.FC<{
|
||||
|
@ -186,33 +110,26 @@ export const RiskScoreEnableSection: React.FC<{
|
|||
privileges: RiskEngineMissingPrivilegesResponse;
|
||||
}> = ({ privileges }) => {
|
||||
const { addSuccess } = useAppToasts();
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const { data: riskEngineStatus, isFetching: isStatusLoading } = useRiskEngineStatus();
|
||||
const initRiskEngineMutation = useInitRiskEngineMutation({
|
||||
onSuccess: () => {
|
||||
addSuccess(i18n.RISK_SCORE_MODULE_TURNED_ON, toastOptions);
|
||||
},
|
||||
onSettled: () => {
|
||||
setIsModalVisible(false);
|
||||
addSuccess(i18n.RISK_ENGINE_TURNED_ON, toastOptions);
|
||||
},
|
||||
});
|
||||
|
||||
const enableRiskEngineMutation = useEnableRiskEngineMutation({
|
||||
onSuccess: () => {
|
||||
addSuccess(i18n.RISK_SCORE_MODULE_TURNED_ON, toastOptions);
|
||||
addSuccess(i18n.RISK_ENGINE_TURNED_ON, toastOptions);
|
||||
},
|
||||
});
|
||||
const disableRiskEngineMutation = useDisableRiskEngineMutation({
|
||||
onSuccess: () => {
|
||||
addSuccess(i18n.RISK_SCORE_MODULE_TURNED_OFF, toastOptions);
|
||||
addSuccess(i18n.RISK_ENGINE_TURNED_OFF, toastOptions);
|
||||
},
|
||||
});
|
||||
|
||||
const currentRiskEngineStatus = riskEngineStatus?.risk_engine_status;
|
||||
|
||||
const closeModal = () => setIsModalVisible(false);
|
||||
const showModal = () => setIsModalVisible(true);
|
||||
|
||||
const isLoading =
|
||||
initRiskEngineMutation.isLoading ||
|
||||
enableRiskEngineMutation.isLoading ||
|
||||
|
@ -220,8 +137,6 @@ export const RiskScoreEnableSection: React.FC<{
|
|||
privileges.isLoading ||
|
||||
isStatusLoading;
|
||||
|
||||
const isUpdateAvailable = riskEngineStatus?.isUpdateAvailable;
|
||||
|
||||
const onSwitchClick = () => {
|
||||
if (!currentRiskEngineStatus || isLoading) {
|
||||
return;
|
||||
|
@ -253,48 +168,13 @@ export const RiskScoreEnableSection: React.FC<{
|
|||
)}
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexItem grow={0}>
|
||||
<RiskScoreUpdateModal
|
||||
isVisible={isModalVisible}
|
||||
onConfirm={() => initRiskEngineMutation.mutate()}
|
||||
isLoading={initRiskEngineMutation.isLoading}
|
||||
closeModal={closeModal}
|
||||
<EuiFlexItem grow={false}>
|
||||
<RiskEngineStatusRow
|
||||
currentRiskEngineStatus={currentRiskEngineStatus}
|
||||
onSwitchClick={onSwitchClick}
|
||||
isLoading={isLoading}
|
||||
privileges={privileges}
|
||||
/>
|
||||
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="s" alignItems={'baseline'}>
|
||||
{isUpdateAvailable && <EuiBadge color="success">{i18n.UPDATE_AVAILABLE}</EuiBadge>}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{isUpdateAvailable && (
|
||||
<EuiFlexGroup gutterSize="s" alignItems={'center'}>
|
||||
<EuiFlexItem>
|
||||
{initRiskEngineMutation.isLoading && !isModalVisible && (
|
||||
<EuiLoadingSpinner size="m" />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiButtonEmpty
|
||||
disabled={initRiskEngineMutation.isLoading}
|
||||
color={'primary'}
|
||||
onClick={showModal}
|
||||
data-test-subj="risk-score-update-button"
|
||||
>
|
||||
{i18n.START_UPDATE}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
{!isUpdateAvailable && (
|
||||
<RiskEngineStatusRow
|
||||
currentRiskEngineStatus={currentRiskEngineStatus}
|
||||
onSwitchClick={onSwitchClick}
|
||||
isLoading={isLoading && !currentRiskEngineStatus}
|
||||
privileges={privileges}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
<EuiSpacer />
|
||||
|
|
|
@ -7,8 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { RiskScoreEntity } from '../../../../common/search_strategy';
|
||||
import { RiskScoreEntity } from '../../../common/search_strategy';
|
||||
|
||||
const RiskScoreHeaderTitleComponent = ({
|
||||
riskScoreEntity,
|
|
@ -4,54 +4,58 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiEmptyPrompt, EuiPanel } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { RiskScoreEntity } from '../../../../common/entity_analytics/risk_engine';
|
||||
import { getRiskEntityTranslation } from '../risk_score/translations';
|
||||
import { RiskScoreEntity } from '../../../common/search_strategy';
|
||||
import { RiskScoreHeaderTitle } from './risk_score_header_title';
|
||||
import { HeaderSection } from '../../common/components/header_section';
|
||||
|
||||
export const BETA = i18n.translate('xpack.securitySolution.riskScore.technicalPreviewLabel', {
|
||||
defaultMessage: 'Beta',
|
||||
});
|
||||
|
||||
export const HOST_WARNING_TITLE = i18n.translate(
|
||||
const HOST_WARNING_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.hostsDashboardWarningPanelTitle',
|
||||
{
|
||||
defaultMessage: 'No host risk score data available to display',
|
||||
}
|
||||
);
|
||||
|
||||
export const USER_WARNING_TITLE = i18n.translate(
|
||||
const USER_WARNING_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.usersDashboardWarningPanelTitle',
|
||||
{
|
||||
defaultMessage: 'No user risk score data available to display',
|
||||
}
|
||||
);
|
||||
|
||||
export const HOST_WARNING_BODY = i18n.translate(
|
||||
const HOST_WARNING_BODY = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.hostsDashboardWarningPanelBody',
|
||||
{
|
||||
defaultMessage: `We haven’t found any host risk score data. Check if you have any global filters in the global KQL search bar. If you have just enabled the host risk module, the risk engine might need an hour to generate host risk score data and display in this panel.`,
|
||||
}
|
||||
);
|
||||
|
||||
export const USER_WARNING_BODY = i18n.translate(
|
||||
const USER_WARNING_BODY = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.usersDashboardWarningPanelBody',
|
||||
{
|
||||
defaultMessage: `We haven’t found any user risk score data. Check if you have any global filters in the global KQL search bar. If you have just enabled the user risk module, the risk engine might need an hour to generate user risk score data and display in this panel.`,
|
||||
}
|
||||
);
|
||||
|
||||
export const RESTART_TOOLTIP = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.usersDashboardRestartTooltip',
|
||||
{
|
||||
defaultMessage:
|
||||
'The risk score calculation might take a while to run. However, by pressing restart, you can force it to run immediately.',
|
||||
}
|
||||
);
|
||||
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]
|
||||
);
|
||||
|
||||
export const RISK_DATA_TITLE = (riskEntity: RiskScoreEntity) =>
|
||||
i18n.translate('xpack.securitySolution.alertDetails.overview.hostRiskDataTitle', {
|
||||
defaultMessage: '{riskEntity} Risk Data',
|
||||
values: {
|
||||
riskEntity: getRiskEntityTranslation(riskEntity),
|
||||
},
|
||||
});
|
||||
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} />
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
export const RiskScoresNoDataDetected = React.memo(RiskScoresNoDataDetectedComponent);
|
||||
RiskScoresNoDataDetected.displayName = 'RiskScoresNoDataDetected';
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { RiskScoreEntity } from '../../../../common/search_strategy';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
|
||||
import { RiskScoreEnableButton } from './risk_score_enable_button';
|
||||
|
||||
describe('RiskScoreEnableButton', () => {
|
||||
const mockRefetch = jest.fn();
|
||||
const timerange = {
|
||||
from: 'mockStartDate',
|
||||
to: 'mockEndDate',
|
||||
};
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe.each([[RiskScoreEntity.host], [RiskScoreEntity.user]])('%s', (riskScoreEntity) => {
|
||||
it('Renders expected children', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<RiskScoreEnableButton
|
||||
refetch={mockRefetch}
|
||||
riskScoreEntity={riskScoreEntity}
|
||||
timerange={timerange}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId(`enable_${riskScoreEntity}_risk_score`)).toHaveTextContent(
|
||||
'Enable'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,102 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import type { RiskScoreEntity } from '../../../../common/search_strategy';
|
||||
import { useSpaceId } from '../../../common/hooks/use_space_id';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import type { inputsModel } from '../../../common/store';
|
||||
import { REQUEST_NAMES, useFetch } from '../../../common/hooks/use_fetch';
|
||||
import { useRiskScoreToastContent } from './use_risk_score_toast_content';
|
||||
import { installRiskScoreModule } from './utils';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
|
||||
import { SecuritySolutionLinkButton } from '../../../common/components/links';
|
||||
import { SecurityPageName } from '../../../../common/constants';
|
||||
|
||||
const RiskScoreEnableButtonComponent = ({
|
||||
refetch,
|
||||
riskScoreEntity,
|
||||
disabled = false,
|
||||
timerange,
|
||||
}: {
|
||||
refetch: inputsModel.Refetch;
|
||||
riskScoreEntity: RiskScoreEntity;
|
||||
disabled?: boolean;
|
||||
timerange: {
|
||||
from: string;
|
||||
to: string;
|
||||
};
|
||||
}) => {
|
||||
const spaceId = useSpaceId();
|
||||
const { http, dashboard, ...startServices } = useKibana().services;
|
||||
const { renderDocLink, renderDashboardLink } = useRiskScoreToastContent();
|
||||
const { fetch, isLoading } = useFetch(REQUEST_NAMES.ENABLE_RISK_SCORE, installRiskScoreModule);
|
||||
const isRiskEngineEnabled = useIsExperimentalFeatureEnabled('riskScoringRoutesEnabled');
|
||||
|
||||
const onBoardingRiskScore = useCallback(() => {
|
||||
fetch({
|
||||
dashboard,
|
||||
http,
|
||||
refetch,
|
||||
renderDashboardLink,
|
||||
renderDocLink,
|
||||
riskScoreEntity,
|
||||
spaceId,
|
||||
timerange,
|
||||
startServices,
|
||||
});
|
||||
}, [
|
||||
dashboard,
|
||||
fetch,
|
||||
http,
|
||||
refetch,
|
||||
renderDashboardLink,
|
||||
renderDocLink,
|
||||
riskScoreEntity,
|
||||
spaceId,
|
||||
timerange,
|
||||
startServices,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isRiskEngineEnabled ? (
|
||||
<SecuritySolutionLinkButton
|
||||
color="primary"
|
||||
fill
|
||||
deepLinkId={SecurityPageName.entityAnalyticsManagement}
|
||||
data-test-subj={`enable_${riskScoreEntity}_risk_score`}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.riskScore.enableButtonTitle"
|
||||
defaultMessage="Enable"
|
||||
/>
|
||||
</SecuritySolutionLinkButton>
|
||||
) : (
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill
|
||||
onClick={onBoardingRiskScore}
|
||||
isLoading={isLoading}
|
||||
data-test-subj={`enable_${riskScoreEntity}_risk_score`}
|
||||
disabled={disabled}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.riskScore.enableButtonTitle"
|
||||
defaultMessage="Enable"
|
||||
/>
|
||||
</EuiButton>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const RiskScoreEnableButton = React.memo(RiskScoreEnableButtonComponent);
|
||||
RiskScoreEnableButton.displayName = 'RiskScoreEnableButton';
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiEmptyPrompt, EuiPanel, EuiToolTip } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import { RiskScoreEntity } from '../../../../common/search_strategy';
|
||||
|
||||
import { HeaderSection } from '../../../common/components/header_section';
|
||||
import * as i18n from './translations';
|
||||
import { RiskScoreHeaderTitle } from './risk_score_header_title';
|
||||
import { RiskScoreRestartButton } from './risk_score_restart_button';
|
||||
import type { inputsModel } from '../../../common/store';
|
||||
import { useIsNewRiskScoreModuleInstalled } from '../../api/hooks/use_risk_engine_status';
|
||||
|
||||
const RiskScoresNoDataDetectedComponent = ({
|
||||
entityType,
|
||||
refetch,
|
||||
}: {
|
||||
entityType: RiskScoreEntity;
|
||||
refetch: inputsModel.Refetch;
|
||||
}) => {
|
||||
const isNewRiskScoreModuleInstalled = useIsNewRiskScoreModuleInstalled();
|
||||
|
||||
const translations = useMemo(
|
||||
() => ({
|
||||
title:
|
||||
entityType === RiskScoreEntity.user ? i18n.USER_WARNING_TITLE : i18n.HOST_WARNING_TITLE,
|
||||
body: entityType === RiskScoreEntity.user ? i18n.USER_WARNING_BODY : i18n.HOST_WARNING_BODY,
|
||||
}),
|
||||
[entityType]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPanel data-test-subj={`${entityType}-risk-score-no-data-detected`} hasBorder>
|
||||
<HeaderSection title={<RiskScoreHeaderTitle riskScoreEntity={entityType} />} titleSize="s" />
|
||||
<EuiEmptyPrompt
|
||||
title={<h2>{translations.title}</h2>}
|
||||
body={translations.body}
|
||||
actions={
|
||||
<>
|
||||
{!isNewRiskScoreModuleInstalled && (
|
||||
<EuiToolTip content={i18n.RESTART_TOOLTIP}>
|
||||
<RiskScoreRestartButton refetch={refetch} riskScoreEntity={entityType} />
|
||||
</EuiToolTip>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
export const RiskScoresNoDataDetected = React.memo(RiskScoresNoDataDetectedComponent);
|
||||
RiskScoresNoDataDetected.displayName = 'RiskScoresNoDataDetected';
|
|
@ -1,99 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent, { type UserEvent } from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { RiskScoreEntity } from '../../../../common/search_strategy';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import { RiskScoreRestartButton } from './risk_score_restart_button';
|
||||
|
||||
import { restartRiskScoreTransforms } from './utils';
|
||||
|
||||
jest.mock('./utils');
|
||||
|
||||
const mockRestartRiskScoreTransforms = restartRiskScoreTransforms as jest.Mock;
|
||||
|
||||
const mockUseState = React.useState;
|
||||
jest.mock('../../../common/hooks/use_fetch', () => ({
|
||||
...jest.requireActual('../../../common/hooks/use_fetch'),
|
||||
useFetch: jest.fn().mockImplementation(() => {
|
||||
const [isLoading, setIsLoading] = mockUseState(false);
|
||||
return {
|
||||
fetch: jest.fn().mockImplementation((param) => {
|
||||
setIsLoading(true);
|
||||
mockRestartRiskScoreTransforms(param);
|
||||
}),
|
||||
isLoading,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('RiskScoreRestartButton', () => {
|
||||
let user: UserEvent;
|
||||
const mockRefetch = jest.fn();
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.clearAllMocks();
|
||||
// Workaround for timeout via https://github.com/testing-library/user-event/issues/833#issuecomment-1171452841
|
||||
user = userEvent.setup({
|
||||
advanceTimers: jest.advanceTimersByTime,
|
||||
pointerEventsCheck: 0,
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllTimers();
|
||||
jest.clearAllMocks();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
describe.each([[RiskScoreEntity.host], [RiskScoreEntity.user]])('%s', (riskScoreEntity) => {
|
||||
it('Renders expected children', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<RiskScoreRestartButton refetch={mockRefetch} riskScoreEntity={riskScoreEntity} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId(`restart_${riskScoreEntity}_risk_score`)).toHaveTextContent(
|
||||
'Restart'
|
||||
);
|
||||
});
|
||||
|
||||
it('calls restartRiskScoreTransforms with correct entity', async () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<RiskScoreRestartButton refetch={mockRefetch} riskScoreEntity={riskScoreEntity} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await user.click(screen.getByTestId(`restart_${riskScoreEntity}_risk_score`));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRestartRiskScoreTransforms).toHaveBeenCalled();
|
||||
expect(mockRestartRiskScoreTransforms.mock.calls[0][0].riskScoreEntity).toEqual(
|
||||
riskScoreEntity
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('Update button state while installing', async () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<RiskScoreRestartButton refetch={mockRefetch} riskScoreEntity={riskScoreEntity} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await user.click(screen.getByTestId(`restart_${riskScoreEntity}_risk_score`));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(`restart_${riskScoreEntity}_risk_score`)).toHaveProperty(
|
||||
'disabled',
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,64 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import type { RiskScoreEntity } from '../../../../common/search_strategy';
|
||||
import { useSpaceId } from '../../../common/hooks/use_space_id';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import type { inputsModel } from '../../../common/store';
|
||||
import { REQUEST_NAMES, useFetch } from '../../../common/hooks/use_fetch';
|
||||
import { useRiskScoreToastContent } from './use_risk_score_toast_content';
|
||||
import { restartRiskScoreTransforms } from './utils';
|
||||
|
||||
const RiskScoreRestartButtonComponent = ({
|
||||
refetch,
|
||||
riskScoreEntity,
|
||||
}: {
|
||||
refetch: inputsModel.Refetch;
|
||||
riskScoreEntity: RiskScoreEntity;
|
||||
}) => {
|
||||
const { fetch, isLoading } = useFetch(
|
||||
REQUEST_NAMES.REFRESH_RISK_SCORE,
|
||||
restartRiskScoreTransforms
|
||||
);
|
||||
const spaceId = useSpaceId();
|
||||
|
||||
const { renderDocLink } = useRiskScoreToastContent();
|
||||
const { http, ...startServices } = useKibana().services;
|
||||
|
||||
const onClick = useCallback(async () => {
|
||||
fetch({
|
||||
http,
|
||||
refetch,
|
||||
renderDocLink,
|
||||
riskScoreEntity,
|
||||
spaceId,
|
||||
startServices,
|
||||
});
|
||||
}, [fetch, http, refetch, renderDocLink, riskScoreEntity, spaceId, startServices]);
|
||||
|
||||
return (
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill
|
||||
onClick={onClick}
|
||||
isLoading={isLoading}
|
||||
data-test-subj={`restart_${riskScoreEntity}_risk_score`}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.riskScore.restartButtonTitle"
|
||||
defaultMessage="Restart"
|
||||
/>
|
||||
</EuiButton>
|
||||
);
|
||||
};
|
||||
|
||||
export const RiskScoreRestartButton = React.memo(RiskScoreRestartButtonComponent);
|
||||
RiskScoreRestartButton.displayName = 'RiskScoreRestartButton';
|
|
@ -1,53 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButton, EuiSpacer } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { EntityAnalyticsLearnMoreLink } from './entity_analytics_doc_link';
|
||||
|
||||
const StyledButton = styled(EuiButton)`
|
||||
float: right;
|
||||
`;
|
||||
|
||||
export const useRiskScoreToastContent = () => {
|
||||
const renderDocLink = useCallback(
|
||||
(message: string) => (
|
||||
<>
|
||||
{message} <EntityAnalyticsLearnMoreLink />
|
||||
</>
|
||||
),
|
||||
[]
|
||||
);
|
||||
const renderDashboardLink = useCallback(
|
||||
(message: string, targetUrl: string) => (
|
||||
<>
|
||||
{message}
|
||||
<EuiSpacer size="s" />
|
||||
<StyledButton href={targetUrl} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.risk_score.toast.viewDashboard"
|
||||
defaultMessage="View dashboard"
|
||||
/>
|
||||
</StyledButton>
|
||||
</>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const renderLinks = useMemo(
|
||||
() => ({
|
||||
renderDocLink,
|
||||
renderDashboardLink,
|
||||
}),
|
||||
[renderDashboardLink, renderDocLink]
|
||||
);
|
||||
|
||||
return renderLinks;
|
||||
};
|
|
@ -1,178 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import type { HttpSetup } from '@kbn/core/public';
|
||||
import { RiskScoreEntity } from '../../../../common/search_strategy';
|
||||
import {
|
||||
getIngestPipelineName,
|
||||
getLegacyIngestPipelineName,
|
||||
getRiskScoreLatestTransformId,
|
||||
getRiskScorePivotTransformId,
|
||||
} from '../../../../common/utils/risk_score_modules';
|
||||
import {
|
||||
bulkDeletePrebuiltSavedObjects,
|
||||
bulkCreatePrebuiltSavedObjects,
|
||||
} from '../../deprecated_risk_engine/api';
|
||||
|
||||
import * as api from '../../deprecated_risk_engine/api';
|
||||
import {
|
||||
installRiskScoreModule,
|
||||
restartRiskScoreTransforms,
|
||||
uninstallRiskScoreModule,
|
||||
} from './utils';
|
||||
|
||||
jest.mock('../../deprecated_risk_engine/api');
|
||||
|
||||
const startServices = coreMock.createStart();
|
||||
const mockHttp = {
|
||||
post: jest.fn(),
|
||||
} as unknown as HttpSetup;
|
||||
const mockSpaceId = 'customSpace';
|
||||
const mockTimerange = {
|
||||
from: 'startDate',
|
||||
to: 'endDate',
|
||||
};
|
||||
const mockRefetch = jest.fn();
|
||||
describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
|
||||
`installRiskScoreModule - %s`,
|
||||
(riskScoreEntity) => {
|
||||
beforeAll(async () => {
|
||||
await installRiskScoreModule({
|
||||
http: mockHttp,
|
||||
refetch: mockRefetch,
|
||||
spaceId: mockSpaceId,
|
||||
timerange: mockTimerange,
|
||||
riskScoreEntity,
|
||||
startServices,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it(`installRiskScore`, () => {
|
||||
expect((api.installRiskScore as jest.Mock).mock.calls[0][0].options.riskScoreEntity).toEqual(
|
||||
riskScoreEntity
|
||||
);
|
||||
});
|
||||
|
||||
it(`Create ${riskScoreEntity} dashboards`, () => {
|
||||
expect(
|
||||
(bulkCreatePrebuiltSavedObjects as jest.Mock).mock.calls[0][0].options.templateName
|
||||
).toEqual(`${riskScoreEntity}RiskScoreDashboards`);
|
||||
});
|
||||
|
||||
it('Refresh module', () => {
|
||||
expect(mockRefetch).toBeCalled();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
describe.each([[RiskScoreEntity.host], [RiskScoreEntity.user]])(
|
||||
'uninstallRiskScoreModule - %s',
|
||||
(riskScoreEntity) => {
|
||||
beforeAll(async () => {
|
||||
await uninstallRiskScoreModule({
|
||||
http: mockHttp,
|
||||
spaceId: mockSpaceId,
|
||||
riskScoreEntity,
|
||||
startServices,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it(`Delete ${riskScoreEntity} dashboards`, () => {
|
||||
expect(
|
||||
(bulkDeletePrebuiltSavedObjects as jest.Mock).mock.calls[0][0].options.templateName
|
||||
).toEqual(`${riskScoreEntity}RiskScoreDashboards`);
|
||||
});
|
||||
|
||||
it('Delete Transforms', () => {
|
||||
expect((api.deleteTransforms as jest.Mock).mock.calls[0][0].transformIds).toEqual([
|
||||
getRiskScorePivotTransformId(riskScoreEntity, mockSpaceId),
|
||||
getRiskScoreLatestTransformId(riskScoreEntity, mockSpaceId),
|
||||
]);
|
||||
});
|
||||
|
||||
it('Delete legacy ingest pipelines', () => {
|
||||
expect((api.deleteIngestPipelines as jest.Mock).mock.calls[0][0].names).toEqual(
|
||||
[
|
||||
getLegacyIngestPipelineName(riskScoreEntity),
|
||||
getIngestPipelineName(riskScoreEntity, mockSpaceId),
|
||||
].join(',')
|
||||
);
|
||||
});
|
||||
|
||||
it('Delete legacy stored scripts', () => {
|
||||
if (riskScoreEntity === RiskScoreEntity.user) {
|
||||
expect((api.deleteStoredScripts as jest.Mock).mock.calls[0][0].ids).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"ml_userriskscore_levels_script",
|
||||
"ml_userriskscore_map_script",
|
||||
"ml_userriskscore_reduce_script",
|
||||
"ml_userriskscore_levels_script_customSpace",
|
||||
"ml_userriskscore_map_script_customSpace",
|
||||
"ml_userriskscore_reduce_script_customSpace",
|
||||
]
|
||||
`);
|
||||
} else {
|
||||
expect((api.deleteStoredScripts as jest.Mock).mock.calls[0][0].ids).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"ml_hostriskscore_levels_script",
|
||||
"ml_hostriskscore_init_script",
|
||||
"ml_hostriskscore_map_script",
|
||||
"ml_hostriskscore_reduce_script",
|
||||
"ml_hostriskscore_levels_script_customSpace",
|
||||
"ml_hostriskscore_init_script_customSpace",
|
||||
"ml_hostriskscore_map_script_customSpace",
|
||||
"ml_hostriskscore_reduce_script_customSpace",
|
||||
]
|
||||
`);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
describe.each([[RiskScoreEntity.host], [RiskScoreEntity.user]])(
|
||||
'Restart Transforms - %s',
|
||||
(riskScoreEntity) => {
|
||||
beforeAll(async () => {
|
||||
await restartRiskScoreTransforms({
|
||||
http: mockHttp,
|
||||
refetch: mockRefetch,
|
||||
riskScoreEntity,
|
||||
spaceId: mockSpaceId,
|
||||
startServices,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('Restart Transforms with correct Ids', () => {
|
||||
expect((api.stopTransforms as jest.Mock).mock.calls[0][0].transformIds).toEqual([
|
||||
getRiskScorePivotTransformId(riskScoreEntity, mockSpaceId),
|
||||
getRiskScoreLatestTransformId(riskScoreEntity, mockSpaceId),
|
||||
]);
|
||||
|
||||
expect((api.startTransforms as jest.Mock).mock.calls[0][0].transformIds).toEqual([
|
||||
getRiskScorePivotTransformId(riskScoreEntity, mockSpaceId),
|
||||
getRiskScoreLatestTransformId(riskScoreEntity, mockSpaceId),
|
||||
]);
|
||||
});
|
||||
|
||||
it('Refresh module', () => {
|
||||
expect(mockRefetch).toBeCalled();
|
||||
});
|
||||
}
|
||||
);
|
|
@ -1,348 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { HttpSetup } from '@kbn/core/public';
|
||||
import type { DashboardStart } from '@kbn/dashboard-plugin/public';
|
||||
import type { StartRenderServices } from '../../../types';
|
||||
import { RiskScoreEntity } from '../../../../common/search_strategy';
|
||||
import * as utils from '../../../../common/utils/risk_score_modules';
|
||||
import type { inputsModel } from '../../../common/store';
|
||||
|
||||
import {
|
||||
deleteStoredScripts,
|
||||
deleteTransforms,
|
||||
deleteIngestPipelines,
|
||||
bulkDeletePrebuiltSavedObjects,
|
||||
installRiskScore,
|
||||
bulkCreatePrebuiltSavedObjects,
|
||||
stopTransforms,
|
||||
startTransforms,
|
||||
} from '../../deprecated_risk_engine/api';
|
||||
import {
|
||||
INGEST_PIPELINE_DELETION_ERROR_MESSAGE,
|
||||
TRANSFORM_DELETION_ERROR_MESSAGE,
|
||||
UNINSTALLATION_ERROR,
|
||||
} from '../../deprecated_risk_engine/api/translations';
|
||||
|
||||
interface InstallRiskScoreModule {
|
||||
dashboard?: DashboardStart;
|
||||
http: HttpSetup;
|
||||
refetch?: inputsModel.Refetch;
|
||||
renderDashboardLink?: (message: string, dashboardUrl: string) => React.ReactNode;
|
||||
renderDocLink?: (message: string) => React.ReactNode;
|
||||
riskScoreEntity: RiskScoreEntity;
|
||||
spaceId?: string;
|
||||
timerange: {
|
||||
from: string;
|
||||
to: string;
|
||||
};
|
||||
startServices: StartRenderServices;
|
||||
}
|
||||
|
||||
type UpgradeRiskScoreModule = InstallRiskScoreModule;
|
||||
|
||||
const installHostRiskScoreModule = async ({
|
||||
dashboard,
|
||||
http,
|
||||
refetch,
|
||||
renderDashboardLink,
|
||||
renderDocLink,
|
||||
timerange,
|
||||
startServices,
|
||||
}: InstallRiskScoreModule) => {
|
||||
await installRiskScore({
|
||||
http,
|
||||
renderDocLink,
|
||||
options: {
|
||||
riskScoreEntity: RiskScoreEntity.host,
|
||||
},
|
||||
startServices,
|
||||
});
|
||||
|
||||
// Install dashboards and relevant saved objects
|
||||
await bulkCreatePrebuiltSavedObjects({
|
||||
http,
|
||||
dashboard,
|
||||
renderDashboardLink,
|
||||
renderDocLink,
|
||||
...timerange,
|
||||
options: {
|
||||
templateName: `${RiskScoreEntity.host}RiskScoreDashboards`,
|
||||
},
|
||||
startServices,
|
||||
});
|
||||
|
||||
if (refetch) {
|
||||
refetch();
|
||||
}
|
||||
};
|
||||
|
||||
const installUserRiskScoreModule = async ({
|
||||
dashboard,
|
||||
http,
|
||||
refetch,
|
||||
renderDashboardLink,
|
||||
renderDocLink,
|
||||
spaceId = 'default',
|
||||
timerange,
|
||||
startServices,
|
||||
}: InstallRiskScoreModule) => {
|
||||
await installRiskScore({
|
||||
http,
|
||||
renderDocLink,
|
||||
options: {
|
||||
riskScoreEntity: RiskScoreEntity.user,
|
||||
},
|
||||
startServices,
|
||||
});
|
||||
|
||||
// Install dashboards and relevant saved objects
|
||||
await bulkCreatePrebuiltSavedObjects({
|
||||
dashboard,
|
||||
http,
|
||||
options: {
|
||||
templateName: `${RiskScoreEntity.user}RiskScoreDashboards`,
|
||||
},
|
||||
renderDashboardLink,
|
||||
renderDocLink,
|
||||
startServices,
|
||||
...timerange,
|
||||
});
|
||||
|
||||
if (refetch) {
|
||||
refetch();
|
||||
}
|
||||
};
|
||||
|
||||
export const installRiskScoreModule = async (settings: InstallRiskScoreModule) => {
|
||||
if (settings.riskScoreEntity === RiskScoreEntity.user) {
|
||||
await installUserRiskScoreModule(settings);
|
||||
} else {
|
||||
await installHostRiskScoreModule(settings);
|
||||
}
|
||||
};
|
||||
|
||||
export const uninstallRiskScoreModule = async ({
|
||||
http,
|
||||
refetch,
|
||||
renderDocLink,
|
||||
riskScoreEntity,
|
||||
spaceId = 'default',
|
||||
startServices,
|
||||
}: {
|
||||
http: HttpSetup;
|
||||
refetch?: inputsModel.Refetch;
|
||||
renderDocLink?: (message: string) => React.ReactNode;
|
||||
riskScoreEntity: RiskScoreEntity;
|
||||
spaceId?: string;
|
||||
deleteAll?: boolean;
|
||||
startServices: StartRenderServices;
|
||||
}) => {
|
||||
const legacyTransformIds = [
|
||||
// transform Ids never changed since 8.3
|
||||
utils.getRiskScorePivotTransformId(riskScoreEntity, spaceId),
|
||||
utils.getRiskScoreLatestTransformId(riskScoreEntity, spaceId),
|
||||
];
|
||||
const legacyRiskScoreHostsScriptIds = [
|
||||
// 8.4
|
||||
utils.getLegacyRiskScoreLevelScriptId(RiskScoreEntity.host),
|
||||
utils.getLegacyRiskScoreInitScriptId(RiskScoreEntity.host),
|
||||
utils.getLegacyRiskScoreMapScriptId(RiskScoreEntity.host),
|
||||
utils.getLegacyRiskScoreReduceScriptId(RiskScoreEntity.host),
|
||||
// 8.3 and after 8.5
|
||||
utils.getRiskScoreLevelScriptId(RiskScoreEntity.host, spaceId),
|
||||
utils.getRiskScoreInitScriptId(RiskScoreEntity.host, spaceId),
|
||||
utils.getRiskScoreMapScriptId(RiskScoreEntity.host, spaceId),
|
||||
utils.getRiskScoreReduceScriptId(RiskScoreEntity.host, spaceId),
|
||||
];
|
||||
const legacyRiskScoreUsersScriptIds = [
|
||||
// 8.4
|
||||
utils.getLegacyRiskScoreLevelScriptId(RiskScoreEntity.user),
|
||||
utils.getLegacyRiskScoreMapScriptId(RiskScoreEntity.user),
|
||||
utils.getLegacyRiskScoreReduceScriptId(RiskScoreEntity.user),
|
||||
// 8.3 and after 8.5
|
||||
utils.getRiskScoreLevelScriptId(RiskScoreEntity.user, spaceId),
|
||||
utils.getRiskScoreMapScriptId(RiskScoreEntity.user, spaceId),
|
||||
utils.getRiskScoreReduceScriptId(RiskScoreEntity.user, spaceId),
|
||||
];
|
||||
|
||||
const legacyIngestPipelineNames = [
|
||||
// 8.4
|
||||
utils.getLegacyIngestPipelineName(riskScoreEntity),
|
||||
// 8.3 and 8.5
|
||||
utils.getIngestPipelineName(riskScoreEntity, spaceId),
|
||||
];
|
||||
|
||||
await Promise.all([
|
||||
/**
|
||||
* Intended not to pass notification to bulkDeletePrebuiltSavedObjects.
|
||||
* As the only error it can happen is saved object not found, and
|
||||
* that is what bulkDeletePrebuiltSavedObjects wants.
|
||||
* (Before 8.5 once an saved object was created, it was shared across different spaces.
|
||||
* If it has been upgrade in one space, "saved object not found" will happen when upgrading other spaces.
|
||||
* Or it could be users manually deleted the saved object.)
|
||||
*/
|
||||
bulkDeletePrebuiltSavedObjects({
|
||||
http,
|
||||
options: {
|
||||
templateName: `${riskScoreEntity}RiskScoreDashboards`,
|
||||
},
|
||||
startServices,
|
||||
}),
|
||||
deleteTransforms({
|
||||
http,
|
||||
renderDocLink,
|
||||
errorMessage: `${UNINSTALLATION_ERROR} - ${TRANSFORM_DELETION_ERROR_MESSAGE(
|
||||
legacyTransformIds.length
|
||||
)}`,
|
||||
transformIds: legacyTransformIds,
|
||||
options: {
|
||||
deleteDestIndex: true,
|
||||
deleteDestDataView: true,
|
||||
forceDelete: false,
|
||||
},
|
||||
startServices,
|
||||
}),
|
||||
/**
|
||||
* Intended not to pass notification to deleteIngestPipelines.
|
||||
* As the only error it can happen is ingest pipeline not found, and
|
||||
* that is what deleteIngestPipelines wants.
|
||||
* (Before 8.5 once an ingest pipeline was created, it was shared across different spaces.
|
||||
* If it has been upgrade in one space, "ingest pipeline not found" will happen when upgrading other spaces.
|
||||
* Or it could be users manually deleted the ingest pipeline.)
|
||||
*/
|
||||
deleteIngestPipelines({
|
||||
http,
|
||||
errorMessage: `${UNINSTALLATION_ERROR} - ${INGEST_PIPELINE_DELETION_ERROR_MESSAGE(
|
||||
legacyIngestPipelineNames.length
|
||||
)}`,
|
||||
names: legacyIngestPipelineNames.join(','),
|
||||
startServices,
|
||||
}),
|
||||
/**
|
||||
* Intended not to pass notification to deleteStoredScripts.
|
||||
* As the only error it can happen is script not found, and
|
||||
* that is what deleteStoredScripts wants.
|
||||
* (In 8.4 once a script was created, it was shared across different spaces.
|
||||
* If it has been upgrade in one space, "script not found" will happen when upgrading other spaces.
|
||||
* Or it could be users manually deleted the script.)
|
||||
*/
|
||||
deleteStoredScripts({
|
||||
http,
|
||||
ids:
|
||||
riskScoreEntity === RiskScoreEntity.user
|
||||
? legacyRiskScoreUsersScriptIds
|
||||
: legacyRiskScoreHostsScriptIds,
|
||||
startServices,
|
||||
}),
|
||||
]);
|
||||
|
||||
if (refetch) {
|
||||
refetch();
|
||||
}
|
||||
};
|
||||
|
||||
export const upgradeHostRiskScoreModule = async ({
|
||||
dashboard,
|
||||
http,
|
||||
refetch,
|
||||
renderDashboardLink,
|
||||
renderDocLink,
|
||||
spaceId = 'default',
|
||||
timerange,
|
||||
startServices,
|
||||
}: UpgradeRiskScoreModule) => {
|
||||
await uninstallRiskScoreModule({
|
||||
http,
|
||||
renderDocLink,
|
||||
riskScoreEntity: RiskScoreEntity.host,
|
||||
spaceId,
|
||||
startServices,
|
||||
});
|
||||
await installRiskScoreModule({
|
||||
dashboard,
|
||||
http,
|
||||
refetch,
|
||||
renderDashboardLink,
|
||||
renderDocLink,
|
||||
riskScoreEntity: RiskScoreEntity.host,
|
||||
spaceId,
|
||||
timerange,
|
||||
startServices,
|
||||
});
|
||||
};
|
||||
|
||||
export const upgradeUserRiskScoreModule = async ({
|
||||
dashboard,
|
||||
http,
|
||||
refetch,
|
||||
renderDashboardLink,
|
||||
renderDocLink,
|
||||
spaceId = 'default',
|
||||
timerange,
|
||||
startServices,
|
||||
}: UpgradeRiskScoreModule) => {
|
||||
await uninstallRiskScoreModule({
|
||||
http,
|
||||
renderDocLink,
|
||||
riskScoreEntity: RiskScoreEntity.user,
|
||||
spaceId,
|
||||
startServices,
|
||||
});
|
||||
await installRiskScoreModule({
|
||||
dashboard,
|
||||
http,
|
||||
refetch,
|
||||
renderDashboardLink,
|
||||
renderDocLink,
|
||||
riskScoreEntity: RiskScoreEntity.user,
|
||||
spaceId,
|
||||
timerange,
|
||||
startServices,
|
||||
});
|
||||
};
|
||||
|
||||
export const restartRiskScoreTransforms = async ({
|
||||
http,
|
||||
refetch,
|
||||
renderDocLink,
|
||||
riskScoreEntity,
|
||||
spaceId,
|
||||
startServices,
|
||||
}: {
|
||||
http: HttpSetup;
|
||||
refetch?: inputsModel.Refetch;
|
||||
renderDocLink?: (message: string) => React.ReactNode;
|
||||
riskScoreEntity: RiskScoreEntity;
|
||||
spaceId?: string;
|
||||
startServices: StartRenderServices;
|
||||
}) => {
|
||||
const transformIds = [
|
||||
utils.getRiskScorePivotTransformId(riskScoreEntity, spaceId),
|
||||
utils.getRiskScoreLatestTransformId(riskScoreEntity, spaceId),
|
||||
];
|
||||
|
||||
await stopTransforms({
|
||||
http,
|
||||
renderDocLink,
|
||||
transformIds,
|
||||
startServices,
|
||||
});
|
||||
|
||||
const res = await startTransforms({
|
||||
http,
|
||||
renderDocLink,
|
||||
transformIds,
|
||||
startServices,
|
||||
});
|
||||
|
||||
if (refetch) {
|
||||
refetch();
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
|
@ -1,34 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiCallOut, EuiText, EuiFlexGroup, EuiSpacer } from '@elastic/eui';
|
||||
import * as i18n from '../translations';
|
||||
import { SecuritySolutionLinkButton } from '../../common/components/links';
|
||||
import { SecurityPageName } from '../../../common/constants';
|
||||
|
||||
export const RiskScoreUpdatePanel = () => {
|
||||
return (
|
||||
<>
|
||||
<EuiCallOut title={i18n.UPDATE_PANEL_TITLE} color="primary" iconType="starEmpty">
|
||||
<EuiText>{i18n.UPDATE_PANEL_MESSAGE}</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup gutterSize="m">
|
||||
<SecuritySolutionLinkButton
|
||||
color="primary"
|
||||
fill
|
||||
deepLinkId={SecurityPageName.entityAnalyticsManagement}
|
||||
data-test-subj="update-risk-score-button"
|
||||
>
|
||||
{i18n.UPDATE_PANEL_GO_TO_MANAGE}
|
||||
</SecuritySolutionLinkButton>
|
||||
</EuiFlexGroup>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,87 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { TopRiskScoreContributors } from '.';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import type { RuleRisk } from '../../../../common/search_strategy';
|
||||
|
||||
jest.mock('../../../common/containers/query_toggle');
|
||||
const testProps = {
|
||||
riskScore: [],
|
||||
setQuery: jest.fn(),
|
||||
deleteQuery: jest.fn(),
|
||||
hostName: 'test-host-name',
|
||||
from: '2020-07-07T08:20:18.966Z',
|
||||
to: '2020-07-08T08:20:18.966Z',
|
||||
loading: false,
|
||||
toggleStatus: true,
|
||||
queryId: 'test-query-id',
|
||||
};
|
||||
|
||||
describe('Top Risk Score Contributors', () => {
|
||||
it('renders', () => {
|
||||
const { queryByTestId } = render(
|
||||
<TestProviders>
|
||||
<TopRiskScoreContributors {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(queryByTestId('topRiskScoreContributors')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders sorted items', () => {
|
||||
const ruleRisk: RuleRisk[] = [
|
||||
{
|
||||
rule_name: 'third',
|
||||
rule_risk: 10,
|
||||
rule_id: '3',
|
||||
},
|
||||
{
|
||||
rule_name: 'first',
|
||||
rule_risk: 99,
|
||||
rule_id: '1',
|
||||
},
|
||||
{
|
||||
rule_name: 'second',
|
||||
rule_risk: 55,
|
||||
rule_id: '2',
|
||||
},
|
||||
];
|
||||
|
||||
const { queryAllByRole } = render(
|
||||
<TestProviders>
|
||||
<TopRiskScoreContributors {...testProps} rules={ruleRisk} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(queryAllByRole('row')[1]).toHaveTextContent('first');
|
||||
expect(queryAllByRole('row')[2]).toHaveTextContent('second');
|
||||
expect(queryAllByRole('row')[3]).toHaveTextContent('third');
|
||||
});
|
||||
|
||||
describe('toggleStatus', () => {
|
||||
test('toggleStatus=true, render components', () => {
|
||||
const { queryByTestId } = render(
|
||||
<TestProviders>
|
||||
<TopRiskScoreContributors {...testProps} toggleStatus={true} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(queryByTestId('topRiskScoreContributors-table')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('toggleStatus=false, do not render components', () => {
|
||||
const { queryByTestId } = render(
|
||||
<TestProviders>
|
||||
<TopRiskScoreContributors {...testProps} toggleStatus={false} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(queryByTestId('topRiskScoreContributors-table')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,116 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import type { EuiTableFieldDataColumnType } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiInMemoryTable } from '@elastic/eui';
|
||||
|
||||
import { HeaderSection } from '../../../common/components/header_section';
|
||||
import { InspectButton, InspectButtonContainer } from '../../../common/components/inspect';
|
||||
import * as i18n from './translations';
|
||||
|
||||
import type { RuleRisk } from '../../../../common/search_strategy';
|
||||
|
||||
import { RuleLink } from '../../../detection_engine/rule_management_ui/components/rules_table/use_columns';
|
||||
|
||||
export interface TopRiskScoreContributorsProps {
|
||||
loading: boolean;
|
||||
rules?: RuleRisk[];
|
||||
queryId: string;
|
||||
toggleStatus: boolean;
|
||||
toggleQuery?: (status: boolean) => void;
|
||||
}
|
||||
interface TableItem {
|
||||
rank: number;
|
||||
name: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
const columns: Array<EuiTableFieldDataColumnType<TableItem>> = [
|
||||
{
|
||||
name: i18n.RANK_TITLE,
|
||||
field: 'rank',
|
||||
width: '45px',
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
name: i18n.RULE_NAME_TITLE,
|
||||
field: 'name',
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
render: (value: TableItem['name'], { id }: TableItem) =>
|
||||
id ? <RuleLink id={id} name={value} /> : value,
|
||||
},
|
||||
];
|
||||
|
||||
const PAGE_SIZE = 5;
|
||||
|
||||
const TopRiskScoreContributorsComponent: React.FC<TopRiskScoreContributorsProps> = ({
|
||||
rules = [],
|
||||
loading,
|
||||
queryId,
|
||||
toggleStatus,
|
||||
toggleQuery,
|
||||
}) => {
|
||||
const items = useMemo(() => {
|
||||
return rules
|
||||
?.sort((a, b) => b.rule_risk - a.rule_risk)
|
||||
.map(({ rule_name: name, rule_id: id }, i) => ({ rank: i + 1, name, id }));
|
||||
}, [rules]);
|
||||
|
||||
const tablePagination = useMemo(
|
||||
() => ({
|
||||
showPerPageOptions: false,
|
||||
pageSize: PAGE_SIZE,
|
||||
totalItemCount: items.length,
|
||||
}),
|
||||
[items.length]
|
||||
);
|
||||
|
||||
return (
|
||||
<InspectButtonContainer>
|
||||
<EuiPanel hasBorder data-test-subj="topRiskScoreContributors">
|
||||
<EuiFlexGroup gutterSize={'none'}>
|
||||
<EuiFlexItem grow={1}>
|
||||
<HeaderSection
|
||||
title={i18n.TOP_RISK_SCORE_CONTRIBUTORS}
|
||||
hideSubtitle
|
||||
toggleQuery={toggleQuery}
|
||||
toggleStatus={toggleStatus}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{toggleStatus && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<InspectButton queryId={queryId} title={i18n.TOP_RISK_SCORE_CONTRIBUTORS} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
|
||||
{toggleStatus && (
|
||||
<EuiFlexGroup
|
||||
data-test-subj="topRiskScoreContributors-table"
|
||||
gutterSize="none"
|
||||
direction="column"
|
||||
>
|
||||
<EuiFlexItem grow={1}>
|
||||
<EuiInMemoryTable
|
||||
items={items}
|
||||
columns={columns}
|
||||
pagination={tablePagination}
|
||||
loading={loading}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiPanel>
|
||||
</InspectButtonContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const TopRiskScoreContributors = React.memo(TopRiskScoreContributorsComponent);
|
||||
TopRiskScoreContributors.displayName = 'TopRiskScoreContributors';
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const TOP_RISK_SCORE_CONTRIBUTORS = i18n.translate(
|
||||
'xpack.securitySolution.hosts.topRiskScoreContributors.title',
|
||||
{
|
||||
defaultMessage: 'Top risk score contributors',
|
||||
}
|
||||
);
|
||||
|
||||
export const RANK_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.hosts.topRiskScoreContributors.rankColumnTitle',
|
||||
{
|
||||
defaultMessage: 'Rank',
|
||||
}
|
||||
);
|
||||
|
||||
export const RULE_NAME_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.hosts.topRiskScoreContributors.ruleNameColumnTitle',
|
||||
{
|
||||
defaultMessage: 'Rule name',
|
||||
}
|
||||
);
|
|
@ -45,7 +45,7 @@ describe('All users query tab body', () => {
|
|||
isInspected: false,
|
||||
totalCount: 0,
|
||||
refetch: jest.fn(),
|
||||
isModuleEnabled: true,
|
||||
hasEngineBeenInstalled: true,
|
||||
});
|
||||
mockUseRiskScoreKpi.mockReturnValue({
|
||||
loading: false,
|
||||
|
|
|
@ -21,12 +21,10 @@ 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 { RiskScoresNoDataDetected } from './risk_score_onboarding/risk_score_no_data_detected';
|
||||
import { useRiskEngineStatus } from '../api/hooks/use_risk_engine_status';
|
||||
import { RiskScoreUpdatePanel } from './risk_score_update_panel';
|
||||
import { useMissingRiskEnginePrivileges } from '../hooks/use_missing_risk_engine_privileges';
|
||||
import { RiskEnginePrivilegesCallOut } from './risk_engine_privileges_callout';
|
||||
import { useUpsellingComponent } from '../../common/hooks/use_upselling';
|
||||
import { RiskScoresNoDataDetected } from './risk_score_no_data_detected';
|
||||
|
||||
const UserRiskScoreTableManage = manageQuery(UserRiskScoreTable);
|
||||
|
||||
|
@ -39,7 +37,6 @@ export const UserRiskScoreQueryTabBody = ({
|
|||
startDate: from,
|
||||
type,
|
||||
}: UsersComponentsQueryProps) => {
|
||||
const { data: riskScoreEngineStatus } = useRiskEngineStatus();
|
||||
const getUserRiskScoreSelector = useMemo(() => usersSelectors.userRiskScoreSelector(), []);
|
||||
const { activePage, limit, sort } = useDeepEqualSelector((state: State) =>
|
||||
getUserRiskScoreSelector(state)
|
||||
|
@ -69,23 +66,15 @@ export const UserRiskScoreQueryTabBody = ({
|
|||
|
||||
const privileges = useMissingRiskEnginePrivileges();
|
||||
|
||||
const {
|
||||
data,
|
||||
inspect,
|
||||
isDeprecated,
|
||||
isInspected,
|
||||
isModuleEnabled,
|
||||
loading,
|
||||
refetch,
|
||||
totalCount,
|
||||
} = useRiskScore({
|
||||
filterQuery,
|
||||
pagination,
|
||||
riskEntity: RiskScoreEntity.user,
|
||||
skip: querySkip,
|
||||
sort,
|
||||
timerange,
|
||||
});
|
||||
const { data, inspect, isInspected, hasEngineBeenInstalled, loading, refetch, totalCount } =
|
||||
useRiskScore({
|
||||
filterQuery,
|
||||
pagination,
|
||||
riskEntity: RiskScoreEntity.user,
|
||||
skip: querySkip,
|
||||
sort,
|
||||
timerange,
|
||||
});
|
||||
|
||||
const { severityCount, loading: isKpiLoading } = useRiskScoreKpi({
|
||||
filterQuery,
|
||||
|
@ -93,10 +82,7 @@ export const UserRiskScoreQueryTabBody = ({
|
|||
skip: querySkip,
|
||||
});
|
||||
|
||||
const status = {
|
||||
isDisabled: !isModuleEnabled && !loading,
|
||||
isDeprecated: isDeprecated && !loading,
|
||||
};
|
||||
const isDisabled = !hasEngineBeenInstalled && !loading;
|
||||
|
||||
const RiskScoreUpsell = useUpsellingComponent('entity_analytics_panel');
|
||||
if (RiskScoreUpsell) {
|
||||
|
@ -111,26 +97,25 @@ export const UserRiskScoreQueryTabBody = ({
|
|||
);
|
||||
}
|
||||
|
||||
if (status.isDisabled || status.isDeprecated) {
|
||||
if (isDisabled) {
|
||||
return (
|
||||
<EuiPanel hasBorder>
|
||||
<EnableRiskScore
|
||||
{...status}
|
||||
entityType={RiskScoreEntity.host}
|
||||
refetch={refetch}
|
||||
timerange={timerange}
|
||||
/>
|
||||
<EnableRiskScore isDisabled={isDisabled} entityType={RiskScoreEntity.host} />
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
||||
if (isModuleEnabled && userSeveritySelectionRedux.length === 0 && data && data.length === 0) {
|
||||
return <RiskScoresNoDataDetected entityType={RiskScoreEntity.user} refetch={refetch} />;
|
||||
if (
|
||||
hasEngineBeenInstalled &&
|
||||
userSeveritySelectionRedux.length === 0 &&
|
||||
data &&
|
||||
data.length === 0
|
||||
) {
|
||||
return <RiskScoresNoDataDetected entityType={RiskScoreEntity.user} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{riskScoreEngineStatus?.isUpdateAvailable && <RiskScoreUpdatePanel />}
|
||||
<UserRiskScoreTableManage
|
||||
deleteQuery={deleteQuery}
|
||||
data={data ?? []}
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
export * from './ingest_pipelines';
|
||||
export * from './transforms';
|
||||
export * from './stored_scripts';
|
||||
export * from './saved_objects';
|
||||
export * from './onboarding';
|
|
@ -1,82 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import type { HttpSetup } from '@kbn/core/public';
|
||||
import { createIngestPipeline, deleteIngestPipelines } from './ingest_pipelines';
|
||||
|
||||
const mockRequest = jest.fn();
|
||||
const mockHttp = {
|
||||
put: mockRequest,
|
||||
post: mockRequest,
|
||||
delete: mockRequest,
|
||||
} as unknown as HttpSetup;
|
||||
|
||||
const startServices = coreMock.createStart();
|
||||
const mockAddDanger = jest.spyOn(startServices.notifications.toasts, 'addDanger');
|
||||
|
||||
const mockRenderDocLink = jest.fn();
|
||||
|
||||
describe('createIngestPipeline', () => {
|
||||
const mockOptions = { name: 'test', processors: [] };
|
||||
|
||||
beforeAll(async () => {
|
||||
mockRequest.mockRejectedValue({ body: { message: 'test error' } });
|
||||
await createIngestPipeline({
|
||||
http: mockHttp,
|
||||
options: mockOptions,
|
||||
renderDocLink: mockRenderDocLink,
|
||||
startServices,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('create ingest pipeline', () => {
|
||||
expect(mockRequest.mock.calls[0][0]).toEqual(`/api/ingest_pipelines`);
|
||||
});
|
||||
|
||||
it('handles error', () => {
|
||||
expect(mockAddDanger.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"text": [Function],
|
||||
"title": "Failed to create Ingest pipeline",
|
||||
}
|
||||
`);
|
||||
expect(mockRenderDocLink.mock.calls[0][0]).toEqual('test error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteIngestPipelines', () => {
|
||||
beforeAll(async () => {
|
||||
mockRequest.mockRejectedValue({ body: { message: 'test error' } });
|
||||
await deleteIngestPipelines({
|
||||
http: mockHttp,
|
||||
names: 'test,abc',
|
||||
renderDocLink: mockRenderDocLink,
|
||||
startServices,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('delete ingest pipeline', () => {
|
||||
expect(mockRequest.mock.calls[0][0]).toEqual(`/api/ingest_pipelines/test,abc`);
|
||||
});
|
||||
|
||||
it('handles error', () => {
|
||||
expect(mockAddDanger.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"text": [Function],
|
||||
"title": "Failed to delete Ingest pipelines",
|
||||
}
|
||||
`);
|
||||
expect(mockRenderDocLink.mock.calls[0][0]).toEqual('test error');
|
||||
});
|
||||
});
|
|
@ -1,67 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
import {
|
||||
INGEST_PIPELINE_CREATION_ERROR_MESSAGE,
|
||||
INGEST_PIPELINE_DELETION_ERROR_MESSAGE,
|
||||
} from './translations';
|
||||
import type { CreateIngestPipeline, DeleteIngestPipeline } from './types';
|
||||
|
||||
const INGEST_PIPELINES_API_BASE_PATH = `/api/ingest_pipelines`;
|
||||
|
||||
export async function createIngestPipeline({
|
||||
errorMessage,
|
||||
http,
|
||||
options,
|
||||
renderDocLink,
|
||||
signal,
|
||||
startServices: { notifications, ...startServices },
|
||||
}: CreateIngestPipeline) {
|
||||
const res = await http
|
||||
.post(INGEST_PIPELINES_API_BASE_PATH, {
|
||||
body: JSON.stringify(options),
|
||||
signal,
|
||||
})
|
||||
.catch((e) => {
|
||||
notifications.toasts.addDanger({
|
||||
title: errorMessage ?? INGEST_PIPELINE_CREATION_ERROR_MESSAGE,
|
||||
text: toMountPoint(
|
||||
renderDocLink ? renderDocLink(e?.body?.message) : e?.body?.message,
|
||||
startServices
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function deleteIngestPipelines({
|
||||
errorMessage,
|
||||
http,
|
||||
names, // separate with ','
|
||||
renderDocLink,
|
||||
signal,
|
||||
startServices: { notifications, ...startServices },
|
||||
}: DeleteIngestPipeline) {
|
||||
const count = names.split(',').length;
|
||||
const res = await http
|
||||
.delete(`${INGEST_PIPELINES_API_BASE_PATH}/${names}`, {
|
||||
signal,
|
||||
})
|
||||
.catch((e) => {
|
||||
notifications.toasts.addDanger({
|
||||
title: errorMessage ?? INGEST_PIPELINE_DELETION_ERROR_MESSAGE(count),
|
||||
text: toMountPoint(
|
||||
renderDocLink ? renderDocLink(e?.body?.message) : e?.body?.message,
|
||||
startServices
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { HttpSetup } from '@kbn/core/public';
|
||||
|
||||
import type { StartRenderServices } from '../../../types';
|
||||
import { INTERNAL_RISK_SCORE_URL } from '../../../../common/constants';
|
||||
|
||||
import { RiskScoreEntity } from '../../../../common/search_strategy';
|
||||
import {
|
||||
HOST_RISK_SCORES_ENABLED_TITLE,
|
||||
INSTALLATION_ERROR,
|
||||
RISK_SCORES_ENABLED_TEXT,
|
||||
USER_RISK_SCORES_ENABLED_TITLE,
|
||||
} from './translations';
|
||||
|
||||
interface Options {
|
||||
riskScoreEntity: RiskScoreEntity;
|
||||
}
|
||||
|
||||
type Response = Record<string, { success?: boolean; error?: Error }>;
|
||||
const toastLifeTimeMs = 600000;
|
||||
|
||||
export const installRiskScore = ({
|
||||
errorMessage,
|
||||
http,
|
||||
options,
|
||||
renderDocLink,
|
||||
signal,
|
||||
startServices,
|
||||
}: {
|
||||
errorMessage?: string;
|
||||
http: HttpSetup;
|
||||
options: Options;
|
||||
renderDocLink?: (message: string) => React.ReactNode;
|
||||
signal?: AbortSignal;
|
||||
startServices: Pick<StartRenderServices, 'notifications'>;
|
||||
}) => {
|
||||
const { notifications } = startServices;
|
||||
return http
|
||||
.post<Response[]>(INTERNAL_RISK_SCORE_URL, {
|
||||
version: '1',
|
||||
body: JSON.stringify(options),
|
||||
signal,
|
||||
})
|
||||
.then((result) => {
|
||||
const resp = result.reduce(
|
||||
(acc, curr) => {
|
||||
const [[key, res]] = Object.entries(curr);
|
||||
if (res.success) {
|
||||
return res.success != null ? { ...acc, success: [...acc.success, `${key}`] } : acc;
|
||||
} else {
|
||||
return res.error != null
|
||||
? { ...acc, error: [...acc.error, `${key}: ${res?.error?.message}`] }
|
||||
: acc;
|
||||
}
|
||||
},
|
||||
{ success: [] as string[], error: [] as string[] }
|
||||
);
|
||||
|
||||
if (resp.error.length > 0) {
|
||||
notifications.toasts.addError(new Error(errorMessage ?? INSTALLATION_ERROR), {
|
||||
title: errorMessage ?? INSTALLATION_ERROR,
|
||||
toastMessage: renderDocLink
|
||||
? (renderDocLink(resp.error.join(', ')) as unknown as string)
|
||||
: resp.error.join(', '),
|
||||
toastLifeTimeMs,
|
||||
});
|
||||
} else {
|
||||
notifications.toasts.addSuccess({
|
||||
'data-test-subj': `${options.riskScoreEntity}EnableSuccessToast`,
|
||||
title:
|
||||
options.riskScoreEntity === RiskScoreEntity.user
|
||||
? USER_RISK_SCORES_ENABLED_TITLE
|
||||
: HOST_RISK_SCORES_ENABLED_TITLE,
|
||||
text: RISK_SCORES_ENABLED_TEXT(resp.success.join(', ')),
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
notifications.toasts.addError(new Error(errorMessage ?? INSTALLATION_ERROR), {
|
||||
title: errorMessage ?? INSTALLATION_ERROR,
|
||||
toastMessage: renderDocLink ? renderDocLink(e?.body?.message) : e?.body?.message,
|
||||
toastLifeTimeMs,
|
||||
});
|
||||
});
|
||||
};
|
|
@ -1,151 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { HttpSetup } from '@kbn/core/public';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
import type { DashboardStart } from '@kbn/dashboard-plugin/public';
|
||||
import type { StartRenderServices } from '../../../types';
|
||||
import {
|
||||
RISKY_HOSTS_DASHBOARD_TITLE,
|
||||
RISKY_USERS_DASHBOARD_TITLE,
|
||||
} from '../../components/risk_score/constants';
|
||||
import {
|
||||
prebuiltSavedObjectsBulkCreateUrl,
|
||||
prebuiltSavedObjectsBulkDeleteUrl,
|
||||
} from '../../../../common/constants';
|
||||
|
||||
import { RiskScoreEntity } from '../../../../common/search_strategy';
|
||||
|
||||
import {
|
||||
DELETE_SAVED_OBJECTS_FAILURE,
|
||||
IMPORT_SAVED_OBJECTS_FAILURE,
|
||||
IMPORT_SAVED_OBJECTS_SUCCESS,
|
||||
} from './translations';
|
||||
|
||||
const toastLifeTimeMs = 600000;
|
||||
|
||||
type DashboardsSavedObjectTemplate = `${RiskScoreEntity}RiskScoreDashboards`;
|
||||
|
||||
interface Options {
|
||||
templateName: DashboardsSavedObjectTemplate;
|
||||
}
|
||||
|
||||
export const bulkCreatePrebuiltSavedObjects = async ({
|
||||
dashboard,
|
||||
to,
|
||||
errorMessage,
|
||||
http,
|
||||
options,
|
||||
renderDashboardLink,
|
||||
renderDocLink,
|
||||
from,
|
||||
startServices: { notifications, ...startServices },
|
||||
}: {
|
||||
dashboard?: DashboardStart;
|
||||
to: string;
|
||||
errorMessage?: string;
|
||||
http: HttpSetup;
|
||||
options: Options;
|
||||
renderDashboardLink?: (message: string, dashboardUrl: string) => React.ReactNode;
|
||||
renderDocLink?: (message: string) => React.ReactNode;
|
||||
from: string;
|
||||
startServices: StartRenderServices;
|
||||
}) => {
|
||||
const res = await http
|
||||
.post<
|
||||
Record<
|
||||
DashboardsSavedObjectTemplate,
|
||||
{
|
||||
success?: boolean;
|
||||
error: Error;
|
||||
body?: Array<{ type: string; title: string; id: string; name: string }>;
|
||||
}
|
||||
>
|
||||
>(prebuiltSavedObjectsBulkCreateUrl(options.templateName), { version: '1' })
|
||||
.then((result) => {
|
||||
const response = result[options.templateName];
|
||||
const error = response?.error?.message;
|
||||
|
||||
if (error) {
|
||||
notifications.toasts.addError(new Error(errorMessage ?? IMPORT_SAVED_OBJECTS_FAILURE), {
|
||||
title: errorMessage ?? IMPORT_SAVED_OBJECTS_FAILURE,
|
||||
toastMessage: renderDocLink ? (renderDocLink(error) as unknown as string) : error,
|
||||
toastLifeTimeMs,
|
||||
});
|
||||
} else {
|
||||
const dashboardTitle =
|
||||
options.templateName === `${RiskScoreEntity.user}RiskScoreDashboards`
|
||||
? RISKY_USERS_DASHBOARD_TITLE
|
||||
: RISKY_HOSTS_DASHBOARD_TITLE;
|
||||
|
||||
const targetDashboard = response?.body?.find(
|
||||
(obj) => obj.type === 'dashboard' && obj?.title === dashboardTitle
|
||||
);
|
||||
|
||||
let targetUrl;
|
||||
if (targetDashboard?.id) {
|
||||
targetUrl = dashboard?.locator?.getRedirectUrl({
|
||||
dashboardId: targetDashboard?.id,
|
||||
timeRange: {
|
||||
to,
|
||||
from,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const successMessage = response?.body?.map((o) => o?.title ?? o?.name).join(', ');
|
||||
|
||||
if (successMessage == null || response?.body?.length == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
notifications.toasts.addSuccess({
|
||||
'data-test-subj': `${options.templateName}SuccessToast`,
|
||||
title: IMPORT_SAVED_OBJECTS_SUCCESS(response?.body?.length),
|
||||
text: toMountPoint(
|
||||
renderDashboardLink && targetUrl
|
||||
? renderDashboardLink(successMessage, targetUrl)
|
||||
: successMessage,
|
||||
startServices
|
||||
),
|
||||
toastLifeTimeMs,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
notifications.toasts.addError(new Error(errorMessage ?? IMPORT_SAVED_OBJECTS_FAILURE), {
|
||||
title: errorMessage ?? IMPORT_SAVED_OBJECTS_FAILURE,
|
||||
toastMessage: renderDocLink ? renderDocLink(e?.body?.message) : e?.body?.message,
|
||||
toastLifeTimeMs,
|
||||
});
|
||||
});
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const bulkDeletePrebuiltSavedObjects = async ({
|
||||
http,
|
||||
errorMessage,
|
||||
options,
|
||||
startServices: { notifications },
|
||||
}: {
|
||||
http: HttpSetup;
|
||||
errorMessage?: string;
|
||||
options: Options;
|
||||
startServices: StartRenderServices;
|
||||
}) => {
|
||||
const res = await http
|
||||
.post(prebuiltSavedObjectsBulkDeleteUrl(options.templateName), { version: '1' })
|
||||
.catch((e) => {
|
||||
notifications.toasts.addDanger({
|
||||
title: errorMessage ?? DELETE_SAVED_OBJECTS_FAILURE,
|
||||
text: e?.body?.message,
|
||||
});
|
||||
});
|
||||
|
||||
return res;
|
||||
};
|
|
@ -1,84 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import type { HttpSetup } from '@kbn/core/public';
|
||||
import { createStoredScript, deleteStoredScript } from './stored_scripts';
|
||||
|
||||
const mockRequest = jest.fn();
|
||||
const mockHttp = {
|
||||
put: mockRequest,
|
||||
post: mockRequest,
|
||||
delete: mockRequest,
|
||||
} as unknown as HttpSetup;
|
||||
|
||||
const startServices = coreMock.createStart();
|
||||
const mockAddDanger = jest.spyOn(startServices.notifications.toasts, 'addDanger');
|
||||
|
||||
const mockRenderDocLink = jest.fn();
|
||||
|
||||
describe('createStoredScript', () => {
|
||||
const mockOptions = { id: 'test', script: { lang: 'painless', source: '' } };
|
||||
|
||||
beforeAll(async () => {
|
||||
mockRequest.mockRejectedValue({ body: { message: 'test error' } });
|
||||
await createStoredScript({
|
||||
http: mockHttp,
|
||||
options: mockOptions,
|
||||
renderDocLink: mockRenderDocLink,
|
||||
startServices,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('create stored script', () => {
|
||||
expect(mockRequest.mock.calls[0][0]).toEqual(`/internal/risk_score/stored_scripts/create`);
|
||||
});
|
||||
|
||||
it('handles error', () => {
|
||||
expect(mockAddDanger.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"text": [Function],
|
||||
"title": "Failed to create stored script",
|
||||
}
|
||||
`);
|
||||
expect(mockRenderDocLink.mock.calls[0][0]).toEqual('test error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteStoredScript', () => {
|
||||
const mockOptions = { id: 'test' };
|
||||
|
||||
beforeAll(async () => {
|
||||
mockRequest.mockRejectedValue({ body: { message: 'test error' } });
|
||||
await deleteStoredScript({
|
||||
http: mockHttp,
|
||||
options: mockOptions,
|
||||
renderDocLink: mockRenderDocLink,
|
||||
startServices,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('delete stored script', () => {
|
||||
expect(mockRequest.mock.calls[0][0]).toEqual(`/internal/risk_score/stored_scripts/delete`);
|
||||
});
|
||||
|
||||
it('handles error', () => {
|
||||
expect(mockAddDanger.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"text": [Function],
|
||||
"title": "Failed to delete stored script",
|
||||
}
|
||||
`);
|
||||
expect(mockRenderDocLink.mock.calls[0][0]).toEqual('test error');
|
||||
});
|
||||
});
|
|
@ -1,92 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
import {
|
||||
RISK_SCORE_CREATE_STORED_SCRIPT,
|
||||
RISK_SCORE_DELETE_STORED_SCRIPT,
|
||||
} from '../../../../common/constants';
|
||||
import {
|
||||
STORED_SCRIPT_CREATION_ERROR_MESSAGE,
|
||||
STORED_SCRIPT_DELETION_ERROR_MESSAGE,
|
||||
} from './translations';
|
||||
import type { CreateStoredScript, DeleteStoredScript, DeleteStoredScripts } from './types';
|
||||
|
||||
export async function createStoredScript({
|
||||
errorMessage,
|
||||
http,
|
||||
options,
|
||||
renderDocLink,
|
||||
signal,
|
||||
startServices: { notifications, ...startServices },
|
||||
}: CreateStoredScript) {
|
||||
const res = await http
|
||||
.put(RISK_SCORE_CREATE_STORED_SCRIPT, {
|
||||
version: '1',
|
||||
body: JSON.stringify(options),
|
||||
signal,
|
||||
})
|
||||
.catch((e) => {
|
||||
notifications.toasts.addDanger({
|
||||
title: errorMessage ?? STORED_SCRIPT_CREATION_ERROR_MESSAGE,
|
||||
text: toMountPoint(
|
||||
renderDocLink ? renderDocLink(e?.body?.message) : e?.body?.message,
|
||||
startServices
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function deleteStoredScript({
|
||||
errorMessage,
|
||||
http,
|
||||
options,
|
||||
renderDocLink,
|
||||
signal,
|
||||
startServices: { notifications, ...startServices },
|
||||
}: DeleteStoredScript) {
|
||||
const res = await http
|
||||
.delete(RISK_SCORE_DELETE_STORED_SCRIPT, {
|
||||
version: '1',
|
||||
body: JSON.stringify(options),
|
||||
signal,
|
||||
})
|
||||
.catch((e) => {
|
||||
notifications.toasts.addDanger({
|
||||
title: errorMessage ?? STORED_SCRIPT_DELETION_ERROR_MESSAGE,
|
||||
text: toMountPoint(
|
||||
renderDocLink ? renderDocLink(e?.body?.message) : e?.body?.message,
|
||||
startServices
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function deleteStoredScripts({
|
||||
http,
|
||||
signal,
|
||||
errorMessage,
|
||||
ids,
|
||||
startServices,
|
||||
}: DeleteStoredScripts) {
|
||||
const result = await Promise.all(
|
||||
ids.map((id) => {
|
||||
return deleteStoredScript({
|
||||
http,
|
||||
signal,
|
||||
errorMessage,
|
||||
options: { id },
|
||||
startServices,
|
||||
});
|
||||
})
|
||||
);
|
||||
return result;
|
||||
}
|
|
@ -1,214 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import type { HttpSetup } from '@kbn/core/public';
|
||||
import { createTransform, deleteTransforms, getTransformState, stopTransforms } from './transforms';
|
||||
|
||||
const mockRequest = jest.fn();
|
||||
const mockHttp = {
|
||||
get: mockRequest,
|
||||
put: mockRequest,
|
||||
post: mockRequest,
|
||||
delete: mockRequest,
|
||||
} as unknown as HttpSetup;
|
||||
|
||||
const startServices = coreMock.createStart();
|
||||
const mockAddError = jest.spyOn(startServices.notifications.toasts, 'addError');
|
||||
|
||||
const mockRenderDocLink = jest.fn();
|
||||
|
||||
describe('createTransform', () => {
|
||||
const mockOptions = {};
|
||||
|
||||
beforeAll(async () => {
|
||||
mockRequest.mockResolvedValue({
|
||||
errors: [
|
||||
{ id: 'test', error: { name: 'test error', output: { payload: { cause: 'unknown' } } } },
|
||||
],
|
||||
});
|
||||
await createTransform({
|
||||
http: mockHttp,
|
||||
options: mockOptions,
|
||||
renderDocLink: mockRenderDocLink,
|
||||
transformId: 'test',
|
||||
startServices,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('create transform', () => {
|
||||
expect(mockRequest.mock.calls[0][0]).toEqual(`/internal/transform/transforms/test`);
|
||||
});
|
||||
|
||||
it('handles error', () => {
|
||||
expect(mockAddError.mock.calls[0][1].title).toEqual('Failed to create Transform');
|
||||
expect(mockRenderDocLink.mock.calls[0][0]).toEqual('test: unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTransformState', () => {
|
||||
beforeAll(async () => {
|
||||
mockRequest.mockResolvedValue({
|
||||
count: 0,
|
||||
});
|
||||
await getTransformState({
|
||||
http: mockHttp,
|
||||
renderDocLink: mockRenderDocLink,
|
||||
transformId: 'test',
|
||||
startServices,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('get transform state', () => {
|
||||
expect(mockRequest.mock.calls[0][0]).toEqual(`/internal/transform/transforms/test/_stats`);
|
||||
});
|
||||
|
||||
it('handles error', () => {
|
||||
expect(mockAddError.mock.calls[0][1].title).toEqual('Failed to get Transform state');
|
||||
expect(mockRenderDocLink.mock.calls[0][0]).toEqual('Transform not found: test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('startTransforms', () => {
|
||||
beforeAll(async () => {
|
||||
mockRequest.mockResolvedValue({
|
||||
count: 0,
|
||||
});
|
||||
await getTransformState({
|
||||
http: mockHttp,
|
||||
renderDocLink: mockRenderDocLink,
|
||||
transformId: 'test',
|
||||
startServices,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('get transform state', () => {
|
||||
expect(mockRequest.mock.calls[0][0]).toEqual(`/internal/transform/transforms/test/_stats`);
|
||||
});
|
||||
|
||||
it('handles error', () => {
|
||||
expect(mockAddError.mock.calls[0][1].title).toEqual('Failed to get Transform state');
|
||||
expect(mockRenderDocLink.mock.calls[0][0]).toEqual('Transform not found: test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('stopTransforms', () => {
|
||||
beforeAll(async () => {
|
||||
// mock get transform state result
|
||||
mockRequest.mockResolvedValueOnce({
|
||||
transforms: [{ id: 'test', state: 'stopped' }],
|
||||
count: 1,
|
||||
});
|
||||
// mock stop transform result
|
||||
mockRequest.mockResolvedValueOnce({
|
||||
test: {
|
||||
success: false,
|
||||
error: {
|
||||
root_cause: '',
|
||||
type: '',
|
||||
reason: 'unknown',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await stopTransforms({
|
||||
http: mockHttp,
|
||||
transformIds: ['test'],
|
||||
renderDocLink: mockRenderDocLink,
|
||||
startServices,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('get transform state', () => {
|
||||
expect(mockRequest.mock.calls[0][0]).toEqual(`/internal/transform/transforms/test/_stats`);
|
||||
});
|
||||
|
||||
it('stop transform', () => {
|
||||
expect(mockRequest.mock.calls[1][0]).toEqual(`/internal/transform/stop_transforms`);
|
||||
});
|
||||
|
||||
it('handles error', () => {
|
||||
expect(mockAddError.mock.calls[0][1].title).toEqual('Failed to stop Transform');
|
||||
expect(mockRenderDocLink.mock.calls[0][0]).toEqual('test: unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteTransforms', () => {
|
||||
const mockOptions = {
|
||||
deleteDestIndex: true,
|
||||
deleteDestDataView: true,
|
||||
forceDelete: false,
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
// mock get transform state result
|
||||
mockRequest.mockResolvedValueOnce({
|
||||
transforms: [{ id: 'test', state: 'stopped' }],
|
||||
count: 1,
|
||||
});
|
||||
// mock stop transform result
|
||||
mockRequest.mockResolvedValueOnce({
|
||||
test: {
|
||||
success: true,
|
||||
},
|
||||
});
|
||||
// mock delete transform result
|
||||
mockRequest.mockResolvedValue({
|
||||
test: {
|
||||
transformDeleted: {
|
||||
success: false,
|
||||
error: {
|
||||
root_cause: '',
|
||||
type: '',
|
||||
reason: 'unknown',
|
||||
},
|
||||
},
|
||||
destIndexDeleted: false,
|
||||
destDataViewDeleted: false,
|
||||
},
|
||||
});
|
||||
await deleteTransforms({
|
||||
http: mockHttp,
|
||||
options: mockOptions,
|
||||
transformIds: ['test'],
|
||||
renderDocLink: mockRenderDocLink,
|
||||
startServices,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('get transform state', () => {
|
||||
expect(mockRequest.mock.calls[0][0]).toEqual(`/internal/transform/transforms/test/_stats`);
|
||||
});
|
||||
|
||||
it('stop transform', () => {
|
||||
expect(mockRequest.mock.calls[1][0]).toEqual(`/internal/transform/stop_transforms`);
|
||||
});
|
||||
|
||||
it('delete transform', () => {
|
||||
expect(mockRequest.mock.calls[2][0]).toEqual(`/internal/transform/delete_transforms`);
|
||||
});
|
||||
|
||||
it('handles error', () => {
|
||||
expect(mockAddError.mock.calls[0][1].title).toEqual('Failed to delete Transform');
|
||||
expect(mockRenderDocLink.mock.calls[0][0]).toEqual('test: unknown');
|
||||
});
|
||||
});
|
|
@ -1,327 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
GET_TRANSFORM_STATE_ERROR_MESSAGE,
|
||||
GET_TRANSFORM_STATE_NOT_FOUND_MESSAGE,
|
||||
START_TRANSFORMS_ERROR_MESSAGE,
|
||||
STOP_TRANSFORMS_ERROR_MESSAGE,
|
||||
TRANSFORM_CREATION_ERROR_MESSAGE,
|
||||
TRANSFORM_DELETION_ERROR_MESSAGE,
|
||||
} from './translations';
|
||||
import type {
|
||||
CreateTransform,
|
||||
CreateTransformResult,
|
||||
DeleteTransforms,
|
||||
DeleteTransformsResult,
|
||||
GetTransformsState,
|
||||
GetTransformState,
|
||||
StartTransforms,
|
||||
StartTransformsResult,
|
||||
StopTransforms,
|
||||
StopTransformsResult,
|
||||
} from './types';
|
||||
|
||||
const TRANSFORM_API_BASE_PATH = `/internal/transform`;
|
||||
const toastLifeTimeMs = 600000;
|
||||
|
||||
const getErrorToastMessage = ({
|
||||
messageBody,
|
||||
renderDocLink,
|
||||
}: {
|
||||
messageBody: string;
|
||||
renderDocLink?: (message: string) => React.ReactNode;
|
||||
}) => (renderDocLink ? (renderDocLink(messageBody) as unknown as string) : messageBody);
|
||||
|
||||
export async function createTransform({
|
||||
errorMessage,
|
||||
http,
|
||||
options,
|
||||
renderDocLink,
|
||||
signal,
|
||||
transformId,
|
||||
startServices: { notifications },
|
||||
}: CreateTransform) {
|
||||
const res = await http
|
||||
.put<CreateTransformResult>(`${TRANSFORM_API_BASE_PATH}/transforms/${transformId}`, {
|
||||
body: JSON.stringify(options),
|
||||
version: '1',
|
||||
signal,
|
||||
})
|
||||
.then((result) => {
|
||||
const { errors } = result;
|
||||
const errorMessageTitle = errorMessage ?? TRANSFORM_CREATION_ERROR_MESSAGE;
|
||||
|
||||
if (errors && errors.length > 0) {
|
||||
const failedIds = errors?.map<string>(({ id, error }) => {
|
||||
if (error?.output?.payload?.cause) {
|
||||
return `${id}: ${error?.output?.payload?.cause}`;
|
||||
}
|
||||
return id;
|
||||
}, []);
|
||||
|
||||
notifications.toasts.addError(new Error(errorMessageTitle), {
|
||||
title: errorMessageTitle,
|
||||
toastMessage: getErrorToastMessage({
|
||||
messageBody: failedIds.join(', '),
|
||||
renderDocLink,
|
||||
}),
|
||||
toastLifeTimeMs,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
})
|
||||
.catch((e) => {
|
||||
notifications.toasts.addError(e, {
|
||||
title: errorMessage ?? TRANSFORM_CREATION_ERROR_MESSAGE,
|
||||
toastMessage: getErrorToastMessage({ messageBody: e?.body?.message, renderDocLink }),
|
||||
toastLifeTimeMs,
|
||||
});
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function startTransforms({
|
||||
http,
|
||||
renderDocLink,
|
||||
signal,
|
||||
errorMessage,
|
||||
transformIds,
|
||||
startServices: { notifications },
|
||||
}: StartTransforms) {
|
||||
const res = await http
|
||||
.post<StartTransformsResult>(`${TRANSFORM_API_BASE_PATH}/start_transforms`, {
|
||||
body: JSON.stringify(
|
||||
transformIds.map((id) => ({
|
||||
id,
|
||||
}))
|
||||
),
|
||||
version: '1',
|
||||
signal,
|
||||
})
|
||||
.then((result) => {
|
||||
const failedIds = Object.entries(result).reduce<string[]>((acc, [key, val]) => {
|
||||
return !val.success
|
||||
? [...acc, val?.error?.reason ? `${key}: ${val?.error?.reason}` : key]
|
||||
: acc;
|
||||
}, []);
|
||||
const errorMessageTitle = errorMessage ?? START_TRANSFORMS_ERROR_MESSAGE(failedIds.length);
|
||||
|
||||
if (failedIds.length > 0) {
|
||||
notifications.toasts.addError(new Error(errorMessageTitle), {
|
||||
title: errorMessageTitle,
|
||||
toastMessage: getErrorToastMessage({
|
||||
messageBody: failedIds.join(', '),
|
||||
renderDocLink,
|
||||
}),
|
||||
toastLifeTimeMs,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
})
|
||||
.catch((e) => {
|
||||
notifications.toasts.addError(e, {
|
||||
title: errorMessage ?? START_TRANSFORMS_ERROR_MESSAGE(transformIds.length),
|
||||
toastMessage: getErrorToastMessage({ messageBody: e?.body?.message, renderDocLink }),
|
||||
toastLifeTimeMs,
|
||||
});
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function getTransformState({
|
||||
http,
|
||||
renderDocLink,
|
||||
signal,
|
||||
errorMessage = GET_TRANSFORM_STATE_ERROR_MESSAGE,
|
||||
transformId,
|
||||
startServices: { notifications },
|
||||
}: GetTransformState) {
|
||||
const res = await http
|
||||
.get<{ transforms: Array<{ id: string; state: string }>; count: number }>(
|
||||
`${TRANSFORM_API_BASE_PATH}/transforms/${transformId}/_stats`,
|
||||
{
|
||||
version: '1',
|
||||
signal,
|
||||
}
|
||||
)
|
||||
.then((result) => {
|
||||
if (result.count === 0) {
|
||||
notifications.toasts.addError(new Error(errorMessage), {
|
||||
title: errorMessage,
|
||||
toastMessage: getErrorToastMessage({
|
||||
messageBody: `${GET_TRANSFORM_STATE_NOT_FOUND_MESSAGE}: ${transformId}`,
|
||||
renderDocLink,
|
||||
}),
|
||||
toastLifeTimeMs,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
})
|
||||
.catch((e) => {
|
||||
notifications.toasts.addError(e, {
|
||||
title: errorMessage,
|
||||
toastMessage: getErrorToastMessage({ messageBody: e?.body?.message, renderDocLink }),
|
||||
toastLifeTimeMs,
|
||||
});
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function getTransformsState({
|
||||
http,
|
||||
signal,
|
||||
errorMessage,
|
||||
transformIds,
|
||||
startServices,
|
||||
}: GetTransformsState) {
|
||||
const states = await Promise.all(
|
||||
transformIds.map((transformId) => {
|
||||
const transformState = getTransformState({
|
||||
http,
|
||||
signal,
|
||||
errorMessage,
|
||||
transformId,
|
||||
startServices,
|
||||
});
|
||||
return transformState;
|
||||
})
|
||||
);
|
||||
return states;
|
||||
}
|
||||
|
||||
export async function stopTransforms({
|
||||
http,
|
||||
signal,
|
||||
errorMessage,
|
||||
transformIds,
|
||||
renderDocLink,
|
||||
startServices,
|
||||
}: StopTransforms) {
|
||||
const { notifications } = startServices;
|
||||
const states = await getTransformsState({ http, signal, transformIds, startServices });
|
||||
const res = await http
|
||||
.post<StopTransformsResult>(`${TRANSFORM_API_BASE_PATH}/stop_transforms`, {
|
||||
version: '1',
|
||||
body: JSON.stringify(
|
||||
states.reduce(
|
||||
(acc, state) =>
|
||||
state != null && state.transforms.length > 0
|
||||
? [
|
||||
...acc,
|
||||
{
|
||||
id: state.transforms[0].id,
|
||||
state: state.transforms[0].state,
|
||||
},
|
||||
]
|
||||
: acc,
|
||||
[] as Array<{ id: string; state: string }>
|
||||
)
|
||||
),
|
||||
signal,
|
||||
})
|
||||
.then((result) => {
|
||||
const failedIds = Object.entries(result).reduce<string[]>((acc, [key, val]) => {
|
||||
return !val.success
|
||||
? [...acc, val?.error?.reason ? `${key}: ${val?.error?.reason}` : key]
|
||||
: acc;
|
||||
}, []);
|
||||
|
||||
const errorMessageTitle = errorMessage ?? STOP_TRANSFORMS_ERROR_MESSAGE(failedIds.length);
|
||||
if (failedIds.length > 0) {
|
||||
notifications.toasts.addError(new Error(errorMessageTitle), {
|
||||
title: errorMessageTitle,
|
||||
toastMessage: getErrorToastMessage({
|
||||
messageBody: failedIds.join(', '),
|
||||
renderDocLink,
|
||||
}),
|
||||
toastLifeTimeMs,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
})
|
||||
.catch((e) => {
|
||||
notifications.toasts.addError(e, {
|
||||
title: errorMessage ?? STOP_TRANSFORMS_ERROR_MESSAGE(transformIds.length),
|
||||
toastMessage: getErrorToastMessage({
|
||||
messageBody: e?.body?.message,
|
||||
renderDocLink,
|
||||
}),
|
||||
toastLifeTimeMs,
|
||||
});
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function deleteTransforms({
|
||||
http,
|
||||
signal,
|
||||
errorMessage,
|
||||
transformIds,
|
||||
options,
|
||||
renderDocLink,
|
||||
startServices,
|
||||
}: DeleteTransforms) {
|
||||
const { notifications } = startServices;
|
||||
await stopTransforms({ http, signal, transformIds, startServices });
|
||||
const res = await http
|
||||
.post<DeleteTransformsResult>(`${TRANSFORM_API_BASE_PATH}/delete_transforms`, {
|
||||
version: '1',
|
||||
body: JSON.stringify({
|
||||
transformsInfo: transformIds.map((id) => ({
|
||||
id,
|
||||
state: 'stopped',
|
||||
})),
|
||||
...(options ? options : {}),
|
||||
}),
|
||||
signal,
|
||||
})
|
||||
.then((result) => {
|
||||
const failedIds = Object.entries(result).reduce<string[]>((acc, [key, val]) => {
|
||||
return !val.transformDeleted.success
|
||||
? [
|
||||
...acc,
|
||||
val?.transformDeleted?.error?.reason
|
||||
? `${key}: ${val?.transformDeleted?.error?.reason}`
|
||||
: key,
|
||||
]
|
||||
: acc;
|
||||
}, []);
|
||||
const errorMessageTitle = errorMessage ?? TRANSFORM_DELETION_ERROR_MESSAGE(failedIds.length);
|
||||
|
||||
if (failedIds.length > 0) {
|
||||
notifications.toasts.addError(new Error(errorMessageTitle), {
|
||||
title: errorMessageTitle,
|
||||
toastMessage: getErrorToastMessage({
|
||||
messageBody: failedIds.join(', '),
|
||||
renderDocLink,
|
||||
}),
|
||||
toastLifeTimeMs,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
})
|
||||
.catch((e) => {
|
||||
notifications.toasts.addError(e, {
|
||||
title: errorMessage ?? TRANSFORM_DELETION_ERROR_MESSAGE(transformIds.length),
|
||||
toastMessage: getErrorToastMessage({
|
||||
messageBody: e?.body?.message,
|
||||
renderDocLink,
|
||||
}),
|
||||
toastLifeTimeMs,
|
||||
});
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
|
@ -1,128 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const INGEST_PIPELINE_CREATION_ERROR_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.api.ingestPipeline.create.errorMessageTitle',
|
||||
{
|
||||
defaultMessage: 'Failed to create Ingest pipeline',
|
||||
}
|
||||
);
|
||||
|
||||
export const INGEST_PIPELINE_DELETION_ERROR_MESSAGE = (totalCount: number) =>
|
||||
i18n.translate('xpack.securitySolution.riskScore.api.ingestPipeline.delete.errorMessageTitle', {
|
||||
values: { totalCount },
|
||||
defaultMessage: `Failed to delete Ingest {totalCount, plural, =1 {pipeline} other {pipelines}}`,
|
||||
});
|
||||
|
||||
export const STORED_SCRIPT_CREATION_ERROR_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.api.storedScript.create.errorMessageTitle',
|
||||
{
|
||||
defaultMessage: 'Failed to create stored script',
|
||||
}
|
||||
);
|
||||
|
||||
export const STORED_SCRIPT_DELETION_ERROR_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.api.storedScript.delete.errorMessageTitle',
|
||||
{
|
||||
defaultMessage: `Failed to delete stored script`,
|
||||
}
|
||||
);
|
||||
|
||||
export const TRANSFORM_CREATION_ERROR_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.api.transforms.create.errorMessageTitle',
|
||||
{
|
||||
defaultMessage: 'Failed to create Transform',
|
||||
}
|
||||
);
|
||||
|
||||
export const TRANSFORM_DELETION_ERROR_MESSAGE = (totalCount: number) =>
|
||||
i18n.translate('xpack.securitySolution.riskScore.api.transforms.delete.errorMessageTitle', {
|
||||
values: { totalCount },
|
||||
defaultMessage: `Failed to delete {totalCount, plural, =1 {Transform} other {Transforms}}`,
|
||||
});
|
||||
|
||||
export const GET_TRANSFORM_STATE_ERROR_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.api.transforms.getState.errorMessageTitle',
|
||||
{
|
||||
defaultMessage: `Failed to get Transform state`,
|
||||
}
|
||||
);
|
||||
|
||||
export const GET_TRANSFORM_STATE_NOT_FOUND_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.api.transforms.getState.notFoundMessageTitle',
|
||||
{
|
||||
defaultMessage: `Transform not found`,
|
||||
}
|
||||
);
|
||||
|
||||
export const START_TRANSFORMS_ERROR_MESSAGE = (totalCount: number) =>
|
||||
i18n.translate('xpack.securitySolution.riskScore.api.transforms.start.errorMessageTitle', {
|
||||
values: { totalCount },
|
||||
defaultMessage: `Failed to start {totalCount, plural, =1 {Transform} other {Transforms}}`,
|
||||
});
|
||||
|
||||
export const STOP_TRANSFORMS_ERROR_MESSAGE = (totalCount: number) =>
|
||||
i18n.translate('xpack.securitySolution.riskScore.api.transforms.stop.errorMessageTitle', {
|
||||
values: { totalCount },
|
||||
defaultMessage: `Failed to stop {totalCount, plural, =1 {Transform} other {Transforms}}`,
|
||||
});
|
||||
|
||||
export const INSTALLATION_ERROR = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.install.errorMessageTitle',
|
||||
{
|
||||
defaultMessage: 'Installation error',
|
||||
}
|
||||
);
|
||||
|
||||
export const UNINSTALLATION_ERROR = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.uninstall.errorMessageTitle',
|
||||
{
|
||||
defaultMessage: 'Uninstallation error',
|
||||
}
|
||||
);
|
||||
|
||||
export const IMPORT_SAVED_OBJECTS_SUCCESS = (totalCount: number) =>
|
||||
i18n.translate('xpack.securitySolution.riskScore.savedObjects.bulkCreateSuccessTitle', {
|
||||
values: { totalCount },
|
||||
defaultMessage: `{totalCount} {totalCount, plural, =1 {saved object} other {saved objects}} imported successfully`,
|
||||
});
|
||||
|
||||
export const IMPORT_SAVED_OBJECTS_FAILURE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.savedObjects.bulkCreateFailureTitle',
|
||||
{
|
||||
defaultMessage: `Failed to import saved objects`,
|
||||
}
|
||||
);
|
||||
|
||||
export const DELETE_SAVED_OBJECTS_FAILURE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.savedObjects.bulkDeleteFailureTitle',
|
||||
{
|
||||
defaultMessage: `Failed to delete saved objects`,
|
||||
}
|
||||
);
|
||||
|
||||
export const HOST_RISK_SCORES_ENABLED_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.hostRiskScoresEnabledTitle',
|
||||
{
|
||||
defaultMessage: `Host Risk Scores enabled`,
|
||||
}
|
||||
);
|
||||
|
||||
export const USER_RISK_SCORES_ENABLED_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.userRiskScoresEnabledTitle',
|
||||
{
|
||||
defaultMessage: `User Risk Scores enabled`,
|
||||
}
|
||||
);
|
||||
|
||||
export const RISK_SCORES_ENABLED_TEXT = (items: string) =>
|
||||
i18n.translate('xpack.securitySolution.riskScore.savedObjects.enableRiskScoreSuccessTitle', {
|
||||
values: { items },
|
||||
defaultMessage: `{items} imported successfully`,
|
||||
});
|
|
@ -1,107 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type { HttpSetup } from '@kbn/core/public';
|
||||
import type { StartRenderServices } from '../../../types';
|
||||
|
||||
interface RiskyScoreApiBase {
|
||||
errorMessage?: string;
|
||||
http: HttpSetup;
|
||||
renderDocLink?: (message: string) => React.ReactNode;
|
||||
signal?: AbortSignal;
|
||||
startServices: StartRenderServices;
|
||||
}
|
||||
export interface CreateIngestPipeline extends RiskyScoreApiBase {
|
||||
options: {
|
||||
name: string;
|
||||
processors: string | Array<Record<string, unknown>>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DeleteIngestPipeline extends RiskyScoreApiBase {
|
||||
names: string;
|
||||
}
|
||||
|
||||
export interface CreateStoredScript extends RiskyScoreApiBase {
|
||||
options: {
|
||||
id: string;
|
||||
script: {
|
||||
lang: string | 'painless' | 'expression' | 'mustache' | 'java';
|
||||
options?: Record<string, string>;
|
||||
source: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface DeleteStoredScript extends RiskyScoreApiBase {
|
||||
options: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DeleteStoredScripts extends RiskyScoreApiBase {
|
||||
ids: string[];
|
||||
}
|
||||
|
||||
export interface CreateTransform extends RiskyScoreApiBase {
|
||||
transformId: string;
|
||||
options: string | Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface CreateTransformResult {
|
||||
transformsCreated: string[];
|
||||
errors?: Array<{
|
||||
id: string;
|
||||
error?: { name: string; output?: { payload?: { cause?: string } } };
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface StartTransforms extends RiskyScoreApiBase {
|
||||
transformIds: string[];
|
||||
}
|
||||
|
||||
interface TransformResult {
|
||||
success: boolean;
|
||||
error?: { root_cause?: unknown; type?: string; reason?: string };
|
||||
}
|
||||
|
||||
export type StartTransformsResult = Record<string, TransformResult>;
|
||||
|
||||
export interface StopTransforms extends RiskyScoreApiBase {
|
||||
transformIds: string[];
|
||||
}
|
||||
|
||||
export type StopTransformsResult = Record<string, TransformResult>;
|
||||
|
||||
export interface GetTransformState extends RiskyScoreApiBase {
|
||||
transformId: string;
|
||||
}
|
||||
|
||||
export interface GetTransformsState extends RiskyScoreApiBase {
|
||||
transformIds: string[];
|
||||
}
|
||||
|
||||
export interface DeleteTransforms extends RiskyScoreApiBase {
|
||||
transformIds: string[];
|
||||
options?: {
|
||||
deleteDestIndex?: boolean;
|
||||
deleteDestDataView?: boolean;
|
||||
forceDelete?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export type DeleteTransformsResult = Record<
|
||||
string,
|
||||
{
|
||||
transformDeleted: TransformResult;
|
||||
destIndexDeleted: {
|
||||
success: boolean;
|
||||
};
|
||||
destDataViewDeleted: {
|
||||
success: boolean;
|
||||
};
|
||||
}
|
||||
>;
|
|
@ -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 {
|
||||
RiskScoreEntity,
|
||||
getHostRiskIndex,
|
||||
getUserRiskIndex,
|
||||
} from '../../../common/search_strategy';
|
||||
import { useSpaceId } from '../../common/hooks/use_space_id';
|
||||
|
||||
export const useGetDefaulRiskIndex = (
|
||||
riskEntity: RiskScoreEntity,
|
||||
onlyLatest: boolean = true
|
||||
): string | undefined => {
|
||||
const spaceId = useSpaceId();
|
||||
|
||||
if (!spaceId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return riskEntity === RiskScoreEntity.host
|
||||
? getHostRiskIndex(spaceId, onlyLatest)
|
||||
: getUserRiskIndex(spaceId, onlyLatest);
|
||||
};
|
|
@ -11,11 +11,11 @@ Object {
|
|||
"fieldAttrs": Object {},
|
||||
"fieldFormats": Object {},
|
||||
"id": "d594baeb-5eca-480c-8885-ba79eaf41372",
|
||||
"name": "ml_host_risk_score_latest_mockSpaceId_no_timestamp",
|
||||
"name": "ea_host_risk_score_latest_mockSpaceId_no_timestamp",
|
||||
"runtimeFieldMap": Object {},
|
||||
"sourceFilters": Array [],
|
||||
"timeFieldName": "",
|
||||
"title": "ml_host_risk_score_latest_mockSpaceId",
|
||||
"title": "ea_host_risk_score_latest_mockSpaceId",
|
||||
},
|
||||
},
|
||||
"datasourceStates": Object {
|
||||
|
|
|
@ -11,11 +11,11 @@ Object {
|
|||
"fieldAttrs": Object {},
|
||||
"fieldFormats": Object {},
|
||||
"id": "d594baeb-5eca-480c-8885-ba79eaf41372",
|
||||
"name": "ml_host_risk_score_mockSpaceId",
|
||||
"name": "ea_host_risk_score_mockSpaceId",
|
||||
"runtimeFieldMap": Object {},
|
||||
"sourceFilters": Array [],
|
||||
"timeFieldName": "@timestamp",
|
||||
"title": "ml_host_risk_score_mockSpaceId",
|
||||
"title": "ea_host_risk_score_mockSpaceId",
|
||||
},
|
||||
},
|
||||
"datasourceStates": Object {
|
||||
|
|
|
@ -131,14 +131,14 @@ export const getRiskScoreDonutAttributes: GetLensAttributes = ({
|
|||
adHocDataViews: {
|
||||
[internalReferenceId]: {
|
||||
id: internalReferenceId,
|
||||
title: `ml_${stackByField}_risk_score_latest_${extraOptions.spaceId}`,
|
||||
title: `ea_${stackByField}_risk_score_latest_${extraOptions.spaceId}`,
|
||||
timeFieldName: '',
|
||||
sourceFilters: [],
|
||||
fieldFormats: {},
|
||||
runtimeFieldMap: {},
|
||||
fieldAttrs: {},
|
||||
allowNoIndex: false,
|
||||
name: `ml_${stackByField}_risk_score_latest_${extraOptions.spaceId}_no_timestamp`,
|
||||
name: `ea_${stackByField}_risk_score_latest_${extraOptions.spaceId}_no_timestamp`,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -180,14 +180,14 @@ export const getRiskScoreOverTimeAreaAttributes: GetLensAttributes = ({
|
|||
adHocDataViews: {
|
||||
[internalReferenceId]: {
|
||||
id: internalReferenceId,
|
||||
title: `ml_${stackByField}_risk_score_${extraOptions.spaceId}`,
|
||||
title: `ea_${stackByField}_risk_score_${extraOptions.spaceId}`,
|
||||
timeFieldName: '@timestamp',
|
||||
sourceFilters: [],
|
||||
fieldFormats: {},
|
||||
runtimeFieldMap: {},
|
||||
fieldAttrs: {},
|
||||
allowNoIndex: false,
|
||||
name: `ml_${stackByField}_risk_score_${extraOptions.spaceId}`,
|
||||
name: `ea_${stackByField}_risk_score_${extraOptions.spaceId}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -18,9 +18,6 @@ import { EmptyPrompt } from '../../common/components/empty_prompt';
|
|||
import { SiemSearchBar } from '../../common/components/search_bar';
|
||||
import { InputsModelId } from '../../common/store/inputs/constants';
|
||||
import { FiltersGlobal } from '../../common/components/filters_global';
|
||||
import { useRiskEngineStatus } from '../api/hooks/use_risk_engine_status';
|
||||
import { RiskScoreUpdatePanel } from '../components/risk_score_update_panel';
|
||||
import { useHasSecurityCapability } from '../../helper_hooks';
|
||||
import { EntityAnalyticsHeader } from '../components/entity_analytics_header';
|
||||
import { EntityAnalyticsAnomalies } from '../components/entity_analytics_anomalies';
|
||||
|
||||
|
@ -31,9 +28,7 @@ import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experime
|
|||
const EntityAnalyticsComponent = () => {
|
||||
const [skipEmptyPrompt, setSkipEmptyPrompt] = React.useState(false);
|
||||
const onSkip = React.useCallback(() => setSkipEmptyPrompt(true), [setSkipEmptyPrompt]);
|
||||
const { data: riskScoreEngineStatus } = useRiskEngineStatus();
|
||||
const { indicesExist, loading: isSourcererLoading, sourcererDataView } = useSourcererDataView();
|
||||
const isRiskScoreModuleLicenseAvailable = useHasSecurityCapability('entity-analytics');
|
||||
const isEntityStoreFeatureFlagDisabled = useIsExperimentalFeatureEnabled('entityStoreDisabled');
|
||||
const showEmptyPrompt = !indicesExist && !skipEmptyPrompt;
|
||||
return (
|
||||
|
@ -53,12 +48,6 @@ const EntityAnalyticsComponent = () => {
|
|||
<EuiLoadingSpinner size="l" data-test-subj="entityAnalyticsLoader" />
|
||||
) : (
|
||||
<EuiFlexGroup direction="column" data-test-subj="entityAnalyticsSections">
|
||||
{riskScoreEngineStatus?.isUpdateAvailable && isRiskScoreModuleLicenseAvailable && (
|
||||
<EuiFlexItem>
|
||||
<RiskScoreUpdatePanel />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
<EuiFlexItem>
|
||||
<EntityAnalyticsHeader />
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -35,21 +35,21 @@ export const HIDE_USERS_RISK_SCORE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const RISK_SCORE_MODULE_STATUS = i18n.translate(
|
||||
export const RISK_ENGINE_STATUS = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.riskScorePreview.status',
|
||||
{
|
||||
defaultMessage: 'Status',
|
||||
}
|
||||
);
|
||||
|
||||
export const RISK_SCORE_MODULE_STATUS_ON = i18n.translate(
|
||||
export const RISK_ENGINE_STATUS_ON = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.riskScorePreview.statusOn',
|
||||
{
|
||||
defaultMessage: 'On',
|
||||
}
|
||||
);
|
||||
|
||||
export const RISK_SCORE_MODULE_STATUS_OFF = i18n.translate(
|
||||
export const RISK_ENGINE_STATUS_OFF = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.riskScorePreview.statusOff',
|
||||
{
|
||||
defaultMessage: 'Off',
|
||||
|
@ -77,20 +77,6 @@ export const EA_DASHBOARD_LINK = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const EA_DOCS_RISK_HOSTS = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.riskScorePreview.eaDocsHosts',
|
||||
{
|
||||
defaultMessage: 'Host risk score',
|
||||
}
|
||||
);
|
||||
|
||||
export const EA_DOCS_RISK_USERS = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.riskScorePreview.eaDocsUsers',
|
||||
{
|
||||
defaultMessage: 'User risk score',
|
||||
}
|
||||
);
|
||||
|
||||
export const EA_DOCS_ENTITY_RISK_SCORE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.riskScorePreview.eaDocsEntities',
|
||||
{
|
||||
|
@ -145,71 +131,6 @@ export const PREVIEW_QUERY_ERROR_TITLE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const UPDATE_AVAILABLE = i18n.translate('xpack.securitySolution.riskScore.updateAvailable', {
|
||||
defaultMessage: 'Update available',
|
||||
});
|
||||
|
||||
export const START_UPDATE = i18n.translate('xpack.securitySolution.riskScore.startUpdate', {
|
||||
defaultMessage: 'Start update',
|
||||
});
|
||||
|
||||
export const UPDATING_RISK_ENGINE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.updatingRiskEngine',
|
||||
{
|
||||
defaultMessage: 'Updating risk engine...',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_RISK_ENGINE_MODAL_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.updateRiskEngineModa.title',
|
||||
{
|
||||
defaultMessage: 'Do you want to update the entity risk engine?',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_RISK_ENGINE_MODAL_EXISTING_USER_HOST_1 = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.updateRiskEngineModal.existingUserHost_1',
|
||||
{
|
||||
defaultMessage: 'Existing user and host risk score transforms will be deleted',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_RISK_ENGINE_MODAL_EXISTING_USER_HOST_2 = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.updateRiskEngineModal.existingUserHost_2',
|
||||
{
|
||||
defaultMessage: ', as they are no longer required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_RISK_ENGINE_MODAL_EXISTING_DATA_1 = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.updateRiskEngineModal.existingData_1',
|
||||
{
|
||||
defaultMessage: 'Legacy risk score data will not be deleted',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_RISK_ENGINE_MODAL_EXISTING_DATA_2 = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.updateRiskEngineModal.existingData_2',
|
||||
{
|
||||
defaultMessage:
|
||||
', it will still exist in the index but will no longer be available in the user interface. You will need to remove legacy risk score data manually.',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_RISK_ENGINE_MODAL_BUTTON_NO = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.updateRiskEngineModal.buttonNo',
|
||||
{
|
||||
defaultMessage: 'No, not yet',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_RISK_ENGINE_MODAL_BUTTON_YES = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.updateRiskEngineModal.buttonYes',
|
||||
{
|
||||
defaultMessage: 'Yes, update now!',
|
||||
}
|
||||
);
|
||||
|
||||
export const ERROR_PANEL_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.errorPanel.title',
|
||||
{
|
||||
|
@ -231,50 +152,6 @@ export const ERROR_PANEL_ERRORS = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const UPDATE_PANEL_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.updatePanel.title',
|
||||
{
|
||||
defaultMessage: 'New entity risk scoring engine available',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_PANEL_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.updatePanel.message',
|
||||
{
|
||||
defaultMessage:
|
||||
'A new entity risk scoring engine is available. Update now to get the latest features.',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_PANEL_GO_TO_MANAGE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.updatePanel.goToManage',
|
||||
{
|
||||
defaultMessage: 'Manage',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_PANEL_GO_TO_DISMISS = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.updatePanel.Dismiss',
|
||||
{
|
||||
defaultMessage: 'Dismiss',
|
||||
}
|
||||
);
|
||||
|
||||
export const getMaxSpaceTitle = (maxSpaces: number) =>
|
||||
i18n.translate('xpack.securitySolution.riskScore.maxSpacePanel.title', {
|
||||
defaultMessage:
|
||||
'You cannot enable entity risk scoring in more than {maxSpaces, plural, =1 {# Kibana space} other {# Kibana spaces}}.',
|
||||
values: { maxSpaces },
|
||||
});
|
||||
|
||||
export const MAX_SPACE_PANEL_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.maxSpacePanel.message',
|
||||
{
|
||||
defaultMessage:
|
||||
'You can disable entity risk scoring in the space it is currently enabled before enabling it in this space',
|
||||
}
|
||||
);
|
||||
|
||||
export const CHECK_PRIVILEGES = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.errors.privileges.check',
|
||||
{
|
||||
|
@ -289,14 +166,14 @@ export const NEED_TO_HAVE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const RISK_SCORE_MODULE_TURNED_ON = i18n.translate(
|
||||
export const RISK_ENGINE_TURNED_ON = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.moduleTurnedOn',
|
||||
{
|
||||
defaultMessage: 'Entity risk score has been turned on',
|
||||
}
|
||||
);
|
||||
|
||||
export const RISK_SCORE_MODULE_TURNED_OFF = i18n.translate(
|
||||
export const RISK_ENGINE_TURNED_OFF = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.moduleTurnedOff',
|
||||
{
|
||||
defaultMessage: 'Entity risk score has been turned off',
|
||||
|
|
|
@ -15,11 +15,6 @@ jest.mock('../../../../common/containers/use_search_strategy', () => ({
|
|||
useSearchStrategy: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../entity_analytics/api/hooks/use_risk_engine_status', () => ({
|
||||
useIsNewRiskScoreModuleInstalled: jest
|
||||
.fn()
|
||||
.mockReturnValue({ isLoading: false, installed: true }),
|
||||
}));
|
||||
const mockUseSearchStrategy = useSearchStrategy as jest.Mock;
|
||||
const mockSearch = jest.fn();
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@ import type { ESTermQuery } from '../../../../../common/typed_json';
|
|||
import * as i18n from './translations';
|
||||
import type { InspectResponse } from '../../../../types';
|
||||
import { useSearchStrategy } from '../../../../common/containers/use_search_strategy';
|
||||
import { useIsNewRiskScoreModuleInstalled } from '../../../../entity_analytics/api/hooks/use_risk_engine_status';
|
||||
|
||||
export const ID = 'hostsAllQuery';
|
||||
|
||||
|
@ -62,9 +61,6 @@ export const useAllHost = ({
|
|||
getHostsSelector(state, type)
|
||||
);
|
||||
|
||||
const { installed: isNewRiskScoreModuleInstalled, isLoading: riskScoreStatusLoading } =
|
||||
useIsNewRiskScoreModuleInstalled();
|
||||
|
||||
const [hostsRequest, setHostRequest] = useState<HostsRequestOptionsInput | null>(null);
|
||||
|
||||
const wrappedLoadMore = useCallback(
|
||||
|
@ -130,9 +126,6 @@ export const useAllHost = ({
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (riskScoreStatusLoading) {
|
||||
return;
|
||||
}
|
||||
setHostRequest((prevRequest) => {
|
||||
const myRequest: HostsRequestOptionsInput = {
|
||||
...(prevRequest ?? {}),
|
||||
|
@ -149,25 +142,13 @@ export const useAllHost = ({
|
|||
direction,
|
||||
field: sortField,
|
||||
},
|
||||
isNewRiskScoreModuleInstalled,
|
||||
};
|
||||
if (!deepEqual(prevRequest, myRequest)) {
|
||||
return myRequest;
|
||||
}
|
||||
return prevRequest;
|
||||
});
|
||||
}, [
|
||||
activePage,
|
||||
direction,
|
||||
endDate,
|
||||
filterQuery,
|
||||
indexNames,
|
||||
limit,
|
||||
startDate,
|
||||
sortField,
|
||||
isNewRiskScoreModuleInstalled,
|
||||
riskScoreStatusLoading,
|
||||
]);
|
||||
}, [activePage, direction, endDate, filterQuery, indexNames, limit, startDate, sortField]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!skip && hostsRequest) {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { EuiPanel } from '@elastic/eui';
|
||||
import { noop } from 'lodash/fp';
|
||||
import { RiskScoresNoDataDetected } from '../../../../entity_analytics/components/risk_score_no_data_detected';
|
||||
import { useUpsellingComponent } from '../../../../common/hooks/use_upselling';
|
||||
import { RiskEnginePrivilegesCallOut } from '../../../../entity_analytics/components/risk_engine_privileges_callout';
|
||||
import { useMissingRiskEnginePrivileges } from '../../../../entity_analytics/hooks/use_missing_risk_engine_privileges';
|
||||
|
@ -23,9 +24,6 @@ import { hostsModel, hostsSelectors } from '../../store';
|
|||
import type { State } from '../../../../common/store';
|
||||
import { useQueryToggle } from '../../../../common/containers/query_toggle';
|
||||
import { EMPTY_SEVERITY_COUNT, RiskScoreEntity } from '../../../../../common/search_strategy';
|
||||
import { RiskScoresNoDataDetected } from '../../../../entity_analytics/components/risk_score_onboarding/risk_score_no_data_detected';
|
||||
import { useRiskEngineStatus } from '../../../../entity_analytics/api/hooks/use_risk_engine_status';
|
||||
import { RiskScoreUpdatePanel } from '../../../../entity_analytics/components/risk_score_update_panel';
|
||||
|
||||
const HostRiskScoreTableManage = manageQuery(HostRiskScoreTable);
|
||||
|
||||
|
@ -50,8 +48,6 @@ export const HostRiskScoreQueryTabBody = ({
|
|||
getHostRiskScoreFilterQuerySelector(state, hostsModel.HostsType.page)
|
||||
);
|
||||
|
||||
const { data: riskScoreEngineStatus } = useRiskEngineStatus();
|
||||
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
cursorStart: activePage * limit,
|
||||
|
@ -68,23 +64,15 @@ export const HostRiskScoreQueryTabBody = ({
|
|||
const timerange = useMemo(() => ({ from, to }), [from, to]);
|
||||
|
||||
const privileges = useMissingRiskEnginePrivileges();
|
||||
const {
|
||||
data,
|
||||
inspect,
|
||||
isDeprecated,
|
||||
isInspected,
|
||||
isModuleEnabled,
|
||||
loading,
|
||||
refetch,
|
||||
totalCount,
|
||||
} = useRiskScore({
|
||||
filterQuery,
|
||||
pagination,
|
||||
riskEntity: RiskScoreEntity.host,
|
||||
skip: querySkip,
|
||||
sort,
|
||||
timerange,
|
||||
});
|
||||
const { data, inspect, isInspected, hasEngineBeenInstalled, loading, refetch, totalCount } =
|
||||
useRiskScore({
|
||||
filterQuery,
|
||||
pagination,
|
||||
riskEntity: RiskScoreEntity.host,
|
||||
skip: querySkip,
|
||||
sort,
|
||||
timerange,
|
||||
});
|
||||
|
||||
const { severityCount, loading: isKpiLoading } = useRiskScoreKpi({
|
||||
filterQuery,
|
||||
|
@ -92,11 +80,7 @@ export const HostRiskScoreQueryTabBody = ({
|
|||
riskEntity: RiskScoreEntity.host,
|
||||
});
|
||||
|
||||
const status = {
|
||||
isDisabled: !isModuleEnabled && !loading,
|
||||
isDeprecated: isDeprecated && !loading,
|
||||
};
|
||||
|
||||
const isDisabled = !hasEngineBeenInstalled && !loading;
|
||||
const RiskScoreUpsell = useUpsellingComponent('entity_analytics_panel');
|
||||
|
||||
if (RiskScoreUpsell) {
|
||||
|
@ -111,32 +95,26 @@ export const HostRiskScoreQueryTabBody = ({
|
|||
);
|
||||
}
|
||||
|
||||
if (status.isDisabled || status.isDeprecated) {
|
||||
if (isDisabled) {
|
||||
return (
|
||||
<EuiPanel hasBorder>
|
||||
<EnableRiskScore
|
||||
{...status}
|
||||
entityType={RiskScoreEntity.host}
|
||||
refetch={refetch}
|
||||
timerange={timerange}
|
||||
/>
|
||||
<EnableRiskScore isDisabled={isDisabled} entityType={RiskScoreEntity.host} />
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!loading &&
|
||||
isModuleEnabled &&
|
||||
hasEngineBeenInstalled &&
|
||||
severitySelectionRedux.length === 0 &&
|
||||
data &&
|
||||
data.length === 0
|
||||
) {
|
||||
return <RiskScoresNoDataDetected entityType={RiskScoreEntity.host} refetch={refetch} />;
|
||||
return <RiskScoresNoDataDetected entityType={RiskScoreEntity.host} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{riskScoreEngineStatus?.isUpdateAvailable && <RiskScoreUpdatePanel />}
|
||||
<HostRiskScoreTableManage
|
||||
deleteQuery={deleteQuery}
|
||||
data={data ?? []}
|
||||
|
|
|
@ -15,11 +15,6 @@ import { UsersType } from '../../store/model';
|
|||
|
||||
jest.mock('../../../../common/containers/query_toggle');
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('../../../../entity_analytics/api/hooks/use_risk_engine_status', () => ({
|
||||
useIsNewRiskScoreModuleInstalled: jest
|
||||
.fn()
|
||||
.mockReturnValue({ isLoading: false, installed: true }),
|
||||
}));
|
||||
|
||||
const mockSearch = jest.fn();
|
||||
|
||||
|
|
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