Deprecate Cloud Defend

This commit is contained in:
Ido Cohen 2025-03-11 15:45:13 +02:00 committed by GitHub
parent ec3b18662a
commit aa850d4b9f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
132 changed files with 16 additions and 10527 deletions

3
.github/CODEOWNERS vendored
View file

@ -1010,7 +1010,6 @@ x-pack/solutions/security/packages/security-ai-prompts @elastic/security-generat
x-pack/solutions/security/packages/side-nav @elastic/security-threat-hunting-explore
x-pack/solutions/security/packages/storybook/config @elastic/security-threat-hunting-explore
x-pack/solutions/security/packages/upselling @elastic/security-threat-hunting-explore
x-pack/solutions/security/plugins/cloud_defend @elastic/kibana-cloud-security-posture
x-pack/solutions/security/plugins/cloud_security_posture @elastic/kibana-cloud-security-posture
x-pack/solutions/security/plugins/ecs_data_quality_dashboard @elastic/security-threat-hunting-explore
x-pack/solutions/security/plugins/elastic_assistant @elastic/security-generative-ai
@ -2553,12 +2552,10 @@ x-pack/test/automatic_import_api_integration @elastic/security-scalability
## Packages
x-pack/packages/kbn-cloud-security-posture @elastic/kibana-cloud-security-posture
## Plugins
x-pack/plugins/cloud_defend @elastic/kibana-cloud-security-posture
x-pack/plugins/cloud_security_posture @elastic/kibana-cloud-security-posture
x-pack/plugins/kubernetes_security @elastic/kibana-cloud-security-posture
## Security Solution sub teams
x-pack/solutions/security/plugins/security_solution/public/common/components/sessions_viewer @elastic/kibana-cloud-security-posture
x-pack/solutions/security/plugins/security_solution/public/cloud_defend @elastic/kibana-cloud-security-posture
x-pack/solutions/security/plugins/security_solution/public/cloud_security_posture @elastic/kibana-cloud-security-posture
x-pack/solutions/security/plugins/security_solution/public/kubernetes @elastic/kibana-cloud-security-posture
x-pack/solutions/security/plugins/security_solution/server/lib/asset_inventory @elastic/kibana-cloud-security-posture

View file

@ -216,7 +216,6 @@
"@kbn/cloud": "link:src/platform/packages/shared/cloud",
"@kbn/cloud-chat-plugin": "link:x-pack/platform/plugins/private/cloud_integrations/cloud_chat",
"@kbn/cloud-data-migration-plugin": "link:x-pack/platform/plugins/private/cloud_integrations/cloud_data_migration",
"@kbn/cloud-defend-plugin": "link:x-pack/solutions/security/plugins/cloud_defend",
"@kbn/cloud-experiments-plugin": "link:x-pack/platform/plugins/private/cloud_integrations/cloud_experiments",
"@kbn/cloud-full-story-plugin": "link:x-pack/platform/plugins/private/cloud_integrations/cloud_full_story",
"@kbn/cloud-integration-saml-provider-plugin": "link:x-pack/test/cloud_integration/plugins/saml_provider",

View file

@ -12,7 +12,6 @@ pageLoadAssetSize:
charts: 55000
cloud: 21076
cloudDataMigration: 19170
cloudDefend: 18697
cloudExperiments: 109746
cloudFullStory: 18493
cloudLinks: 55984

View file

@ -29,7 +29,6 @@ export enum SecurityPageName {
cloudSecurityPostureDashboard = 'cloud_security_posture-dashboard',
cloudSecurityPostureFindings = 'cloud_security_posture-findings',
cloudSecurityPostureRules = 'cloud_security_posture-rules',
cloudDefendPolicies = 'cloud_defend-policies',
dashboards = 'dashboards',
dataQuality = 'data_quality',
detections = 'detections',

View file

@ -164,8 +164,6 @@
"@kbn/cloud-chat-plugin/*": ["x-pack/platform/plugins/private/cloud_integrations/cloud_chat/*"],
"@kbn/cloud-data-migration-plugin": ["x-pack/platform/plugins/private/cloud_integrations/cloud_data_migration"],
"@kbn/cloud-data-migration-plugin/*": ["x-pack/platform/plugins/private/cloud_integrations/cloud_data_migration/*"],
"@kbn/cloud-defend-plugin": ["x-pack/solutions/security/plugins/cloud_defend"],
"@kbn/cloud-defend-plugin/*": ["x-pack/solutions/security/plugins/cloud_defend/*"],
"@kbn/cloud-experiments-plugin": ["x-pack/platform/plugins/private/cloud_integrations/cloud_experiments"],
"@kbn/cloud-experiments-plugin/*": ["x-pack/platform/plugins/private/cloud_integrations/cloud_experiments/*"],
"@kbn/cloud-full-story-plugin": ["x-pack/platform/plugins/private/cloud_integrations/cloud_full_story"],

View file

@ -20,7 +20,6 @@
"xpack.canvas": "platform/plugins/private/canvas",
"xpack.cases": "platform/plugins/shared/cases",
"xpack.cloud": "platform/plugins/shared/cloud",
"xpack.cloudDefend": "solutions/security/plugins/cloud_defend",
"xpack.cloudLinks": "platform/plugins/private/cloud_integrations/cloud_links",
"xpack.cloudDataMigration": "platform/plugins/private/cloud_integrations/cloud_data_migration",
"xpack.csp": [

View file

@ -13983,89 +13983,6 @@
"xpack.cloudDataMigration.readInstructionsButtonLabel": "Déplacer vers Elastic Cloud",
"xpack.cloudDataMigration.slaBackedSupport.text": "Obtenir toutes les réponses à vos questions grâce au support technique conforme aux SLA",
"xpack.cloudDataMigration.upgrade.text": "Effectuer la mise à niveau vers les versions plus récentes beaucoup plus facilement",
"xpack.cloudDefend.addResponse": "Ajouter une réponse",
"xpack.cloudDefend.addSelector": "Ajouter un sélecteur",
"xpack.cloudDefend.addSelectorCondition": "Ajouter une condition",
"xpack.cloudDefend.alertActionRequired": "L'action d'alerte est requise lorsque l'action \"block\" est utilisée.",
"xpack.cloudDefend.cloudDefendPage.defaultNoDataConfig.pageTitle": "Aucune donnée trouvée",
"xpack.cloudDefend.cloudDefendPage.defaultNoDataConfig.solutionNameLabel": "Defend pour les conteneurs",
"xpack.cloudDefend.cloudDefendPage.errorRenderer.errorDescription": "{error} {statusCode} : {body}",
"xpack.cloudDefend.cloudDefendPage.errorRenderer.errorTitle": "Nous n'avons pas pu récupérer vos données de protection du cloud",
"xpack.cloudDefend.cloudDefendPage.loadingDescription": "Chargement...",
"xpack.cloudDefend.cloudDefendPage.packageNotInstalled.solutionNameLabel": "Defend pour les conteneurs (D4C)",
"xpack.cloudDefend.cloudDefendPage.packageNotInstalledRenderer.addCloudDefendmIntegrationButtonTitle": "Ajouter une intégration D4C",
"xpack.cloudDefend.cloudDefendPage.packageNotInstalledRenderer.learnMoreTitle": "Découvrir Defend pour les conteneurs (D4C)",
"xpack.cloudDefend.cloudDefendPage.packageNotInstalledRenderer.promptDescription": "Pour commencer, ajoutez l'intégration de Defend pour les conteneurs (D4C). {learnMore}.",
"xpack.cloudDefend.cloudDefendPage.packageNotInstalledRenderer.promptTitle": "Détectez les dérives de conteneur et bloquez les comportements malveillants à la source.",
"xpack.cloudDefend.conditions": "Conditions :",
"xpack.cloudDefend.controlDuplicate": "Dupliquer",
"xpack.cloudDefend.controlExclude": "Exclure",
"xpack.cloudDefend.controlExcludeSelectors": "Exclure les sélecteurs",
"xpack.cloudDefend.controlGeneralView": "Vue générale",
"xpack.cloudDefend.controlMatchSelectors": "Sélecteurs de correspondance",
"xpack.cloudDefend.controlRemove": "Supprimer",
"xpack.cloudDefend.controlResponseActionAlert": "Alerte",
"xpack.cloudDefend.controlResponseActionAlertAndBlock": "Alerte et bloc",
"xpack.cloudDefend.controlResponseActionBlock": "Bloc",
"xpack.cloudDefend.controlResponseActionBlockHelp": "Une action d'alerte doit être activée pour bloquer un événement.",
"xpack.cloudDefend.controlResponseActionLog": "Logarithmique",
"xpack.cloudDefend.controlResponseActions": "Actions",
"xpack.cloudDefend.controlResponses": "Réponses",
"xpack.cloudDefend.controlResponsesHelp": "Utilisez les réponses pour mapper un ou plusieurs sélecteurs à un ensemble d'actions. Les sélecteurs peuvent également servir à \"exclure\" des événements.",
"xpack.cloudDefend.controlSelectors": "Sélecteurs",
"xpack.cloudDefend.controlSelectorsHelp": "Créez des sélecteurs de fichier ou de processus pour trouver une correspondance sur des opérations et/ou des conditions d'intérêt.",
"xpack.cloudDefend.controlYamlHelp": "Configurez votre stratégie en créant des sélecteurs de \"fichier\" ou \"processus\" et des réponses ci-dessous.",
"xpack.cloudDefend.controlYamlView": "Vue YAML",
"xpack.cloudDefend.description": "Description",
"xpack.cloudDefend.enableControl": "Activer la politique",
"xpack.cloudDefend.enableControlHelp": "Active la politique de prévention des dérives, dalerte et de logging montrée ci-dessous.",
"xpack.cloudDefend.errorActionRequired": "Au moins une action est requise.",
"xpack.cloudDefend.errorBlockActionRequiresTargetFilePath": "Laction \"block\" requiert que targetFilePath soit inclus dans tous les sélecteurs \"match\" utilisant des opérations FIM ou au moins dans un sélecteur \"exclude\". Notez que les sélecteurs sans opération correspondront à toutes les opérations, y compris createFile, modifyFile ou deleteFile",
"xpack.cloudDefend.errorConditionRequired": "Au moins une condition par sélecteur est requise.",
"xpack.cloudDefend.errorDuplicateName": "Ce nom est déjà utilisé par un autre sélecteur.",
"xpack.cloudDefend.errorGenericEmptyValue": "Les valeurs \"{condition}\" ne peuvent pas être vides",
"xpack.cloudDefend.errorGenericRegexFailure": "Les valeurs \"{condition}\" doivent correspondre au modèle : /{pattern}/",
"xpack.cloudDefend.errorInvalidFullContainerImageName": "Les valeurs \"Nom complet de l'image de conteneur \" doivent être au format suivant : image_repo/image_name, par exemple \"docker.io/nginx\".",
"xpack.cloudDefend.errorInvalidName": "Les noms des sélecteurs doivent être alphanumériques et ne doivent pas inclure d'espaces.",
"xpack.cloudDefend.errorInvalidPodLabel": "Les valeurs détiquette du pod Kubernetes doivent avoir pour format : \"key:value\". Un caractère générique \"*\" peut être utilisé à la fin de la valeur, par exemple \"key:val*\". Pour trouver une correspondance sur une valeur d'étiquette vide, utilisez \"key:\".",
"xpack.cloudDefend.errorInvalidProcessExecutable": "Les valeurs \"Process executable\" doivent utiliser des chemins absolus. Un caractère générique \"*\" de fin peut être utilisé pour faire correspondre tous les fichiers du répertoire cible. Utilisez un double \"**\" pour faire correspondre tous les fichiers de manière récursive. Par ex. /usr/bin/**",
"xpack.cloudDefend.errorInvalidTargetFilePath": "Les valeurs de \"Target file path\" doivent être des chemins absolus. Un caractère générique \"*\" de fin peut être utilisé pour faire correspondre tous les fichiers du répertoire cible. Utilisez un double \"**\" pour faire correspondre tous les fichiers de manière récursive. Par ex. /etc/**",
"xpack.cloudDefend.errorMaxSelectorsResponsesExceeded": "Vous ne pouvez pas dépasser {max} sélecteurs + réponses pour un type donné (p.ex. : fichier, processus)",
"xpack.cloudDefend.errorMaxValueBytesExceeded": "Les valeurs \"{condition}\" ne peuvent excéder {maxValueBytes} bytes",
"xpack.cloudDefend.errorValueRequired": "Au moins une valeur est requise.",
"xpack.cloudDefend.fileResponse": "Réponse de fichier",
"xpack.cloudDefend.fileResponseIconTooltip": "Réponse de fichier. Seuls les sélecteurs de fichier peuvent être utilisés à des fins de mise en correspondance/exclusion.",
"xpack.cloudDefend.fileSelector": "Sélecteur de fichier",
"xpack.cloudDefend.fileSelectorIconTooltip": "Sélecteur de fichier. Trouve une correspondance uniquement sur des opérations de fichier.",
"xpack.cloudDefend.ignoreVolumeFilesHelp": "Ignorez les opérations sur les montages de fichier uniquement, par exemple fichiers montés, configMaps, secrets, etc...",
"xpack.cloudDefend.ignoreVolumeMountsHelp": "Ignorez les opérations sur tous les montages de volume.",
"xpack.cloudDefend.name": "Nom",
"xpack.cloudDefend.navigation.policiesNavItemLabel": "Container Workload Protection",
"xpack.cloudDefend.networkResponse": "Réseau (bientôt disponible)",
"xpack.cloudDefend.networkSelector": "Réseau (bientôt disponible)",
"xpack.cloudDefend.policies.policiesPageHeader": "Defend pour les conteneurs (D4C)",
"xpack.cloudDefend.policies.policiesPageHeader.addIntegrationButtonLabel": "Ajouter une intégration",
"xpack.cloudDefend.policies.policiesTable.agentPolicyColumnTitle": "Politique d'agent",
"xpack.cloudDefend.policies.policiesTable.createdAtColumnTitle": "Créé à",
"xpack.cloudDefend.policies.policiesTable.createdByColumnTitle": "Créé par",
"xpack.cloudDefend.policies.policiesTable.integrationNameColumnTitle": "Nom de l'intégration",
"xpack.cloudDefend.policies.policiesTable.numberOfAgentsColumnTitle": "Nombre d'agents",
"xpack.cloudDefend.policies.policyEmptyState.integrationsNotFoundForNameTitle": "pour \"{name}\"",
"xpack.cloudDefend.policies.policyEmptyState.integrationsNotFoundTitle": "Aucune politique n'a été trouvée",
"xpack.cloudDefend.policies.policyEmptyState.integrationsNotFoundWithFiltersTitle": "Nous n'avons trouvé aucune politique avec les filtres ci-dessus.",
"xpack.cloudDefend.policies.policySearchField.searchPlaceholder": "Rechercher le nom de l'intégration",
"xpack.cloudDefend.policies.totalIntegrationsCountMessage": "Affichage de {pageCount} sur {totalCount, plural, one {# intégration} other {# intégrations}}",
"xpack.cloudDefend.processResponse": "Réponse du processus",
"xpack.cloudDefend.processResponseIconTooltip": "Réponse du processus. Seuls les sélecteurs de processus peuvent être utilisés à des fins de mise en correspondance/exclusion.",
"xpack.cloudDefend.processSelector": "Sélecteur de processus",
"xpack.cloudDefend.processSelectorIconTooltip": "Sélecteur de processus. Trouve une correspondance uniquement sur des opérations de processus.",
"xpack.cloudDefend.subscriptionNotAllowed.promptDescription": "Pour utiliser ces fonctionnalités de sécurité du cloud, vous devez {link}.",
"xpack.cloudDefend.subscriptionNotAllowed.promptLinkText": "démarrer un essai ou mettre à niveau votre abonnement",
"xpack.cloudDefend.subscriptionNotAllowed.promptTitle": "Mettre à niveau pour bénéficier des fonctionnalités d'abonnement",
"xpack.cloudDefend.unusedSelector": "Non utilisé",
"xpack.cloudDefend.unusedSelectorHelp": "Ce sélecteur n'est utilisé par aucune réponse.",
"xpack.cloudDefend.warningFIMUsingSlashStarStarText": "Il est dangereux de bloquer des opérations FIM utilisant un targetFilePath de /**. Ceci peut rendre le système instable. Notez que les sélecteurs sans opération correspondront à toutes les opérations, y compris createFile, modifyFile ou deleteFile",
"xpack.cloudDefend.warningFIMUsingSlashStarStarTitle": "Avertissement : Bloquer les opérations FIM",
"xpack.cloudLinks.deploymentLinkLabel": "Gérer ce déploiement",
"xpack.cloudLinks.helpMenuLinks.connectionDetails": "Informations de connexion",
"xpack.cloudLinks.helpMenuLinks.documentation": "Documentation",
@ -33909,7 +33826,6 @@
"xpack.securitySolution.appLinks.alerts": "Alertes",
"xpack.securitySolution.appLinks.attackDiscovery": "Attack discovery",
"xpack.securitySolution.appLinks.blocklistDescription": "Excluez les applications non souhaitées de l'exécution sur vos hôtes.",
"xpack.securitySolution.appLinks.category.cloudSecurity": "Sécurité du cloud",
"xpack.securitySolution.appLinks.category.discover": "Discover",
"xpack.securitySolution.appLinks.category.endpoints": "Points de terminaison",
"xpack.securitySolution.appLinks.category.entityAnalytics": "Analyse des entités",

View file

@ -13853,89 +13853,6 @@
"xpack.cloudDataMigration.readInstructionsButtonLabel": "Elastic Cloudに移動",
"xpack.cloudDataMigration.slaBackedSupport.text": "SLAベースのサポートで、すべての質問を答えてもらいましょう",
"xpack.cloudDataMigration.upgrade.text": "新しいバージョンへのアップグレードがより簡単に",
"xpack.cloudDefend.addResponse": "対応を追加",
"xpack.cloudDefend.addSelector": "セレクターを追加",
"xpack.cloudDefend.addSelectorCondition": "条件を追加",
"xpack.cloudDefend.alertActionRequired": "「ブロック」アクションが使用される場合は、アラートアクションが必須です。",
"xpack.cloudDefend.cloudDefendPage.defaultNoDataConfig.pageTitle": "データが見つかりません",
"xpack.cloudDefend.cloudDefendPage.defaultNoDataConfig.solutionNameLabel": "Defend for containers",
"xpack.cloudDefend.cloudDefendPage.errorRenderer.errorDescription": "{error} {statusCode}: {body}",
"xpack.cloudDefend.cloudDefendPage.errorRenderer.errorTitle": "クラウド防御データを取得できませんでした",
"xpack.cloudDefend.cloudDefendPage.loadingDescription": "読み込み中...",
"xpack.cloudDefend.cloudDefendPage.packageNotInstalled.solutionNameLabel": "Defend for containersD4C",
"xpack.cloudDefend.cloudDefendPage.packageNotInstalledRenderer.addCloudDefendmIntegrationButtonTitle": "D4C統合を追加",
"xpack.cloudDefend.cloudDefendPage.packageNotInstalledRenderer.learnMoreTitle": "Defend for containersD4Cの詳細",
"xpack.cloudDefend.cloudDefendPage.packageNotInstalledRenderer.promptDescription": "開始するには、Defend for containersD4C統合を追加します。{learnMore}。",
"xpack.cloudDefend.cloudDefendPage.packageNotInstalledRenderer.promptTitle": "コンテナーのドリフトを検知し、悪意のある動作を発生時点でブロックします。",
"xpack.cloudDefend.conditions": "条件:",
"xpack.cloudDefend.controlDuplicate": "複製",
"xpack.cloudDefend.controlExclude": "除外",
"xpack.cloudDefend.controlExcludeSelectors": "セレクターを除外",
"xpack.cloudDefend.controlGeneralView": "一般ビュー",
"xpack.cloudDefend.controlMatchSelectors": "セレクターと一致",
"xpack.cloudDefend.controlRemove": "削除",
"xpack.cloudDefend.controlResponseActionAlert": "アラート",
"xpack.cloudDefend.controlResponseActionAlertAndBlock": "アラートを通知してブロック",
"xpack.cloudDefend.controlResponseActionBlock": "ブロック",
"xpack.cloudDefend.controlResponseActionBlockHelp": "イベントをブロックするには、アラートアクションを有効にする必要があります。",
"xpack.cloudDefend.controlResponseActionLog": "ログ",
"xpack.cloudDefend.controlResponseActions": "アクション",
"xpack.cloudDefend.controlResponses": "対応",
"xpack.cloudDefend.controlResponsesHelp": "対応を使用して、1つ以上のセレクターをアクションのセットにマッピングします。セレクターを使用して、イベントを「除外」することもできます。",
"xpack.cloudDefend.controlSelectors": "セレクター",
"xpack.cloudDefend.controlSelectorsHelp": "ファイルまたはプロセスセレクターを作成し、関心がある処理または条件で照合します。",
"xpack.cloudDefend.controlYamlHelp": "「ファイル」または「プロセス」セレクターと次の対応を作成して、ポリシーを構成します。",
"xpack.cloudDefend.controlYamlView": "YAMLビュー",
"xpack.cloudDefend.description": "説明",
"xpack.cloudDefend.enableControl": "ポリシーを有効にする",
"xpack.cloudDefend.enableControlHelp": "以下に示すドリフト防止、アラート、ログポリシーを有効にします。",
"xpack.cloudDefend.errorActionRequired": "1つ以上のアクションが必要です。",
"xpack.cloudDefend.errorBlockActionRequiresTargetFilePath": "「ブロック」アクションでは、targetFilePathが、FIM操作を使用するすべての「一致」セレクターに含まれるか、少なくとも1つの「除外」セレクターに含まれる必要があります。操作を含まないセレクターは、createFile、modifyFile、deleteFileを含むすべての操作と一致します。",
"xpack.cloudDefend.errorConditionRequired": "セレクターにつき1つ以上の条件が必要です。",
"xpack.cloudDefend.errorDuplicateName": "この名前はすでに別のセレクターで使用されています。",
"xpack.cloudDefend.errorGenericEmptyValue": "\"{condition}\"値を空にすることはできません",
"xpack.cloudDefend.errorGenericRegexFailure": "\"{condition}\"値はパターン/{pattern}/と一致する必要があります",
"xpack.cloudDefend.errorInvalidFullContainerImageName": "完全なコンテナーイメージ名値はimage_repo/image_nameの形式でなければなりません。例docker.io/nginx",
"xpack.cloudDefend.errorInvalidName": "セレクター名は英数字でなければならず、スペースは使用できません。",
"xpack.cloudDefend.errorInvalidPodLabel": "Kubernetesポッドラベル値は「キー:値」の形式でなければなりません。ワイルドカード「*」は値の末尾で使用できます(例:キー:値*。空のラベル値で照合するには、「key:」を使用します。",
"xpack.cloudDefend.errorInvalidProcessExecutable": "「プロセス実行ファイル」値は絶対パスを使用する必要があります。末尾に*ワイルドカードを付けると、ターゲットディレクトリのすべてのファイルと一致します。二重の**を使用すると、すべてのファイルと再帰的に一致します。例:/usr/bin/**",
"xpack.cloudDefend.errorInvalidTargetFilePath": "「ターゲットファイルパス」の値は絶対パスでなければなりません。末尾に*ワイルドカードを付けると、ターゲットディレクトリのすべてのファイルと一致します。二重の**を使用すると、すべてのファイルと再帰的に一致します。例:/etc/**",
"xpack.cloudDefend.errorMaxSelectorsResponsesExceeded": "ファイルやプロセスなどの特定のタイプのセレクターと応答は{max}個以下でなければなりません",
"xpack.cloudDefend.errorMaxValueBytesExceeded": "\"{condition}\"値は{maxValueBytes}バイト以下でなければなりません",
"xpack.cloudDefend.errorValueRequired": "1つ以上の値が必要です。",
"xpack.cloudDefend.fileResponse": "ファイル応答",
"xpack.cloudDefend.fileResponseIconTooltip": "ファイル応答。照合/除外ではファイルセレクターのみを使用できます。",
"xpack.cloudDefend.fileSelector": "ファイルセレクター",
"xpack.cloudDefend.fileSelectorIconTooltip": "ファイルセレクター。ファイル操作時にのみ照合します。",
"xpack.cloudDefend.ignoreVolumeFilesHelp": "マウントされたファイル、configMaps、シークレットなどのファイルマウントのみの処理は無視されます。",
"xpack.cloudDefend.ignoreVolumeMountsHelp": "すべてのボリュームマウントでの処理は無視されます。",
"xpack.cloudDefend.name": "名前",
"xpack.cloudDefend.navigation.policiesNavItemLabel": "コンテナーワークロード保護",
"xpack.cloudDefend.networkResponse": "ネットワーク(まもなくリリース予定)",
"xpack.cloudDefend.networkSelector": "ネットワーク(まもなくリリース予定)",
"xpack.cloudDefend.policies.policiesPageHeader": "Defend for containersD4C",
"xpack.cloudDefend.policies.policiesPageHeader.addIntegrationButtonLabel": "統合の追加",
"xpack.cloudDefend.policies.policiesTable.agentPolicyColumnTitle": "エージェントポリシー",
"xpack.cloudDefend.policies.policiesTable.createdAtColumnTitle": "作成日時:",
"xpack.cloudDefend.policies.policiesTable.createdByColumnTitle": "作成者",
"xpack.cloudDefend.policies.policiesTable.integrationNameColumnTitle": "統合名",
"xpack.cloudDefend.policies.policiesTable.numberOfAgentsColumnTitle": "エージェント数",
"xpack.cloudDefend.policies.policyEmptyState.integrationsNotFoundForNameTitle": "\"{name}\"",
"xpack.cloudDefend.policies.policyEmptyState.integrationsNotFoundTitle": "ポリシーが見つかりません",
"xpack.cloudDefend.policies.policyEmptyState.integrationsNotFoundWithFiltersTitle": "上記のフィルターでポリシーが見つかりませんでした。",
"xpack.cloudDefend.policies.policySearchField.searchPlaceholder": "統合名を検索",
"xpack.cloudDefend.policies.totalIntegrationsCountMessage": "{pageCount}/{totalCount, plural, other {#個の統合}}を表示しています",
"xpack.cloudDefend.processResponse": "プロセス応答",
"xpack.cloudDefend.processResponseIconTooltip": "プロセス応答。照合/除外ではプロセスセレクターのみを使用できます。",
"xpack.cloudDefend.processSelector": "プロセスセレクター",
"xpack.cloudDefend.processSelectorIconTooltip": "プロセスセレクター。プロセス処理時にのみ照合します。",
"xpack.cloudDefend.subscriptionNotAllowed.promptDescription": "これらのクラウドセキュリティ機能を使用するには、{link}する必要があります。",
"xpack.cloudDefend.subscriptionNotAllowed.promptLinkText": "試用版を開始するか、サブスクリプションをアップグレード",
"xpack.cloudDefend.subscriptionNotAllowed.promptTitle": "サブスクリプション機能のアップグレード",
"xpack.cloudDefend.unusedSelector": "使用されていません",
"xpack.cloudDefend.unusedSelectorHelp": "このセレクターはどの応答でも使用されません。",
"xpack.cloudDefend.warningFIMUsingSlashStarStarText": "/**のtargetFilePathを使用して、FIM操作をブロックするのは危険です。これはシステムの不安定化につながる可能性があります。操作を含まないセレクターは、createFile、modifyFile、deleteFileを含むすべての操作と一致します。",
"xpack.cloudDefend.warningFIMUsingSlashStarStarTitle": "警告ブロックFIM操作",
"xpack.cloudLinks.deploymentLinkLabel": "このデプロイの管理",
"xpack.cloudLinks.helpMenuLinks.connectionDetails": "接続詳細情報",
"xpack.cloudLinks.helpMenuLinks.documentation": "ドキュメント",
@ -33771,7 +33688,6 @@
"xpack.securitySolution.appLinks.alerts": "アラート",
"xpack.securitySolution.appLinks.attackDiscovery": "Attack discovery",
"xpack.securitySolution.appLinks.blocklistDescription": "不要なアプリケーションがホストで実行されないようにします。",
"xpack.securitySolution.appLinks.category.cloudSecurity": "クラウドセキュリティ",
"xpack.securitySolution.appLinks.category.discover": "Discover",
"xpack.securitySolution.appLinks.category.endpoints": "エンドポイント",
"xpack.securitySolution.appLinks.category.entityAnalytics": "エンティティ分析",

View file

@ -13619,85 +13619,6 @@
"xpack.cloudDataMigration.readInstructionsButtonLabel": "移至 Elastic Cloud",
"xpack.cloudDataMigration.slaBackedSupport.text": "通过 SLA 支持解答您的所有问题",
"xpack.cloudDataMigration.upgrade.text": "更轻松地升级到较新版本",
"xpack.cloudDefend.addResponse": "添加响应",
"xpack.cloudDefend.addSelector": "添加选择器",
"xpack.cloudDefend.addSelectorCondition": "添加条件",
"xpack.cloudDefend.alertActionRequired": "使用'阻止'操作时需要告警操作。",
"xpack.cloudDefend.cloudDefendPage.defaultNoDataConfig.pageTitle": "未找到任何数据",
"xpack.cloudDefend.cloudDefendPage.defaultNoDataConfig.solutionNameLabel": "Defend for containers",
"xpack.cloudDefend.cloudDefendPage.errorRenderer.errorDescription": "{error} {statusCode}{body}",
"xpack.cloudDefend.cloudDefendPage.errorRenderer.errorTitle": "无法提取您的云防御数据",
"xpack.cloudDefend.cloudDefendPage.loadingDescription": "正在加载……",
"xpack.cloudDefend.cloudDefendPage.packageNotInstalled.solutionNameLabel": "Defend for containers (D4C)",
"xpack.cloudDefend.cloudDefendPage.packageNotInstalledRenderer.addCloudDefendmIntegrationButtonTitle": "添加 D4C 集成",
"xpack.cloudDefend.cloudDefendPage.packageNotInstalledRenderer.learnMoreTitle": "详细了解 Defend for containers (D4C)",
"xpack.cloudDefend.cloudDefendPage.packageNotInstalledRenderer.promptDescription": "添加 Defend for containers (D4C) 集成以开始。{learnMore}。",
"xpack.cloudDefend.cloudDefendPage.packageNotInstalledRenderer.promptTitle": "检测容器漂移并从源头阻止恶意行为!",
"xpack.cloudDefend.conditions": "条件:",
"xpack.cloudDefend.controlDuplicate": "复制",
"xpack.cloudDefend.controlExclude": "排除",
"xpack.cloudDefend.controlExcludeSelectors": "排除选择器",
"xpack.cloudDefend.controlGeneralView": "常规视图",
"xpack.cloudDefend.controlMatchSelectors": "匹配选择器",
"xpack.cloudDefend.controlRemove": "移除",
"xpack.cloudDefend.controlResponseActionAlert": "告警",
"xpack.cloudDefend.controlResponseActionAlertAndBlock": "告警并阻止",
"xpack.cloudDefend.controlResponseActionBlock": "阻止",
"xpack.cloudDefend.controlResponseActionBlockHelp": "必须启用告警操作才能阻止事件。",
"xpack.cloudDefend.controlResponseActionLog": "对数",
"xpack.cloudDefend.controlResponseActions": "操作",
"xpack.cloudDefend.controlResponses": "响应",
"xpack.cloudDefend.controlResponsesHelp": "使用响应可将一个或多个选择器映射到一组操作。还可以使用选择器来'排除'事件。",
"xpack.cloudDefend.controlSelectors": "选择器",
"xpack.cloudDefend.controlSelectorsHelp": "创建文件或进程选择器以匹配相关操作和/或条件。",
"xpack.cloudDefend.controlYamlHelp": "通过在下面创建'文件'或'进程'选择器和响应来配置您的策略。",
"xpack.cloudDefend.controlYamlView": "YAML 视图",
"xpack.cloudDefend.description": "描述",
"xpack.cloudDefend.enableControl": "启用策略",
"xpack.cloudDefend.enableControlHelp": "启用下面显示的偏移预防、告警和日志记录策略。",
"xpack.cloudDefend.errorActionRequired": "至少需要一项操作。",
"xpack.cloudDefend.errorBlockActionRequiresTargetFilePath": "'阻止'操作需要 targetFilePath 包含在所有使用 FIM 操作的'匹配'选择器中,或至少位于一个'排除'选择器中。请注意,不含操作的选择器会匹配所有操作,包括 createFile、modifyFile 或 deleteFile",
"xpack.cloudDefend.errorConditionRequired": "每个选择器至少需要一个条件。",
"xpack.cloudDefend.errorDuplicateName": "此名称已由其他选择器使用。",
"xpack.cloudDefend.errorInvalidFullContainerImageName": "'容器映像全名'值必须为以下格式image_repo/image_name例如'docker.io/nginx'",
"xpack.cloudDefend.errorInvalidName": "选择器名称必须为字母数字,并且不包含空格。",
"xpack.cloudDefend.errorInvalidPodLabel": "Kubernetes pod 标签值必须采用以下格式:'key:value'。可以在值的末尾使用通配符'*',例如'key:val*'。要匹配空白标签值,请使用'key:'。",
"xpack.cloudDefend.errorInvalidProcessExecutable": "'进程可执行文件'值必须使用绝对路径。尾随的 * 通配符可用于匹配目标目录中的所有文件。使用双 ** 可以递归方式匹配所有文件。例如,/usr/bin/**",
"xpack.cloudDefend.errorInvalidTargetFilePath": "'目标文件路径'值必须使用绝对路径。尾随的 * 通配符可用于匹配目标目录中的所有文件。使用双 ** 可以递归方式匹配所有文件。例如,/etc/**",
"xpack.cloudDefend.errorMaxSelectorsResponsesExceeded": "给定类型(如文件、进程)不能超过 {max} 个选择器 + 响应",
"xpack.cloudDefend.errorValueRequired": "至少需要一个值。",
"xpack.cloudDefend.fileResponse": "文件响应",
"xpack.cloudDefend.fileResponseIconTooltip": "文件响应。仅文件选择器可用于进行匹配/排除。",
"xpack.cloudDefend.fileSelector": "文件选择器",
"xpack.cloudDefend.fileSelectorIconTooltip": "文件选择器。仅匹配文件操作。",
"xpack.cloudDefend.ignoreVolumeFilesHelp": "仅忽略文件安装相关操作。例如已安装文件、configMaps、机密等……",
"xpack.cloudDefend.ignoreVolumeMountsHelp": "忽略所有卷安装相关操作。",
"xpack.cloudDefend.name": "名称",
"xpack.cloudDefend.navigation.policiesNavItemLabel": "容器工作负载保护",
"xpack.cloudDefend.networkResponse": "网络(即将推出)",
"xpack.cloudDefend.networkSelector": "网络(即将推出)",
"xpack.cloudDefend.policies.policiesPageHeader": "Defend for containers (D4C)",
"xpack.cloudDefend.policies.policiesPageHeader.addIntegrationButtonLabel": "添加集成",
"xpack.cloudDefend.policies.policiesTable.agentPolicyColumnTitle": "代理策略",
"xpack.cloudDefend.policies.policiesTable.createdAtColumnTitle": "创建于",
"xpack.cloudDefend.policies.policiesTable.createdByColumnTitle": "创建者",
"xpack.cloudDefend.policies.policiesTable.integrationNameColumnTitle": "集成名称",
"xpack.cloudDefend.policies.policiesTable.numberOfAgentsColumnTitle": "代理数目",
"xpack.cloudDefend.policies.policyEmptyState.integrationsNotFoundTitle": "未找到策略",
"xpack.cloudDefend.policies.policyEmptyState.integrationsNotFoundWithFiltersTitle": "使用上述筛选,我们无法找到任何策略。",
"xpack.cloudDefend.policies.policySearchField.searchPlaceholder": "搜索集成名称",
"xpack.cloudDefend.policies.totalIntegrationsCountMessage": "正在显示 {pageCount}/{totalCount, plural, other {# 个集成}}",
"xpack.cloudDefend.processResponse": "进程响应",
"xpack.cloudDefend.processResponseIconTooltip": "进程响应。仅进程选择器可用于进行匹配/排除。",
"xpack.cloudDefend.processSelector": "进程选择器",
"xpack.cloudDefend.processSelectorIconTooltip": "进程选择器。仅匹配进程操作。",
"xpack.cloudDefend.subscriptionNotAllowed.promptDescription": "要使用这些云安全功能,您必须 {link}。",
"xpack.cloudDefend.subscriptionNotAllowed.promptLinkText": "开始试用或升级您的订阅",
"xpack.cloudDefend.subscriptionNotAllowed.promptTitle": "升级以使用订阅功能",
"xpack.cloudDefend.unusedSelector": "未在使用中",
"xpack.cloudDefend.unusedSelectorHelp": "没有任何响应使用此选择器。",
"xpack.cloudDefend.warningFIMUsingSlashStarStarText": "使用 /** 类型的 targetFilePath 阻止 FIM 操作较为危险。这可能导致系统不稳定。请注意,不含操作的选择器会匹配所有操作,包括 createFile、modifyFile 或 deleteFile",
"xpack.cloudDefend.warningFIMUsingSlashStarStarTitle": "警告:正在阻止 FIM 操作",
"xpack.cloudLinks.deploymentLinkLabel": "管理此部署",
"xpack.cloudLinks.helpMenuLinks.connectionDetails": "连接详情",
"xpack.cloudLinks.helpMenuLinks.documentation": "文档",
@ -33247,7 +33168,6 @@
"xpack.securitySolution.appLinks.alerts": "告警",
"xpack.securitySolution.appLinks.attackDiscovery": "Attack Discovery",
"xpack.securitySolution.appLinks.blocklistDescription": "阻止不需要的应用程序在您的主机上运行。",
"xpack.securitySolution.appLinks.category.cloudSecurity": "云安全",
"xpack.securitySolution.appLinks.category.discover": "Discover",
"xpack.securitySolution.appLinks.category.endpoints": "终端",
"xpack.securitySolution.appLinks.category.entityAnalytics": "实体分析",

View file

@ -28,7 +28,6 @@ export const AGENT_POLICY_DEFAULT_MONITORING_DATASETS = [
'elastic_agent.auditbeat',
'elastic_agent.heartbeat',
'elastic_agent.cloudbeat',
'elastic_agent.cloud_defend',
'elastic_agent.pf_host_agent',
'elastic_agent.pf_elastic_collector',
'elastic_agent.pf_elastic_symbolizer',

View file

@ -25,7 +25,6 @@ export const FLEET_CLOUD_SECURITY_POSTURE_PACKAGE = 'cloud_security_posture';
export const FLEET_CLOUD_SECURITY_POSTURE_KSPM_POLICY_TEMPLATE = 'kspm';
export const FLEET_CLOUD_SECURITY_POSTURE_CSPM_POLICY_TEMPLATE = 'cspm';
export const FLEET_CLOUD_SECURITY_POSTURE_CNVM_POLICY_TEMPLATE = 'vuln_mgmt';
export const FLEET_CLOUD_DEFEND_PACKAGE = 'cloud_defend';
export const FLEET_CLOUD_BEAT_PACKAGE = 'cloudbeat';
export const FLEET_CONNECTORS_PACKAGE = 'elastic_connectors';
@ -35,8 +34,6 @@ export const GLOBAL_DATA_TAG_EXCLUDED_INPUTS = new Set<string>([
`pf-elastic-symbolizer`,
`pf-elastic-collector`,
`fleet-server`,
FLEET_CLOUD_DEFEND_PACKAGE,
`${FLEET_CLOUD_DEFEND_PACKAGE}/control`,
FLEET_CLOUD_BEAT_PACKAGE,
`${FLEET_CLOUD_BEAT_PACKAGE}/cis_k8s`,
`${FLEET_CLOUD_BEAT_PACKAGE}/cis_eks`,

View file

@ -20,7 +20,6 @@ export {
FLEET_CLOUD_SECURITY_POSTURE_KSPM_POLICY_TEMPLATE,
FLEET_CLOUD_SECURITY_POSTURE_CSPM_POLICY_TEMPLATE,
FLEET_CLOUD_SECURITY_POSTURE_CNVM_POLICY_TEMPLATE,
FLEET_CLOUD_DEFEND_PACKAGE,
FLEET_ENDPOINT_PACKAGE,
// Saved object type
AGENT_POLICY_SAVED_OBJECT_TYPE,

View file

@ -12,7 +12,6 @@ import { PLUGIN_ID, INTEGRATIONS_PLUGIN_ID, pagePathGetters } from '../../../../
const EXCLUDED_PACKAGES = [
'apm',
'cloud_security_posture',
'cloud_defend',
'dga',
'fleet_server',
'osquery_manager',

View file

@ -18,11 +18,7 @@ import {
useGetPackageInfoByKeyQuery,
useStartServices,
} from '../../hooks';
import {
FLEET_KUBERNETES_PACKAGE,
FLEET_CLOUD_SECURITY_POSTURE_PACKAGE,
FLEET_CLOUD_DEFEND_PACKAGE,
} from '../../../common';
import { FLEET_KUBERNETES_PACKAGE, FLEET_CLOUD_SECURITY_POSTURE_PACKAGE } from '../../../common';
import {
getTemplateUrlFromAgentPolicy,
@ -47,7 +43,7 @@ import type {
} from './types';
// Packages that requires custom elastic-agent manifest
const K8S_PACKAGES = new Set([FLEET_KUBERNETES_PACKAGE, FLEET_CLOUD_DEFEND_PACKAGE]);
const K8S_PACKAGES = new Set([FLEET_KUBERNETES_PACKAGE]);
export function useAgentPolicyWithPackagePolicies(policyId?: string) {
const [agentPolicyWithPackagePolicies, setAgentPolicy] = useState<AgentPolicy | null>(null);

View file

@ -126,7 +126,6 @@ Object {
"logs-elastic_agent.auditbeat-testnamespace123",
"logs-elastic_agent.heartbeat-testnamespace123",
"logs-elastic_agent.cloudbeat-testnamespace123",
"logs-elastic_agent.cloud_defend-testnamespace123",
"logs-elastic_agent.pf_host_agent-testnamespace123",
"logs-elastic_agent.pf_elastic_collector-testnamespace123",
"logs-elastic_agent.pf_elastic_symbolizer-testnamespace123",
@ -160,7 +159,6 @@ Object {
"logs-elastic_agent.auditbeat-testnamespace123",
"logs-elastic_agent.heartbeat-testnamespace123",
"logs-elastic_agent.cloudbeat-testnamespace123",
"logs-elastic_agent.cloud_defend-testnamespace123",
"logs-elastic_agent.pf_host_agent-testnamespace123",
"logs-elastic_agent.pf_elastic_collector-testnamespace123",
"logs-elastic_agent.pf_elastic_symbolizer-testnamespace123",
@ -177,7 +175,6 @@ Object {
"metrics-elastic_agent.auditbeat-testnamespace123",
"metrics-elastic_agent.heartbeat-testnamespace123",
"metrics-elastic_agent.cloudbeat-testnamespace123",
"metrics-elastic_agent.cloud_defend-testnamespace123",
"metrics-elastic_agent.pf_host_agent-testnamespace123",
"metrics-elastic_agent.pf_elastic_collector-testnamespace123",
"metrics-elastic_agent.pf_elastic_symbolizer-testnamespace123",
@ -194,7 +191,6 @@ Object {
"traces-elastic_agent.auditbeat-testnamespace123",
"traces-elastic_agent.heartbeat-testnamespace123",
"traces-elastic_agent.cloudbeat-testnamespace123",
"traces-elastic_agent.cloud_defend-testnamespace123",
"traces-elastic_agent.pf_host_agent-testnamespace123",
"traces-elastic_agent.pf_elastic_collector-testnamespace123",
"traces-elastic_agent.pf_elastic_symbolizer-testnamespace123",
@ -228,7 +224,6 @@ Object {
"metrics-elastic_agent.auditbeat-testnamespace123",
"metrics-elastic_agent.heartbeat-testnamespace123",
"metrics-elastic_agent.cloudbeat-testnamespace123",
"metrics-elastic_agent.cloud_defend-testnamespace123",
"metrics-elastic_agent.pf_host_agent-testnamespace123",
"metrics-elastic_agent.pf_elastic_collector-testnamespace123",
"metrics-elastic_agent.pf_elastic_symbolizer-testnamespace123",
@ -262,7 +257,6 @@ Object {
"traces-elastic_agent.auditbeat-testnamespace123",
"traces-elastic_agent.heartbeat-testnamespace123",
"traces-elastic_agent.cloudbeat-testnamespace123",
"traces-elastic_agent.cloud_defend-testnamespace123",
"traces-elastic_agent.pf_host_agent-testnamespace123",
"traces-elastic_agent.pf_elastic_collector-testnamespace123",
"traces-elastic_agent.pf_elastic_symbolizer-testnamespace123",

View file

@ -34,9 +34,6 @@ export const SIEM_MIGRATIONS_FEATURE_ID = 'securitySolutionSiemMigrations' as co
// Same as the plugin id defined by Cloud Security Posture
export const CLOUD_POSTURE_APP_ID = 'csp' as const;
// Same as the plugin id defined by Defend for containers (cloud_defend)
export const CLOUD_DEFEND_APP_ID = 'cloudDefend' as const;
/**
* Id for the notifications alerting type
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function

View file

@ -24,7 +24,6 @@ import {
SERVER_APP_ID,
LEGACY_NOTIFICATIONS_ID,
CLOUD_POSTURE_APP_ID,
CLOUD_DEFEND_APP_ID,
SECURITY_FEATURE_ID_V2,
TIMELINE_FEATURE_ID,
NOTES_FEATURE_ID,
@ -75,7 +74,7 @@ export const getSecurityBaseKibanaFeature = ({
order: 1100,
category: DEFAULT_APP_CATEGORIES.security,
scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security],
app: [APP_ID, CLOUD_POSTURE_APP_ID, CLOUD_DEFEND_APP_ID, 'kibana'],
app: [APP_ID, CLOUD_POSTURE_APP_ID, 'kibana'],
catalogue: [APP_ID],
management: {
insightsAndAlerting: ['triggersActions'],
@ -102,7 +101,7 @@ export const getSecurityBaseKibanaFeature = ({
{ feature: SECURITY_FEATURE_ID_V2, privileges: ['minimal_all'] },
],
},
app: [APP_ID, CLOUD_POSTURE_APP_ID, CLOUD_DEFEND_APP_ID, 'kibana'],
app: [APP_ID, CLOUD_POSTURE_APP_ID, 'kibana'],
catalogue: [APP_ID],
api: [
APP_ID,
@ -150,7 +149,7 @@ export const getSecurityBaseKibanaFeature = ({
{ feature: SECURITY_FEATURE_ID_V2, privileges: ['minimal_read'] },
],
},
app: [APP_ID, CLOUD_POSTURE_APP_ID, CLOUD_DEFEND_APP_ID, 'kibana'],
app: [APP_ID, CLOUD_POSTURE_APP_ID, 'kibana'],
catalogue: [APP_ID],
api: [
APP_ID,

View file

@ -24,7 +24,6 @@ import {
SECURITY_FEATURE_ID_V2,
LEGACY_NOTIFICATIONS_ID,
CLOUD_POSTURE_APP_ID,
CLOUD_DEFEND_APP_ID,
SERVER_APP_ID,
} from '../../constants';
import type { SecurityFeatureParams } from '../types';
@ -60,7 +59,7 @@ export const getSecurityV2BaseKibanaFeature = ({
order: 1100,
category: DEFAULT_APP_CATEGORIES.security,
scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security],
app: [APP_ID, CLOUD_POSTURE_APP_ID, CLOUD_DEFEND_APP_ID, 'kibana'],
app: [APP_ID, CLOUD_POSTURE_APP_ID, 'kibana'],
catalogue: [APP_ID],
management: {
insightsAndAlerting: ['triggersActions'],
@ -75,7 +74,7 @@ export const getSecurityV2BaseKibanaFeature = ({
),
privileges: {
all: {
app: [APP_ID, CLOUD_POSTURE_APP_ID, CLOUD_DEFEND_APP_ID, 'kibana'],
app: [APP_ID, CLOUD_POSTURE_APP_ID, 'kibana'],
catalogue: [APP_ID],
api: [
APP_ID,
@ -103,7 +102,7 @@ export const getSecurityV2BaseKibanaFeature = ({
ui: ['show', 'crud'],
},
read: {
app: [APP_ID, CLOUD_POSTURE_APP_ID, CLOUD_DEFEND_APP_ID, 'kibana'],
app: [APP_ID, CLOUD_POSTURE_APP_ID, 'kibana'],
catalogue: [APP_ID],
api: [
APP_ID,

View file

@ -1,7 +0,0 @@
{
"prefix": "cloudDefend",
"paths": {
"cloudDefend": "."
},
"translations": []
}

View file

@ -1,49 +0,0 @@
# Cloud Defend (for containers)
This plugin currently only exists to provide custom fleet policy UX for a set of new BPF LSM features. The first feature being container "drift prevention".
Drift prevention is a way to block when executables are created or modified. Our agent service detects these events, and applies a set of selectors and responses configured to either block, alert or both.
## Example configuration
```
selectors:
# default selector (user can modify or remove if they want)
- name: default
operation: [createExecutable, modifyExecutable, execMemFd]
# example custom selector
- name: nginxOnly
containerImageName:
- nginx
# example selector used for exclude
- name: excludeCustomNginxBuild
containerImageTag:
- staging
# responses are evaluated from top to bottom
# only the first response with a match will run its actions
responses:
- match: [nginxOnly]
exclude: [excludeCustomNginxBuild]
actions: [alert, block]
# default response
# delete this if no default response needed
- match: [default]
actions: [alert]
```
---
## Development
## pre commit checks
```
node scripts/type_check.js --project x-pack/solutions/security/plugins/cloud_defend/tsconfig.json
node scripts/eslint.js x-pack/solutions/security/plugins/cloud_defend
yarn test:jest x-pack/solutions/security/plugins/cloud_defend
```
See the [kibana contributing guide](https://github.com/elastic/kibana/blob/main/CONTRIBUTING.md) for instructions setting up your development environment.

View file

@ -1,32 +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 { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common';
export const PLUGIN_ID = 'cloudDefend';
export const PLUGIN_NAME = 'Cloud Defend';
export const INTEGRATION_PACKAGE_NAME = 'cloud_defend';
export const INPUT_CONTROL = 'cloud_defend/control';
export const LOGS_CLOUD_DEFEND_PATTERN = 'logs-cloud_defend.*';
export const ALERTS_DATASET = 'cloud_defend.alerts';
export const ALERTS_INDEX_PATTERN = 'logs-cloud_defend.alerts*';
export const ALERTS_INDEX_PATTERN_DEFAULT_NS = 'logs-cloud_defend.alerts-default';
export const FILE_DATASET = 'cloud_defend.file';
export const FILE_INDEX_PATTERN = 'logs-cloud_defend.file*';
export const FILE_INDEX_PATTERN_DEFAULT_NS = 'logs-cloud_defend.file-default';
export const PROCESS_DATASET = 'cloud_defend.process';
export const PROCESS_INDEX_PATTERN = 'logs-cloud_defend.process*';
export const PROCESS_INDEX_PATTERN_DEFAULT_NS = 'logs-cloud_defend.process-default';
export const CURRENT_API_VERSION = '1';
export const POLICIES_ROUTE_PATH = '/internal/cloud_defend/policies';
export const STATUS_ROUTE_PATH = '/internal/cloud_defend/status';
export const CLOUD_DEFEND_FLEET_PACKAGE_KUERY = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${INTEGRATION_PACKAGE_NAME}`;
export const DEFAULT_POLICIES_PER_PAGE = 20;
export const POLICIES_PACKAGE_POLICY_PREFIX = 'package_policy.';

View file

@ -1,26 +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 type {
IndexDetails,
IndexStatus,
CloudDefendSetupStatus,
CloudDefendStatusCode,
AgentPolicyStatus,
CloudDefendPolicy,
PoliciesQueryParams,
SelectorType,
SelectorCondition,
ResponseAction,
Selector,
Response,
} from './latest';
export { policiesQueryParamsSchema } from './latest';
import * as v1 from './v1';
import * as schemaV1 from './schemas/v1';
export { v1, schemaV1 };

View file

@ -1,9 +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 './v1';
export { policiesQueryParamsSchema } from './schemas/v1';
export type { PoliciesQueryParams } from './schemas/v1';

View file

@ -1,61 +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, schema } from '@kbn/config-schema';
import { DEFAULT_POLICIES_PER_PAGE } from '../constants';
export const policiesQueryParamsSchema = schema.object({
/**
* The page of objects to return
*/
page: schema.number({ defaultValue: 1, min: 1 }),
/**
* The number of objects to include in each page
*/
per_page: schema.number({ defaultValue: DEFAULT_POLICIES_PER_PAGE, min: 0 }),
/**
* Once of PackagePolicy fields for sorting the found objects.
* Sortable fields:
* - package_policy.id
* - package_policy.name
* - package_policy.policy_id
* - package_policy.namespace
* - package_policy.updated_at
* - package_policy.updated_by
* - package_policy.created_at
* - package_policy.created_by,
* - package_policy.package.name
* - package_policy.package.title
* - package_policy.package.version
*/
sort_field: schema.oneOf(
[
schema.literal('package_policy.id'),
schema.literal('package_policy.name'),
schema.literal('package_policy.policy_id'),
schema.literal('package_policy.namespace'),
schema.literal('package_policy.updated_at'),
schema.literal('package_policy.updated_by'),
schema.literal('package_policy.created_at'),
schema.literal('package_policy.created_by'),
schema.literal('package_policy.package.name'),
schema.literal('package_policy.package.title'),
],
{ defaultValue: 'package_policy.name' }
),
/**
* The order to sort by
*/
sort_order: schema.oneOf([schema.literal('asc'), schema.literal('desc')], {
defaultValue: 'asc',
}),
/**
* Policy filter
*/
policy_name: schema.maybe(schema.string()),
});
export type PoliciesQueryParams = TypeOf<typeof policiesQueryParamsSchema>;

View file

@ -1,35 +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 { getSelectorsAndResponsesFromYaml, getYamlFromSelectorsAndResponses } from './helpers';
import { MOCK_YAML_CONFIGURATION, MOCK_YAML_INVALID_CONFIGURATION } from '../../public/test/mocks';
describe('getSelectorsAndResponsesFromYaml', () => {
it('converts yaml into arrays of selectors and responses', () => {
const { selectors, responses } = getSelectorsAndResponsesFromYaml(MOCK_YAML_CONFIGURATION);
expect(selectors).toHaveLength(3);
expect(responses).toHaveLength(2);
});
it('returns empty arrays if bad yaml', () => {
const { selectors, responses } = getSelectorsAndResponsesFromYaml(
MOCK_YAML_INVALID_CONFIGURATION
);
expect(selectors).toHaveLength(0);
expect(responses).toHaveLength(0);
});
});
describe('getYamlFromSelectorsAndResponses', () => {
it('converts arrays of selectors and responses into yaml', () => {
const { selectors, responses } = getSelectorsAndResponsesFromYaml(MOCK_YAML_CONFIGURATION);
const yaml = getYamlFromSelectorsAndResponses(selectors, responses);
expect(yaml).toEqual(MOCK_YAML_CONFIGURATION);
});
});

View file

@ -1,111 +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 yaml from 'js-yaml';
import { NewPackagePolicy } from '@kbn/fleet-plugin/common';
import { Truthy } from 'lodash';
import { INTEGRATION_PACKAGE_NAME } from '../constants';
import { Selector, Response } from '..';
/**
* @example
* declare const foo: Array<string | undefined | null>
* foo.filter(isNonNullable) // foo is Array<string>
*/
export const isNonNullable = <T extends unknown>(v: T): v is NonNullable<T> =>
v !== null && v !== undefined;
export const truthy = <T>(value: T): value is Truthy<T> => !!value;
export const extractErrorMessage = (e: unknown, defaultMessage = 'Unknown Error'): string => {
if (e instanceof Error) return e.message;
if (typeof e === 'string') return e;
return defaultMessage; // TODO: i18n
};
export function assert(condition: any, msg?: string): asserts condition {
if (!condition) {
throw new Error(msg);
}
}
export const isCloudDefendPackage = (packageName?: string) =>
packageName === INTEGRATION_PACKAGE_NAME;
export function getInputFromPolicy(policy: NewPackagePolicy, inputId: string) {
return policy.inputs.find((input) => input.type === inputId);
}
export function getSelectorsAndResponsesFromYaml(configuration: string): {
selectors: Selector[];
responses: Response[];
} {
let selectors: Selector[] = [];
let responses: Response[] = [];
try {
const result = yaml.load(configuration);
if (result) {
// iterate selector/response types
Object.keys(result).forEach((selectorType) => {
const obj = result[selectorType];
if (obj.selectors) {
selectors = selectors.concat(
obj.selectors.map((selector: any) => ({ ...selector, type: selectorType }))
);
}
if (obj.responses) {
responses = responses.concat(
obj.responses.map((response: any) => ({ ...response, type: selectorType }))
);
}
});
}
} catch {
// noop
}
return { selectors, responses };
}
export function getYamlFromSelectorsAndResponses(selectors: Selector[], responses: Response[]) {
const schema: any = {};
selectors.reduce((current, selector: any) => {
if (current && selector) {
if (current[selector.type]) {
current[selector.type]?.selectors.push(selector);
} else {
current[selector.type] = { selectors: [selector], responses: [] };
}
}
// the 'any' cast is used so we can keep 'selector.type' type safe
delete selector.type;
return current;
}, schema);
responses.reduce((current, response: any) => {
if (current && response) {
if (current[response.type]) {
current[response.type].responses.push(response);
} else {
current[response.type] = { selectors: [], responses: [response] };
}
}
// the 'any' cast is used so we can keep 'response.type' type safe
delete response.type;
return current;
}, schema);
return yaml.dump(schema);
}

View file

@ -1,44 +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 { LicenseType } from '@kbn/licensing-plugin/common/types';
import { isSubscriptionAllowed } from './subscription';
import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock';
const ON_PREM_ALLOWED_LICENSES: readonly LicenseType[] = ['enterprise', 'trial'];
const ON_PREM_NOT_ALLOWED_LICENSES: readonly LicenseType[] = ['basic', 'gold', 'platinum'];
const ALL_LICENSE_TYPES: readonly LicenseType[] = [
'standard',
...ON_PREM_NOT_ALLOWED_LICENSES,
...ON_PREM_NOT_ALLOWED_LICENSES,
];
describe('isSubscriptionAllowed', () => {
it('should allow any cloud subscription', () => {
const isCloudEnabled = true;
ALL_LICENSE_TYPES.forEach((licenseType) => {
const license = licenseMock.createLicense({ license: { type: licenseType } });
expect(isSubscriptionAllowed(isCloudEnabled, license)).toBeTruthy();
});
});
it('should allow enterprise and trial licenses for on-prem', () => {
const isCloudEnabled = false;
ON_PREM_ALLOWED_LICENSES.forEach((licenseType) => {
const license = licenseMock.createLicense({ license: { type: licenseType } });
expect(isSubscriptionAllowed(isCloudEnabled, license)).toBeTruthy();
});
});
it('should not allow enterprise and trial licenses for on-prem', () => {
const isCloudEnabled = false;
ON_PREM_NOT_ALLOWED_LICENSES.forEach((licenseType) => {
const license = licenseMock.createLicense({ license: { type: licenseType } });
expect(isSubscriptionAllowed(isCloudEnabled, license)).toBeFalsy();
});
});
});

View file

@ -1,24 +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 { ILicense, LicenseType } from '@kbn/licensing-plugin/common/types';
import { PLUGIN_NAME } from '../constants';
const MINIMUM_NON_CLOUD_LICENSE_TYPE: LicenseType = 'enterprise';
export const isSubscriptionAllowed = (isCloudEnabled?: boolean, license?: ILicense): boolean => {
if (isCloudEnabled) {
return true;
}
if (!license) {
return false;
}
const licenseCheck = license.check(PLUGIN_NAME, MINIMUM_NON_CLOUD_LICENSE_TYPE);
return licenseCheck.state === 'valid';
};

View file

@ -1,119 +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 { PackagePolicy, AgentPolicy } from '@kbn/fleet-plugin/common';
export type IndexStatus =
| 'not-empty' // Index contains documents
| 'empty' // Index doesn't contain documents (or doesn't exist)
| 'unprivileged'; // User doesn't have access to query the index
export type CloudDefendStatusCode =
| 'indexed' // alerts index exists and has results
| 'indexing' // index timeout was not surpassed since installation, assumes data is being indexed
| 'unprivileged' // user lacks privileges for the alerts index
| 'index-timeout' // index timeout was surpassed since installation
| 'not-deployed' // no healthy agents were deployed
| 'not-installed'; // number of installed integrations is 0;
export interface IndexDetails {
index: string;
status: IndexStatus;
}
interface BaseCloudDefendSetupStatus {
indicesDetails: IndexDetails[];
latestPackageVersion: string;
installedPackagePolicies: number;
healthyAgents: number;
}
interface CloudDefendSetupNotInstalledStatus extends BaseCloudDefendSetupStatus {
status: Extract<CloudDefendStatusCode, 'not-installed'>;
}
interface CloudDefendSetupInstalledStatus extends BaseCloudDefendSetupStatus {
status: Exclude<CloudDefendStatusCode, 'not-installed'>;
// status can be `indexed` but return with undefined package information in this case
installedPackageVersion: string | undefined;
}
export type CloudDefendSetupStatus =
| CloudDefendSetupInstalledStatus
| CloudDefendSetupNotInstalledStatus;
export type AgentPolicyStatus = Pick<AgentPolicy, 'id' | 'name'> & { agents: number };
export interface CloudDefendPolicy {
package_policy: PackagePolicy;
agent_policy: AgentPolicyStatus;
}
/**
* cloud_defend/control types
*/
// Currently we support file and process selectors (which match on their respective set of hook points)
export type SelectorType = 'file' | 'process';
export type SelectorCondition =
| 'containerImageFullName'
| 'containerImageName'
| 'containerImageTag'
| 'kubernetesClusterId'
| 'kubernetesClusterName'
| 'kubernetesNamespace'
| 'kubernetesPodLabel'
| 'kubernetesPodName'
| 'targetFilePath'
| 'ignoreVolumeFiles'
| 'ignoreVolumeMounts'
| 'operation'
| 'processExecutable'
| 'processName'
| 'sessionLeaderInteractive';
export type ResponseAction = 'log' | 'alert' | 'block';
export interface Selector {
name: string;
operation?: string[];
containerImageFullName?: string[];
containerImageName?: string[];
containerImageTag?: string[];
kubernetesClusterId?: string[];
kubernetesClusterName?: string[];
kubernetesNamespace?: string[];
kubernetesPodLabel?: string[];
kubernetesPodName?: string[];
// selector properties
targetFilePath?: string[];
ignoreVolumeFiles?: boolean;
ignoreVolumeMounts?: boolean;
// process selector properties
processExecutable?: string[];
processName?: string[];
sessionLeaderInteractive?: boolean;
// non yaml fields
type: SelectorType;
// used to track selector error state in UI
hasErrors?: boolean;
}
export interface Response {
match: string[];
exclude?: string[];
actions?: ResponseAction[];
// non yaml fields
type: SelectorType;
// used to track response error state in UI
hasErrors?: boolean;
}

View file

@ -1,19 +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.
*/
/** @type {import('@jest/types').Config.InitialOptions} */
module.exports = {
preset: '@kbn/test',
rootDir: '../../../../..',
roots: ['<rootDir>/x-pack/solutions/security/plugins/cloud_defend'],
coverageDirectory:
'<rootDir>/target/kibana-coverage/jest/x-pack/solutions/security/plugins/cloud_defend',
coverageReporters: ['text', 'html'],
collectCoverageFrom: [
'<rootDir>/x-pack/solutions/security/plugins/cloud_defend/{common,public,server}/**/*.{ts,tsx}',
],
};

View file

@ -1,36 +0,0 @@
{
"type": "plugin",
"id": "@kbn/cloud-defend-plugin",
"owner": [
"@elastic/kibana-cloud-security-posture"
],
"group": "security",
"visibility": "private",
"description": "Defend for containers (D4C)",
"plugin": {
"id": "cloudDefend",
"browser": true,
"server": true,
"configPath": [
"xpack",
"cloudDefend"
],
"requiredPlugins": [
"navigation",
"data",
"fleet",
"unifiedSearch",
"kibanaReact",
"cloud",
"security",
"licensing"
],
"optionalPlugins": [
"usageCollection"
],
"requiredBundles": [
"kibanaReact",
"usageCollection"
]
}
}

View file

@ -1,50 +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 { Route } from '@kbn/shared-ux-router';
import { type RouteProps } from 'react-router-dom';
import { TrackApplicationView } from '@kbn/usage-collection-plugin/public';
import { cloudDefendPages } from '../common/navigation/constants';
import { useSecuritySolutionContext } from './security_solution_context';
import type { CloudDefendPageNavigationItem } from '../common/navigation/types';
type CloudDefendRouteProps = Omit<RouteProps, 'render'> & CloudDefendPageNavigationItem;
// Security SpyRoute can be automatically rendered for pages with static paths, Security will manage everything using the `links` object.
// Pages with dynamic paths are not in the Security `links` object, they must render SpyRoute with the parameters values, if needed.
const STATIC_PATH_PAGE_IDS = Object.fromEntries(
Object.values(cloudDefendPages).map(({ id }) => [id, true])
);
export const CloudDefendRoute: React.FC<CloudDefendRouteProps> = ({
id,
children,
component: Component,
disabled = false,
...cloudDefendRouteProps
}) => {
const SpyRoute = useSecuritySolutionContext()?.getSpyRouteComponent();
if (disabled) {
return null;
}
const routeProps: RouteProps = {
...cloudDefendRouteProps,
...(Component && {
render: (renderProps) => (
<TrackApplicationView viewId={id}>
{STATIC_PATH_PAGE_IDS[id] && SpyRoute && <SpyRoute pageName={id} />}
<Component {...renderProps} />
</TrackApplicationView>
),
}),
};
return <Route {...routeProps}>{children}</Route>;
};

View file

@ -1,94 +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 CloudDefendRouter from './router';
import React from 'react';
import { render } from '@testing-library/react';
import { Router } from '@kbn/shared-ux-router';
import type { CloudDefendPage, CloudDefendPageNavigationItem } from '../common/navigation/types';
import { CloudDefendSecuritySolutionContext } from '../types';
import { createMemoryHistory, MemoryHistory } from 'history';
import * as constants from '../common/navigation/constants';
import { QueryClientProviderProps } from '@tanstack/react-query';
jest.mock('../pages/policies', () => ({
Policies: () => <div data-test-subj="Policies">Policies</div>,
}));
jest.mock('@tanstack/react-query', () => ({
QueryClientProvider: ({ children }: QueryClientProviderProps) => <>{children}</>,
QueryClient: jest.fn(),
}));
describe('CloudDefendRouter', () => {
const originalCloudDefendPages = { ...constants.cloudDefendPages };
const mockConstants = constants as {
cloudDefendPages: Record<CloudDefendPage, CloudDefendPageNavigationItem>;
};
const securityContext: CloudDefendSecuritySolutionContext = {
getFiltersGlobalComponent: jest.fn(),
getSpyRouteComponent: () => () => <div data-test-subj="mockedSpyRoute" />,
};
let history: MemoryHistory;
const renderCloudDefendRouter = () =>
render(
<Router history={history}>
<CloudDefendRouter securitySolutionContext={securityContext} />
</Router>
);
beforeEach(() => {
mockConstants.cloudDefendPages = originalCloudDefendPages;
jest.clearAllMocks();
history = createMemoryHistory();
});
describe('happy path', () => {
it('should render Policies', () => {
history.push('/cloud_defend/policies');
const result = renderCloudDefendRouter();
expect(result.queryByTestId('Policies')).toBeInTheDocument();
});
});
describe('unhappy path', () => {
it('should redirect base path to policies', () => {
history.push('/cloud_defend/some_wrong_path');
const result = renderCloudDefendRouter();
expect(history.location.pathname).toEqual('/cloud_defend/policies');
expect(result.queryByTestId('Policies')).toBeInTheDocument();
});
});
describe('CloudDefendRoute', () => {
it('should not render disabled path', () => {
mockConstants.cloudDefendPages = {
...constants.cloudDefendPages,
policies: {
...constants.cloudDefendPages.policies,
disabled: true,
},
};
history.push('/cloud_defend/policies');
const result = renderCloudDefendRouter();
expect(result.queryByTestId('Policies')).not.toBeInTheDocument();
});
it('should render SpyRoute for static paths', () => {
history.push('/cloud_defend/policies');
const result = renderCloudDefendRouter();
expect(result.queryByTestId('mockedSpyRoute')).toBeInTheDocument();
});
});
});

View file

@ -1,52 +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 { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Redirect } from 'react-router-dom';
import { Routes, Route } from '@kbn/shared-ux-router';
import { cloudDefendPages } from '../common/navigation/constants';
import type { CloudDefendSecuritySolutionContext } from '../types';
import { SecuritySolutionContext } from './security_solution_context';
import { Policies } from '../pages/policies';
import { CloudDefendRoute } from './route';
const queryClient = new QueryClient({
defaultOptions: { queries: { refetchOnWindowFocus: false } },
});
export interface CloudDefendRouterProps {
securitySolutionContext?: CloudDefendSecuritySolutionContext;
}
export const CloudDefendRouter = ({ securitySolutionContext }: CloudDefendRouterProps) => {
const routerElement = (
<QueryClientProvider client={queryClient}>
<Routes>
<CloudDefendRoute {...cloudDefendPages.policies} component={Policies} />
<Route>
<Redirect to={cloudDefendPages.policies.path} />
</Route>
</Routes>
</QueryClientProvider>
);
if (securitySolutionContext) {
return (
<SecuritySolutionContext.Provider value={securitySolutionContext}>
{routerElement}
</SecuritySolutionContext.Provider>
);
}
return <>{routerElement}</>;
};
// Using a default export for usage with `React.lazy`
// eslint-disable-next-line import/no-default-export
export { CloudDefendRouter as default };

View file

@ -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.
*/
import React, { useContext } from 'react';
import type { CloudDefendSecuritySolutionContext } from '../types';
export const SecuritySolutionContext = React.createContext<
CloudDefendSecuritySolutionContext | undefined
>(undefined);
export const useSecuritySolutionContext = () => {
return useContext(SecuritySolutionContext);
};

View file

@ -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.
*/
import { createContext } from 'react';
interface SetupContextValue {
isCloudEnabled?: boolean;
}
/**
* A utility to pass data from the plugin setup lifecycle stage to application components
*/
export const SetupContext = createContext<SetupContextValue>({});

View file

@ -1,13 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_224_212319)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.54808 23.8299C2.21188 23.6595 2 23.3147 2 22.9378V10.0097C2 9.26398 2.78673 8.78062 3.45192 9.11761L14.1792 14.5521C14.5154 14.7225 14.7273 15.0673 14.7273 15.4442V28.3723C14.7273 29.118 13.9405 29.6014 13.2753 29.2644L2.54808 23.8299Z" fill="#00BFB3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.4519 23.8299C29.7881 23.6595 30 23.3147 30 22.9378V10.0097C30 9.26398 29.2133 8.78062 28.5481 9.11761L17.8208 14.5521C17.4846 14.7225 17.2727 15.0673 17.2727 15.4442V28.3723C17.2727 29.118 18.0595 29.6014 18.7247 29.2644L29.4519 23.8299Z" fill="#343741"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.4519 12.3621C16.1678 12.506 15.8322 12.506 15.5481 12.3621L5.33403 7.18758C4.60326 6.81737 4.60326 5.77368 5.33403 5.40347L15.5481 0.228945C15.8322 0.085021 16.1678 0.0850213 16.4519 0.228946L26.666 5.40347C27.3968 5.77368 27.3967 6.81737 26.666 7.18758L16.4519 12.3621Z" fill="#1BA9F5"/>
<path d="M20.5714 18.2857H32V26.3067C32 27.7117 31.2628 29.0137 30.058 29.7366L26.2857 32L22.5134 29.7366C21.3086 29.0137 20.5714 27.7117 20.5714 26.3067V18.2857Z" fill="#FA744E"/>
</g>
<defs>
<clipPath id="clip0_224_212319">
<rect width="32" height="32" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -1,26 +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 { useQuery } from '@tanstack/react-query';
import {
epmRouteService,
type GetInfoResponse,
type DefaultPackagesInstallationError,
} from '@kbn/fleet-plugin/common';
import { INTEGRATION_PACKAGE_NAME } from '../../../common/constants';
import { useKibana } from '../hooks/use_kibana';
/**
* This hook will find our integration and return its PackageInfo
* */
export const useCloudDefendIntegration = () => {
const { http } = useKibana().services;
return useQuery<GetInfoResponse, DefaultPackagesInstallationError>(['integrations'], () =>
http.get<GetInfoResponse>(epmRouteService.getInfoPath(INTEGRATION_PACKAGE_NAME))
);
};

View file

@ -1,21 +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 { useQuery } from '@tanstack/react-query';
import { useKibana } from '../hooks/use_kibana';
import { CloudDefendSetupStatus } from '../../../common';
import { CURRENT_API_VERSION, STATUS_ROUTE_PATH } from '../../../common/constants';
const getCloudDefendSetupStatusQueryKey = 'cloud_defend_status_key';
export const useCloudDefendSetupStatusApi = () => {
const { http } = useKibana().services;
return useQuery<CloudDefendSetupStatus, unknown, CloudDefendSetupStatus>(
[getCloudDefendSetupStatusQueryKey],
() => http.get<CloudDefendSetupStatus>(STATUS_ROUTE_PATH, { version: CURRENT_API_VERSION })
);
};

View file

@ -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.
*/
export const DEFAULT_VISIBLE_ROWS_PER_PAGE = 10; // generic default # of table rows to show (currently we only have a list of policies)
export const LOCAL_STORAGE_PAGE_SIZE = 'cloudDefend:userPageSize';
export const VALID_SELECTOR_NAME_REGEX = /^[a-z0-9][a-z0-9_\-]*$/i; // alphanumberic (no - or _ allowed on first char)
export const MAX_SELECTORS_AND_RESPONSES_PER_TYPE = 64;
export const MAX_SELECTOR_NAME_LENGTH = 128; // chars
export const MAX_CONDITION_VALUE_LENGTH_BYTES = 511; // max length for all condition values. some props override this in cloud_defend/public/types.ts
// TODO: temporary until I change condition value length checks in the yaml editor view to be byte based.
export const MAX_CONDITION_VALUE_LENGTH = 64;
export const FIM_OPERATIONS = ['createFile', 'modifyFile', 'deleteFile'];

View file

@ -1,14 +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 { CoreStart } from '@kbn/core/public';
import { useKibana as useKibanaBase } from '@kbn/kibana-react-plugin/public';
import type { CloudDefendPluginStartDeps } from '../../types';
type CloudDefendKibanaContext = CoreStart & CloudDefendPluginStartDeps;
export const useKibana = () => useKibanaBase<CloudDefendKibanaContext>();

View file

@ -1,26 +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 useLocalStorage from 'react-use/lib/useLocalStorage';
import { DEFAULT_VISIBLE_ROWS_PER_PAGE } from '../constants';
/**
* @description handles persisting the users table row size selection
*/
export const usePageSize = (localStorageKey: string) => {
const [persistedPageSize, setPersistedPageSize] = useLocalStorage(
localStorageKey,
DEFAULT_VISIBLE_ROWS_PER_PAGE
);
let pageSize: number = DEFAULT_VISIBLE_ROWS_PER_PAGE;
if (persistedPageSize) {
pageSize = persistedPageSize;
}
return { pageSize, setPageSize: setPersistedPageSize };
};

View file

@ -1,22 +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 { useContext } from 'react';
import { useQuery } from '@tanstack/react-query';
import { SetupContext } from '../../application/setup_context';
import { isSubscriptionAllowed } from '../../../common/utils/subscription';
import { useKibana } from './use_kibana';
const SUBSCRIPTION_QUERY_KEY = 'cloud_defend_subscription_query_key';
export const useSubscriptionStatus = () => {
const { licensing } = useKibana().services;
const { isCloudEnabled } = useContext(SetupContext);
return useQuery([SUBSCRIPTION_QUERY_KEY], async () => {
const license = await licensing.getLicense();
return isSubscriptionAllowed(isCloudEnabled, license);
});
};

View file

@ -1,26 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import type { CloudDefendPage, CloudDefendPageNavigationItem } from './types';
const NAV_ITEMS_NAMES = {
POLICIES: i18n.translate('xpack.cloudDefend.navigation.policiesNavItemLabel', {
defaultMessage: 'Container Workload Protection',
}),
};
/** The base path for all cloud defend pages. */
const CLOUD_DEFEND_BASE_PATH = '/cloud_defend';
export const cloudDefendPages: Record<CloudDefendPage, CloudDefendPageNavigationItem> = {
policies: {
name: NAV_ITEMS_NAMES.POLICIES,
path: `${CLOUD_DEFEND_BASE_PATH}/policies`,
id: 'cloud_defend-policies',
},
};

View file

@ -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 { cloudDefendPages } from './constants';
import { getSecuritySolutionLink } from './security_solution_links';
import { Chance } from 'chance';
import type { CloudDefendPage } from './types';
const chance = new Chance();
describe('getSecuritySolutionLink', () => {
it('gets the correct link properties', () => {
const cloudDefendPage = chance.pickone<CloudDefendPage>(['policies']);
const link = getSecuritySolutionLink(cloudDefendPage);
expect(link.id).toEqual(cloudDefendPages[cloudDefendPage].id);
expect(link.path).toEqual(cloudDefendPages[cloudDefendPage].path);
expect(link.title).toEqual(cloudDefendPages[cloudDefendPage].name);
});
});

View file

@ -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 { cloudDefendPages } from './constants';
import type { CloudDefendPageId, CloudDefendPage } from './types';
interface CloudDefendLinkItem<TId extends string = CloudDefendPageId> {
id: TId;
title: string;
path: string;
}
/**
* Gets the cloud_defend link properties of a Cloud Defend page for navigation in the security solution.
* @param cloudDefendPage the name of the cloud defend page.
*/
export const getSecuritySolutionLink = <TId extends string = CloudDefendPageId>(
cloudDefendPage: CloudDefendPage
): CloudDefendLinkItem<TId> => {
return {
id: cloudDefendPages[cloudDefendPage].id as TId,
title: cloudDefendPages[cloudDefendPage].name,
path: cloudDefendPages[cloudDefendPage].path,
};
};

View file

@ -1,23 +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 interface CloudDefendNavigationItem {
readonly name: string;
readonly path: string;
readonly disabled?: boolean;
}
export interface CloudDefendPageNavigationItem extends CloudDefendNavigationItem {
id: CloudDefendPageId;
}
export type CloudDefendPage = 'policies';
/**
* All the IDs for the cloud defend pages.
* This needs to match the cloud defend page entries in `SecurityPageName` in `x-pack/solutions/security/plugins/security_solution/common/constants.ts`.
*/
export type CloudDefendPageId = 'cloud_defend-policies';

View file

@ -1,50 +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 { pagePathGetters, pkgKeyFromPackageInfo } from '@kbn/fleet-plugin/public';
import { INTEGRATION_PACKAGE_NAME } from '../../../common/constants';
import { useCloudDefendIntegration } from '../api/use_cloud_defend_integration';
import { useKibana } from '../hooks/use_kibana';
export const useCloudDefendIntegrationLinks = (): {
addIntegrationLink: string | undefined;
docsLink: string;
} => {
const { http } = useKibana().services;
const cloudDefendIntegration = useCloudDefendIntegration();
if (!cloudDefendIntegration.isSuccess)
return {
addIntegrationLink: undefined,
docsLink: 'https://www.elastic.co/guide/index.html',
};
const addIntegrationLink = pagePathGetters
.add_integration_to_policy({
integration: INTEGRATION_PACKAGE_NAME,
pkgkey: pkgKeyFromPackageInfo({
name: cloudDefendIntegration.data.item.name,
version: cloudDefendIntegration.data.item.version,
}),
})
.join('');
const docsLink = pagePathGetters
.integration_details_overview({
integration: INTEGRATION_PACKAGE_NAME,
pkgkey: pkgKeyFromPackageInfo({
name: cloudDefendIntegration.data.item.name,
version: cloudDefendIntegration.data.item.version,
}),
})
.join('');
return {
addIntegrationLink: http.basePath.prepend(addIntegrationLink),
docsLink: http.basePath.prepend(docsLink),
};
};

View file

@ -1,338 +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 {
getSelectorConditions,
conditionCombinationInvalid,
getRestrictedValuesForCondition,
validateBlockRestrictions,
selectorsIncludeConditionsForFIMOperationsUsingSlashStarStar,
} from './utils';
import { Selector, Response } from '../../common';
describe('getSelectorConditions', () => {
it('grabs file conditions for file selectors', () => {
const options = getSelectorConditions('file');
// check at least one common condition present
expect(options.includes('containerImageName')).toBeTruthy();
// check file specific conditions present
expect(options.includes('ignoreVolumeFiles')).toBeTruthy();
expect(options.includes('ignoreVolumeMounts')).toBeTruthy();
expect(options.includes('targetFilePath')).toBeTruthy();
// check that process specific conditions are not included
expect(options.includes('processName')).toBeFalsy();
expect(options.includes('processExecutable')).toBeFalsy();
expect(options.includes('sessionLeaderInteractive')).toBeFalsy();
});
it('grabs process conditions for process selectors', () => {
const options = getSelectorConditions('process');
// check at least one common condition present
expect(options.includes('containerImageName')).toBeTruthy();
// check file specific conditions present
expect(options.includes('ignoreVolumeFiles')).toBeFalsy();
expect(options.includes('ignoreVolumeMounts')).toBeFalsy();
expect(options.includes('targetFilePath')).toBeFalsy();
// check that process specific conditions are not included
expect(options.includes('processName')).toBeTruthy();
expect(options.includes('processExecutable')).toBeTruthy();
expect(options.includes('sessionLeaderInteractive')).toBeTruthy();
});
});
describe('conditionCombinationInvalid', () => {
it('returns true when conditions cannot be combined', () => {
const result = conditionCombinationInvalid(['ignoreVolumeMounts'], 'ignoreVolumeFiles');
expect(result).toBeTruthy();
});
it('returns false when they can', () => {
const result = conditionCombinationInvalid(['containerImageName'], 'ignoreVolumeFiles');
expect(result).toBeFalsy();
});
});
describe('getRestrictedValuesForCondition', () => {
it('works', () => {
let values = getRestrictedValuesForCondition('file', 'operation');
expect(values).toEqual([
'createExecutable',
'modifyExecutable',
'createFile',
'modifyFile',
'deleteFile',
]);
values = getRestrictedValuesForCondition('process', 'operation');
expect(values).toEqual(['fork', 'exec']);
});
});
describe('validateBlockRestrictions', () => {
it('reports an error when some of the FIM selectors (no operation) arent using targetFilePath', () => {
const selectors: Selector[] = [
{
type: 'file',
name: 'sel1', // no operation means all operations
},
{
type: 'file',
name: 'sel2',
operation: ['modifyFile'],
targetFilePath: ['/**'],
},
];
const responses: Response[] = [
{
type: 'file',
match: ['sel1', 'sel2'],
actions: ['block', 'alert'],
},
];
const errors = validateBlockRestrictions(selectors, responses);
expect(errors).toHaveLength(1);
});
it('reports an error when some of the FIM selectors arent using targetFilePath', () => {
const selectors: Selector[] = [
{
type: 'file',
name: 'sel1',
operation: ['createFile'],
},
{
type: 'file',
name: 'sel2',
operation: ['modifyFile'],
targetFilePath: ['/**'],
},
];
const responses: Response[] = [
{
type: 'file',
match: ['sel1', 'sel2'],
actions: ['block', 'alert'],
},
];
const errors = validateBlockRestrictions(selectors, responses);
expect(errors).toHaveLength(1);
});
it('reports an error when none of the FIM selectors use targetFilePath', () => {
const selectors: Selector[] = [
{
type: 'file',
name: 'sel1',
operation: ['createFile'],
},
{
type: 'file',
name: 'sel2',
operation: ['modifyFile'],
},
];
const responses: Response[] = [
{
type: 'file',
match: ['sel1', 'sel2'],
actions: ['block', 'alert'],
},
];
const errors = validateBlockRestrictions(selectors, responses);
expect(errors).toHaveLength(1);
});
it('passes validation when all FIM selectors (response.match) use targetFilePath', () => {
const selectors: Selector[] = [
{
type: 'file',
name: 'sel1',
operation: ['createFile'],
targetFilePath: ['/usr/bin/**', '/etc/**'],
},
{
type: 'file',
name: 'sel2',
operation: ['modifyFile'],
targetFilePath: ['/usr/bin/**', '/etc/**'],
},
];
const responses: Response[] = [
{
type: 'file',
match: ['sel1', 'sel2'],
actions: ['block', 'alert'],
},
];
const errors = validateBlockRestrictions(selectors, responses);
expect(errors).toHaveLength(0);
});
it('passes validation with non fim selectors mixed in', () => {
const selectors: Selector[] = [
{
type: 'file',
name: 'sel1',
operation: ['createFile'],
targetFilePath: ['/usr/bin/**', '/etc/**'],
},
{
type: 'file',
name: 'sel2',
operation: ['createExecutable', 'modifyExecutable'], // this should be allowed. FIM = createFile, modifyFile, deleteFile
},
];
const responses: Response[] = [
{
type: 'file',
match: ['sel1', 'sel2'],
actions: ['block', 'alert'],
},
];
const errors = validateBlockRestrictions(selectors, responses);
expect(errors).toHaveLength(0);
});
it('passes validation if at least 1 exclude uses targetFilePath', () => {
const selectors: Selector[] = [
{
type: 'file',
name: 'sel1',
operation: ['createFile'],
},
{
type: 'file',
name: 'excludePaths',
targetFilePath: ['/etc/**'],
},
];
const responses: Response[] = [
{
type: 'file',
match: ['sel1'],
exclude: ['excludePaths'],
actions: ['block', 'alert'],
},
];
const errors = validateBlockRestrictions(selectors, responses);
expect(errors).toHaveLength(0);
});
it('passes validation if block isnt used', () => {
const selectors: Selector[] = [
{
type: 'file',
name: 'sel1',
operation: ['createFile'],
},
];
const responses: Response[] = [
{
type: 'file',
match: ['sel1'],
exclude: ['excludePaths'],
actions: ['alert'],
},
];
const errors = validateBlockRestrictions(selectors, responses);
expect(errors).toHaveLength(0);
});
it('passes validation if block is used, but no selectors in match', () => {
const responses: Response[] = [
{
type: 'file',
match: [],
actions: ['alert', 'block'],
},
];
const errors = validateBlockRestrictions([], responses);
expect(errors).toHaveLength(0);
});
});
describe('selectorsIncludeConditionsForFIMOperationsUsingSlashStarStar', () => {
it('returns true', () => {
const selectors: Selector[] = [
{
type: 'file',
name: 'sel1',
operation: ['createFile'],
targetFilePath: ['/**'],
},
];
const response: Response = {
type: 'file',
match: ['sel1'],
actions: ['block', 'alert'],
};
const result = selectorsIncludeConditionsForFIMOperationsUsingSlashStarStar(
selectors,
response.match
);
expect(result).toBeTruthy();
});
it('returns false', () => {
const selectors: Selector[] = [
{
type: 'file',
name: 'sel1',
operation: ['createFile'],
targetFilePath: ['/usr/bin/**'],
},
];
const response: Response = {
type: 'file',
match: ['sel1'],
actions: ['block', 'alert'],
};
const result = selectorsIncludeConditionsForFIMOperationsUsingSlashStarStar(
selectors,
response.match
);
expect(result).toBeFalsy();
});
});

View file

@ -1,255 +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 { uniq } from 'lodash';
import { i18n } from '@kbn/i18n';
import { errorBlockActionRequiresTargetFilePath } from '../components/control_general_view/translations';
import {
DefaultFileSelector,
DefaultProcessSelector,
DefaultFileResponse,
DefaultProcessResponse,
SelectorConditionsMap,
} from '../types';
import { Selector, Response, SelectorType, SelectorCondition } from '../../common';
import {
MAX_CONDITION_VALUE_LENGTH_BYTES,
MAX_SELECTORS_AND_RESPONSES_PER_TYPE,
FIM_OPERATIONS,
} from './constants';
export function getSelectorTypeIcon(type: SelectorType) {
switch (type) {
case 'process':
return 'gear';
case 'file':
default:
return 'document';
}
}
export function camelToSentenceCase(prop: string) {
const sentence = prop.replace(/([A-Z])/g, ' $1').toLowerCase();
return sentence[0].toUpperCase() + sentence.slice(1);
}
export function conditionCombinationInvalid(
addedConditions: SelectorCondition[],
condition: SelectorCondition
): boolean {
const options = SelectorConditionsMap[condition];
const invalid = addedConditions.find((added) => {
return options?.not?.includes(added);
});
return !!invalid;
}
type TotalByType = {
[key in SelectorType]: number;
};
export function getTotalsByType(selectors: Selector[], responses: Response[]) {
const totalsByType: TotalByType = { process: 0, file: 0 };
selectors.forEach((selector) => {
totalsByType[selector.type]++;
});
responses.forEach((response) => {
totalsByType[response.type]++;
});
return totalsByType;
}
function selectorUsesFIM(selector?: Selector) {
return (
selector &&
(!selector.operation ||
selector.operation.length === 0 ||
selector.operation.some((r) => FIM_OPERATIONS.indexOf(r) >= 0))
);
}
function selectorsIncludeConditionsForFIMOperations(
selectors: Selector[],
conditions: SelectorCondition[],
selectorNames?: string[],
requireForAll?: boolean
) {
const result =
selectorNames &&
selectorNames.reduce((prev, cur) => {
const selector = selectors.find((s) => s.name === cur);
const usesFIM = selectorUsesFIM(selector);
const hasAllConditions =
!usesFIM ||
!!(
selector &&
conditions.reduce((p, c) => {
return p && Object.hasOwn(selector, c);
}, true)
);
if (requireForAll) {
return prev && hasAllConditions;
} else {
return prev || hasAllConditions;
}
}, requireForAll);
return !!result;
}
export function selectorsIncludeConditionsForFIMOperationsUsingSlashStarStar(
selectors: Selector[],
selectorNames?: string[]
) {
const result =
selectorNames &&
selectorNames.reduce((prev, cur) => {
const selector = selectors.find((s) => s.name === cur);
const usesFIM = selectorUsesFIM(selector);
return prev || !!(usesFIM && selector?.targetFilePath?.includes('/**'));
}, false);
return !!result;
}
export function validateBlockRestrictions(selectors: Selector[], responses: Response[]) {
const errors: string[] = [];
responses.forEach((response) => {
if (response.actions?.includes('block')) {
// check if any selectors are using FIM operations
// and verify that targetFilePath is specfied in all 'match' selectors
// or at least one 'exclude' selector
const excludeUsesTargetFilePath = selectorsIncludeConditionsForFIMOperations(
selectors,
['targetFilePath'],
response.exclude
);
const matchSelectorsAllUsingTargetFilePath = selectorsIncludeConditionsForFIMOperations(
selectors,
['targetFilePath'],
response.match,
true
);
if (!(matchSelectorsAllUsingTargetFilePath || excludeUsesTargetFilePath)) {
errors.push(errorBlockActionRequiresTargetFilePath);
}
}
});
return errors;
}
export function validateMaxSelectorsAndResponses(selectors: Selector[], responses: Response[]) {
const errors: string[] = [];
const totalsByType = getTotalsByType(selectors, responses);
// check selectors + responses doesn't exceed MAX_SELECTORS_AND_RESPONSES_PER_TYPE
Object.values(totalsByType).forEach((count) => {
if (count > MAX_SELECTORS_AND_RESPONSES_PER_TYPE) {
errors.push(
i18n.translate('xpack.cloudDefend.errorMaxSelectorsResponsesExceeded', {
defaultMessage:
'You cannot exceed {max} selectors + responses for a given type e.g file, process',
values: { max: MAX_SELECTORS_AND_RESPONSES_PER_TYPE },
})
);
}
});
return errors;
}
export function validateStringValuesForCondition(condition: SelectorCondition, values?: string[]) {
const errors: string[] = [];
const maxValueBytes =
SelectorConditionsMap[condition].maxValueBytes || MAX_CONDITION_VALUE_LENGTH_BYTES;
const { pattern, patternError } = SelectorConditionsMap[condition];
values?.forEach((value) => {
if (value?.length === 0) {
errors.push(
i18n.translate('xpack.cloudDefend.errorGenericEmptyValue', {
defaultMessage: '"{condition}" values cannot be empty',
values: { condition },
})
);
} else if (pattern && !new RegExp(pattern).test(value)) {
if (patternError) {
errors.push(patternError);
} else {
errors.push(
i18n.translate('xpack.cloudDefend.errorGenericRegexFailure', {
defaultMessage: '"{condition}" values must match the pattern: /{pattern}/',
values: { condition, pattern },
})
);
}
}
const bytes = new Blob([value]).size;
if (bytes > maxValueBytes) {
errors.push(
i18n.translate('xpack.cloudDefend.errorMaxValueBytesExceeded', {
defaultMessage: '"{condition}" values cannot exceed {maxValueBytes} bytes',
values: { condition, maxValueBytes },
})
);
}
});
return uniq(errors);
}
export function getRestrictedValuesForCondition(
type: SelectorType,
condition: SelectorCondition
): string[] | undefined {
const options = SelectorConditionsMap[condition];
if (Array.isArray(options.values)) {
return options.values;
}
if (options?.values?.[type]) {
return options.values[type];
}
}
export function getSelectorConditions(type: SelectorType): SelectorCondition[] {
const allConditions = Object.keys(SelectorConditionsMap) as SelectorCondition[];
return allConditions.filter((key) => {
const options = SelectorConditionsMap[key];
return !options.selectorType || options.selectorType === type;
});
}
export function getDefaultSelectorByType(type: SelectorType): Selector {
switch (type) {
case 'process':
return JSON.parse(JSON.stringify(DefaultProcessSelector));
case 'file':
default:
return JSON.parse(JSON.stringify(DefaultFileSelector));
}
}
export function getDefaultResponseByType(type: SelectorType): Response {
switch (type) {
case 'process':
return { ...DefaultProcessResponse };
case 'file':
default:
return { ...DefaultFileResponse };
}
}

View file

@ -1,361 +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 { useSubscriptionStatus } from '../../common/hooks/use_subscription_status';
import Chance from 'chance';
import {
CloudDefendPage,
DEFAULT_NO_DATA_TEST_SUBJECT,
ERROR_STATE_TEST_SUBJECT,
isCommonError,
LOADING_STATE_TEST_SUBJECT,
PACKAGE_NOT_INSTALLED_TEST_SUBJECT,
SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT,
} from '.';
import { createReactQueryResponse } from '../../test/fixtures/react_query';
import { TestProvider } from '../../test/test_provider';
import { coreMock } from '@kbn/core/public/mocks';
import { render, screen } from '@testing-library/react';
import React, { ComponentProps } from 'react';
import { UseQueryResult } from '@tanstack/react-query';
import { NoDataPage } from '@kbn/kibana-react-plugin/public';
import { useCloudDefendSetupStatusApi } from '../../common/api/use_setup_status_api';
import { useCloudDefendIntegrationLinks } from '../../common/navigation/use_cloud_defend_integration_links';
const chance = new Chance();
jest.mock('../../common/api/use_setup_status_api');
jest.mock('../../common/hooks/use_subscription_status');
jest.mock('../../common/navigation/use_cloud_defend_integration_links');
describe('<CloudDefendPage />', () => {
beforeEach(() => {
jest.resetAllMocks();
(useCloudDefendSetupStatusApi as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: { status: 'indexed' },
})
);
(useCloudDefendIntegrationLinks as jest.Mock).mockImplementation(() => ({
addIntegrationLink: chance.url(),
docsLink: chance.url(),
}));
(useSubscriptionStatus as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: true,
})
);
});
const renderCloudDefendPage = (
props: ComponentProps<typeof CloudDefendPage> = { children: null }
) => {
const mockCore = coreMock.createStart();
render(
<TestProvider
core={{
...mockCore,
application: {
...mockCore.application,
capabilities: {
...mockCore.application.capabilities,
// This is required so that the `noDataConfig` view will show the action button
navLinks: { integrations: true },
},
},
}}
>
<CloudDefendPage {...props} />
</TestProvider>
);
};
it('renders children if setup status is indexed', () => {
const children = chance.sentence();
renderCloudDefendPage({ children });
expect(screen.getByText(children)).toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('renders default loading state when the subscription query is loading', () => {
(useSubscriptionStatus as jest.Mock).mockImplementation(
() =>
createReactQueryResponse({
status: 'loading',
}) as unknown as UseQueryResult
);
const children = chance.sentence();
renderCloudDefendPage({ children });
expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('renders default error state when the subscription query has an error', () => {
(useSubscriptionStatus as jest.Mock).mockImplementation(
() =>
createReactQueryResponse({
status: 'error',
error: new Error('error'),
}) as unknown as UseQueryResult
);
const children = chance.sentence();
renderCloudDefendPage({ children });
expect(screen.getByTestId(ERROR_STATE_TEST_SUBJECT)).toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('renders subscription not allowed prompt if subscription is not installed', () => {
(useSubscriptionStatus as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: false,
})
);
const children = chance.sentence();
renderCloudDefendPage({ children });
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.getByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('renders integrations installation prompt if integration is not installed', () => {
(useCloudDefendSetupStatusApi as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: { status: 'not-installed' },
})
);
const children = chance.sentence();
renderCloudDefendPage({ children });
expect(screen.getByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('renders default loading state when the integration query is loading', () => {
(useCloudDefendSetupStatusApi as jest.Mock).mockImplementation(
() =>
createReactQueryResponse({
status: 'loading',
}) as unknown as UseQueryResult
);
const children = chance.sentence();
renderCloudDefendPage({ children });
expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('renders default error state when the integration query has an error', () => {
(useCloudDefendSetupStatusApi as jest.Mock).mockImplementation(
() =>
createReactQueryResponse({
status: 'error',
error: new Error('error'),
}) as unknown as UseQueryResult
);
const children = chance.sentence();
renderCloudDefendPage({ children });
expect(screen.getByTestId(ERROR_STATE_TEST_SUBJECT)).toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('renders default loading text when query isLoading', () => {
const query = createReactQueryResponse({
status: 'loading',
}) as unknown as UseQueryResult;
const children = chance.sentence();
renderCloudDefendPage({ children, query });
expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument();
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('renders default loading text when query is idle', () => {
const query = createReactQueryResponse({
status: 'idle',
}) as unknown as UseQueryResult;
const children = chance.sentence();
renderCloudDefendPage({ children, query });
expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument();
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('renders default error texts when query isError', () => {
const error = chance.sentence();
const message = chance.sentence();
const statusCode = chance.integer();
const query = createReactQueryResponse({
status: 'error',
error: {
body: {
error,
message,
statusCode,
},
},
}) as unknown as UseQueryResult;
const children = chance.sentence();
renderCloudDefendPage({ children, query });
[error, message, statusCode].forEach((text) =>
expect(screen.getByText(text, { exact: false })).toBeInTheDocument()
);
expect(screen.getByTestId(ERROR_STATE_TEST_SUBJECT)).toBeInTheDocument();
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('prefers custom error render', () => {
const error = chance.sentence();
const message = chance.sentence();
const statusCode = chance.integer();
const query = createReactQueryResponse({
status: 'error',
error: {
body: {
error,
message,
statusCode,
},
},
}) as unknown as UseQueryResult;
const children = chance.sentence();
renderCloudDefendPage({
children,
query,
errorRender: (err) => <div>{isCommonError(err) && err.body.message}</div>,
});
expect(screen.getByText(message)).toBeInTheDocument();
[error, statusCode].forEach((text) => expect(screen.queryByText(text)).not.toBeInTheDocument());
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('prefers custom loading render', () => {
const loading = chance.sentence();
const query = createReactQueryResponse({
status: 'loading',
}) as unknown as UseQueryResult;
const children = chance.sentence();
renderCloudDefendPage({
children,
query,
loadingRender: () => <div>{loading}</div>,
});
expect(screen.getByText(loading)).toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('renders no data prompt when query data is undefined', () => {
const query = createReactQueryResponse({
status: 'success',
data: undefined,
}) as unknown as UseQueryResult;
const children = chance.sentence();
renderCloudDefendPage({ children, query });
expect(screen.getByTestId(DEFAULT_NO_DATA_TEST_SUBJECT)).toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
});
it('prefers custom no data prompt', () => {
const pageTitle = chance.sentence();
const solution = chance.sentence();
const docsLink = chance.sentence();
const noDataRenderer = () => (
<NoDataPage pageTitle={pageTitle} solution={solution} docsLink={docsLink} actions={{}} />
);
const query = createReactQueryResponse({
status: 'success',
data: undefined,
}) as unknown as UseQueryResult;
const children = chance.sentence();
renderCloudDefendPage({
children,
query,
noDataRenderer,
});
expect(screen.getByText(pageTitle)).toBeInTheDocument();
expect(screen.getAllByText(solution, { exact: false })[0]).toBeInTheDocument();
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByText(children)).not.toBeInTheDocument();
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
});
});

View file

@ -1,294 +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 { i18n } from '@kbn/i18n';
import type { UseQueryResult } from '@tanstack/react-query';
import {
EuiButton,
EuiEmptyPrompt,
EuiImage,
EuiFlexGroup,
EuiFlexItem,
EuiLink,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { NoDataPage, NoDataPageProps } from '@kbn/kibana-react-plugin/public';
import { css } from '@emotion/react';
import { SubscriptionNotAllowed } from '../subscription_not_allowed';
import { useSubscriptionStatus } from '../../common/hooks/use_subscription_status';
import { FullSizeCenteredPage } from '../full_size_page';
import { useCloudDefendSetupStatusApi } from '../../common/api/use_setup_status_api';
import { LoadingState } from '../loading_state';
import { useCloudDefendIntegrationLinks } from '../../common/navigation/use_cloud_defend_integration_links';
import noDataIllustration from '../../assets/icons/logo.svg';
export const LOADING_STATE_TEST_SUBJECT = 'cloud_defend_page_loading';
export const ERROR_STATE_TEST_SUBJECT = 'cloud_defend_page_error';
export const PACKAGE_NOT_INSTALLED_TEST_SUBJECT = 'cloud_defend_page_package_not_installed';
export const DEFAULT_NO_DATA_TEST_SUBJECT = 'cloud_defend_page_no_data';
export const SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT = 'cloud_defend_page_subscription_not_allowed';
interface CommonError {
body: {
error: string;
message: string;
statusCode: number;
};
}
export const isCommonError = (error: unknown): error is CommonError => {
if (
!(error as any)?.body ||
!(error as any)?.body?.error ||
!(error as any)?.body?.message ||
!(error as any)?.body?.statusCode
) {
return false;
}
return true;
};
export interface CloudDefendNoDataPageProps {
pageTitle: NoDataPageProps['pageTitle'];
docsLink: NoDataPageProps['docsLink'];
actionHref: NoDataPageProps['actions']['elasticAgent']['href'];
actionTitle: NoDataPageProps['actions']['elasticAgent']['title'];
actionDescription: NoDataPageProps['actions']['elasticAgent']['description'];
testId: string;
}
export const CloudDefendNoDataPage = ({
pageTitle,
docsLink,
actionHref,
actionTitle,
actionDescription,
testId,
}: CloudDefendNoDataPageProps) => {
return (
<NoDataPage
data-test-subj={testId}
css={css`
> :nth-child(3) {
display: block;
margin: auto;
width: 450px;
}
`}
pageTitle={pageTitle}
solution={i18n.translate(
'xpack.cloudDefend.cloudDefendPage.packageNotInstalled.solutionNameLabel',
{
defaultMessage: 'Defend for containers (D4C)',
}
)}
docsLink={docsLink}
logo="logoSecurity"
actions={{
elasticAgent: {
href: actionHref,
isDisabled: !actionHref,
title: actionTitle,
description: actionDescription,
},
}}
/>
);
};
const packageNotInstalledRenderer = ({
addIntegrationLink,
docsLink,
}: {
addIntegrationLink?: string;
docsLink?: string;
}) => {
return (
<FullSizeCenteredPage>
<EuiEmptyPrompt
data-test-subj={PACKAGE_NOT_INSTALLED_TEST_SUBJECT}
icon={<EuiImage size="m" margin="m" src={noDataIllustration} alt="" role="presentation" />}
title={
<h2>
<FormattedMessage
id="xpack.cloudDefend.cloudDefendPage.packageNotInstalledRenderer.promptTitle"
defaultMessage="Detect container drift and block malicious behavior at the source!"
/>
</h2>
}
layout="horizontal"
color="plain"
body={
<p>
<FormattedMessage
id="xpack.cloudDefend.cloudDefendPage.packageNotInstalledRenderer.promptDescription"
defaultMessage="Add the Defend for containers (D4C) integration to begin. {learnMore}."
values={{
learnMore: (
<EuiLink href={docsLink}>
<FormattedMessage
id="xpack.cloudDefend.cloudDefendPage.packageNotInstalledRenderer.learnMoreTitle"
defaultMessage="Learn more about Defend for containers (D4C)"
/>
</EuiLink>
),
}}
/>
</p>
}
actions={
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton color="primary" fill href={addIntegrationLink}>
<FormattedMessage
id="xpack.cloudDefend.cloudDefendPage.packageNotInstalledRenderer.addCloudDefendmIntegrationButtonTitle"
defaultMessage="Add D4C Integration"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
}
/>
</FullSizeCenteredPage>
);
};
const defaultLoadingRenderer = () => (
<LoadingState data-test-subj={LOADING_STATE_TEST_SUBJECT}>
<FormattedMessage
id="xpack.cloudDefend.cloudDefendPage.loadingDescription"
defaultMessage="Loading..."
/>
</LoadingState>
);
const defaultErrorRenderer = (error: unknown) => (
<FullSizeCenteredPage>
<EuiEmptyPrompt
color="danger"
iconType="warning"
data-test-subj={ERROR_STATE_TEST_SUBJECT}
title={
<h2>
<FormattedMessage
id="xpack.cloudDefend.cloudDefendPage.errorRenderer.errorTitle"
defaultMessage="We couldn't fetch your cloud defend data"
/>
</h2>
}
body={
isCommonError(error) ? (
<p>
<FormattedMessage
id="xpack.cloudDefend.cloudDefendPage.errorRenderer.errorDescription"
defaultMessage="{error} {statusCode}: {body}"
values={{
error: error.body.error,
statusCode: error.body.statusCode,
body: error.body.message,
}}
/>
</p>
) : undefined
}
/>
</FullSizeCenteredPage>
);
const defaultNoDataRenderer = (docsLink: string) => (
<FullSizeCenteredPage>
<NoDataPage
data-test-subj={DEFAULT_NO_DATA_TEST_SUBJECT}
pageTitle={i18n.translate('xpack.cloudDefend.cloudDefendPage.defaultNoDataConfig.pageTitle', {
defaultMessage: 'No data found',
})}
solution={i18n.translate(
'xpack.cloudDefend.cloudDefendPage.defaultNoDataConfig.solutionNameLabel',
{
defaultMessage: 'Defend for containers',
}
)}
docsLink={docsLink}
logo={'logoSecurity'}
actions={{}}
/>
</FullSizeCenteredPage>
);
const subscriptionNotAllowedRenderer = () => (
<FullSizeCenteredPage data-test-subj={SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT}>
<SubscriptionNotAllowed />
</FullSizeCenteredPage>
);
interface CloudDefendPageProps<TData, TError> {
children: React.ReactNode;
query?: UseQueryResult<TData, TError>;
loadingRender?: () => React.ReactNode;
errorRender?: (error: TError) => React.ReactNode;
noDataRenderer?: (docsLink: string) => React.ReactNode;
}
export const CloudDefendPage = <TData, TError>({
children,
query,
loadingRender = defaultLoadingRenderer,
errorRender = defaultErrorRenderer,
noDataRenderer = defaultNoDataRenderer,
}: CloudDefendPageProps<TData, TError>) => {
const subscriptionStatus = useSubscriptionStatus();
const getSetupStatus = useCloudDefendSetupStatusApi();
const { addIntegrationLink, docsLink } = useCloudDefendIntegrationLinks();
const render = () => {
if (subscriptionStatus.isError) {
return defaultErrorRenderer(subscriptionStatus.error);
}
if (subscriptionStatus.isLoading) {
return defaultLoadingRenderer();
}
if (!subscriptionStatus.data) {
return subscriptionNotAllowedRenderer();
}
if (getSetupStatus.isError) {
return defaultErrorRenderer(getSetupStatus.error);
}
if (getSetupStatus.isLoading) {
return defaultLoadingRenderer();
}
if (getSetupStatus.data.status === 'not-installed') {
return packageNotInstalledRenderer({ addIntegrationLink, docsLink });
}
if (!query) {
return children;
}
if (query.isError) {
return errorRender(query.error);
}
if (query.isLoading) {
return loadingRender();
}
if (!query.data) {
return noDataRenderer(docsLink);
}
return children;
};
return <>{render()}</>;
};

View file

@ -1,19 +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 { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
export const CloudDefendPageTitle = ({ title }: { title: string }) => (
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiTitle>
<h1>{title}</h1>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
);

View file

@ -1,216 +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 yaml from 'js-yaml';
import { render, waitFor } from '@testing-library/react';
import { coreMock } from '@kbn/core/public/mocks';
import userEvent from '@testing-library/user-event';
import { TestProvider } from '../../test/test_provider';
import {
getCloudDefendNewPolicyMock,
MOCK_YAML_INVALID_CONFIGURATION,
MOCK_YAML_TOO_MANY_FILE_SELECTORS_RESPONSES,
} from '../../test/mocks';
import { ControlGeneralView } from '.';
import { getInputFromPolicy } from '../../../common/utils/helpers';
import { INPUT_CONTROL } from '../../../common/constants';
describe('<ControlGeneralView />', () => {
const onChange = jest.fn();
// defining this here to avoid a warning in testprovider with params.history changing on rerender.
const params = coreMock.createAppMountParameters();
const WrappedComponent = ({ policy = getCloudDefendNewPolicyMock() }) => {
return (
<TestProvider params={params}>
<ControlGeneralView policy={policy} onChange={onChange} show />;
</TestProvider>
);
};
beforeEach(() => {
onChange.mockClear();
});
it('renders a list of selectors and responses', () => {
const { getAllByTestId } = render(<WrappedComponent />);
const input = getInputFromPolicy(getCloudDefendNewPolicyMock(), INPUT_CONTROL);
const configuration = input?.vars?.configuration?.value;
try {
const json = yaml.load(configuration);
expect(json.file.selectors.length).toBe(getAllByTestId('cloud-defend-selector').length);
expect(json.file.responses.length).toBe(getAllByTestId('cloud-defend-file-response').length);
expect(json.file.selectors.length).toBe(3);
expect(json.file.responses.length).toBe(2);
} catch (err) {
throw err;
}
});
it('allows a user to add a new selector', async () => {
const { getAllByTestId, getByTestId, rerender } = render(<WrappedComponent />);
await userEvent.click(getByTestId('cloud-defend-btnAddSelector'));
await userEvent.click(getByTestId('cloud-defend-btnAddFileSelector'));
const policy = onChange.mock.calls[0][0].updatedPolicy;
rerender(<WrappedComponent policy={policy} />);
const input = getInputFromPolicy(policy, INPUT_CONTROL);
const configuration = input?.vars?.configuration?.value;
try {
const json = yaml.load(configuration);
expect(json.file.selectors.length).toBe(getAllByTestId('cloud-defend-selector').length);
} catch (err) {
throw err;
}
});
it('allows a user to add a file response', async () => {
const { getAllByTestId, getByTestId, rerender } = render(<WrappedComponent />);
await userEvent.click(getByTestId('cloud-defend-btnAddResponse'));
await userEvent.click(getByTestId('cloud-defend-btnAddFileResponse'));
const policy = onChange.mock.calls[0][0].updatedPolicy;
rerender(<WrappedComponent policy={policy} />);
const input = getInputFromPolicy(policy, INPUT_CONTROL);
const configuration = input?.vars?.configuration?.value;
try {
const json = yaml.load(configuration);
expect(json.file.responses.length).toBe(getAllByTestId('cloud-defend-file-response').length);
} catch (err) {
throw err;
}
});
it('allows a user to add a process response', async () => {
const { getAllByTestId, getByTestId, rerender } = render(<WrappedComponent />);
await userEvent.click(getByTestId('cloud-defend-btnAddResponse'));
await userEvent.click(getByTestId('cloud-defend-btnAddProcessResponse'));
const policy = onChange.mock.calls[0][0].updatedPolicy;
rerender(<WrappedComponent policy={policy} />);
const input = getInputFromPolicy(policy, INPUT_CONTROL);
const configuration = input?.vars?.configuration?.value;
try {
const json = yaml.load(configuration);
expect(json.process.responses.length).toBe(
getAllByTestId('cloud-defend-process-response').length
);
} catch (err) {
throw err;
}
});
it('updates selector name used in response.match, if its name is changed', async () => {
const { getByTitle, getAllByTestId, rerender } = render(<WrappedComponent />);
const input = await waitFor(
() => getAllByTestId('cloud-defend-selectorcondition-name')[1] as HTMLInputElement
);
await userEvent.type(input, '2');
const policy = onChange.mock.calls[0][0].updatedPolicy;
rerender(<WrappedComponent policy={policy} />);
expect(getByTitle('Remove nginxOnly2 from selection in this group')).toBeTruthy(); // would be 'nginxOnly' had the update not worked
});
it('updates selector name used in response.exclude, if its name is changed', async () => {
const { getByTitle, getAllByTestId, rerender } = render(<WrappedComponent />);
const input = await waitFor(
() => getAllByTestId('cloud-defend-selectorcondition-name')[2] as HTMLInputElement
);
await userEvent.type(input, '3');
const policy = onChange.mock.calls[0][0].updatedPolicy;
rerender(<WrappedComponent policy={policy} />);
expect(getByTitle('Remove excludeCustomNginxBuild3 from selection in this group')).toBeTruthy();
});
it('removes a selector from a match/exclude list of a response if it is deleted', async () => {
const { getByTestId, getAllByTestId } = render(<WrappedComponent />);
const btnSelectorPopover = getAllByTestId('cloud-defend-btnselectorpopover')[0];
btnSelectorPopover.click();
await waitFor(() => getByTestId('cloud-defend-btndeleteselector').click());
const policy = onChange.mock.calls[0][0].updatedPolicy;
const input = getInputFromPolicy(policy, INPUT_CONTROL);
const configuration = input?.vars?.configuration?.value;
try {
const json = yaml.load(configuration);
expect(json.file.responses[0].match).toHaveLength(1);
} catch (err) {
throw err;
}
});
it('doesnt blow up if invalid yaml passed in', async () => {
const { queryAllByTestId } = render(
<WrappedComponent policy={getCloudDefendNewPolicyMock(MOCK_YAML_INVALID_CONFIGURATION)} />
);
expect(queryAllByTestId('cloud-defend-selector')).toHaveLength(0);
expect(queryAllByTestId('cloud-defend-response')).toHaveLength(0);
});
it('prevents the user from adding more than MAX_SELECTORS_AND_RESPONSES_PER_TYPE', async () => {
const { getByTestId } = render(
<WrappedComponent
policy={getCloudDefendNewPolicyMock(MOCK_YAML_TOO_MANY_FILE_SELECTORS_RESPONSES)}
/>
);
await userEvent.click(getByTestId('cloud-defend-btnAddSelector'));
expect(getByTestId('cloud-defend-btnAddFileSelector')).toBeDisabled();
});
it('allows the user to duplicate the selector', async () => {
const { getByTestId, getAllByTestId } = render(<WrappedComponent />);
const btnSelectorPopover = getAllByTestId('cloud-defend-btnselectorpopover')[0];
btnSelectorPopover.click();
await waitFor(() => getByTestId('cloud-defend-btnduplicateselector').click());
const policy = onChange.mock.calls[0][0].updatedPolicy;
const input = getInputFromPolicy(policy, INPUT_CONTROL);
const configuration = input?.vars?.configuration?.value;
try {
const json = yaml.load(configuration);
expect(json.file.selectors).toHaveLength(4);
expect(json.file.selectors[3].name).toEqual(json.file.selectors[0].name + '1');
} catch (err) {
throw err;
}
});
});

View file

@ -1,387 +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, useCallback, useState } from 'react';
import {
EuiContextMenuPanel,
EuiContextMenuItem,
EuiPopover,
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiTitle,
EuiButton,
EuiSpacer,
} from '@elastic/eui';
import { INPUT_CONTROL } from '../../../common/constants';
import { useStyles } from './styles';
import {
getDefaultSelectorByType,
getDefaultResponseByType,
getTotalsByType,
} from '../../common/utils';
import {
getInputFromPolicy,
getYamlFromSelectorsAndResponses,
getSelectorsAndResponsesFromYaml,
} from '../../../common/utils/helpers';
import { ViewDeps } from '../../types';
import { SelectorType, Selector, Response } from '../../../common';
import * as i18n from './translations';
import { ControlGeneralViewSelector } from '../control_general_view_selector';
import { ControlGeneralViewResponse } from '../control_general_view_response';
import { MAX_SELECTORS_AND_RESPONSES_PER_TYPE } from '../../common/constants';
interface AddSelectorButtonProps {
type: 'Selector' | 'Response';
onSelectType(type: SelectorType): void;
selectors: Selector[];
responses: Response[];
}
/**
* dual purpose button for adding selectors and responses by type
*/
const AddButton = ({ type, onSelectType, selectors, responses }: AddSelectorButtonProps) => {
const totalsByType = useMemo(() => getTotalsByType(selectors, responses), [responses, selectors]);
const [isPopoverOpen, setPopover] = useState(false);
const onButtonClick = () => {
setPopover(!isPopoverOpen);
};
const closePopover = () => {
setPopover(false);
};
const addFile = useCallback(() => {
closePopover();
onSelectType('file');
}, [onSelectType]);
const addProcess = useCallback(() => {
closePopover();
onSelectType('process');
}, [onSelectType]);
const isSelector = type === 'Selector';
const items = [
<EuiContextMenuItem
key={`addFile${type}`}
icon="document"
onClick={addFile}
disabled={totalsByType.file >= MAX_SELECTORS_AND_RESPONSES_PER_TYPE}
data-test-subj={`cloud-defend-btnAddFile${type}`}
>
{isSelector ? i18n.fileSelector : i18n.fileResponse}
</EuiContextMenuItem>,
<EuiContextMenuItem
key={`addProcess${type}`}
icon="gear"
onClick={addProcess}
disabled={totalsByType.process >= MAX_SELECTORS_AND_RESPONSES_PER_TYPE}
data-test-subj={`cloud-defend-btnAddProcess${type}`}
>
{isSelector ? i18n.processSelector : i18n.processResponse}
</EuiContextMenuItem>,
<EuiContextMenuItem
key={`addNetwork${type}`}
icon="globe"
disabled
data-test-subj={`cloud-defend-btnAddNetwork${type}`}
>
{isSelector ? i18n.networkSelector : i18n.networkResponse}
</EuiContextMenuItem>,
];
return (
<EuiPopover
id={`btnAdd${type}`}
display="block"
button={
<EuiButton
fullWidth
color="primary"
iconType="plusInCircle"
onClick={onButtonClick}
data-test-subj={`cloud-defend-btnAdd${type}`}
>
{isSelector ? i18n.addSelector : i18n.addResponse}
</EuiButton>
}
isOpen={isPopoverOpen}
closePopover={closePopover}
panelPaddingSize="none"
anchorPosition="downLeft"
>
<EuiContextMenuPanel size="s" items={items} />
</EuiPopover>
);
};
export const ControlGeneralView = ({ policy, onChange, show }: ViewDeps) => {
const styles = useStyles();
const input = getInputFromPolicy(policy, INPUT_CONTROL);
const configuration = input?.vars?.configuration?.value || '';
const { selectors, responses } = useMemo(() => {
return getSelectorsAndResponsesFromYaml(configuration);
}, [configuration]);
const onUpdateYaml = useCallback(
(newSelectors: Selector[], newResponses: Response[]) => {
if (input?.vars?.configuration) {
const isValid =
!newSelectors.find((selector) => selector.hasErrors) &&
!newResponses.find((response) => response.hasErrors);
input.vars.configuration.value = getYamlFromSelectorsAndResponses(
newSelectors,
newResponses
);
onChange({ isValid, updatedPolicy: { ...policy } });
}
},
[input?.vars?.configuration, onChange, policy]
);
const incrementName = useCallback(
(name: string): string => {
// increment name using ints
const numberSuffix = name.search(/\d+$/);
const newName =
numberSuffix !== -1
? name.slice(0, numberSuffix) + (parseInt(name.slice(numberSuffix), 10) + 1)
: name + '1';
const dupe = selectors.find((selector) => selector.name === newName);
if (dupe) {
return incrementName(dupe.name);
}
return newName;
},
[selectors]
);
const onAddSelector = useCallback(
(type: SelectorType) => {
const newSelector = getDefaultSelectorByType(type);
const dupe = selectors.find((selector) => selector.name === newSelector.name);
if (dupe) {
newSelector.name = incrementName(dupe.name);
}
selectors.push(newSelector);
onUpdateYaml(selectors, responses);
},
[incrementName, onUpdateYaml, responses, selectors]
);
const onAddResponse = useCallback(
(type: SelectorType) => {
const newResponse = getDefaultResponseByType(type);
responses.push(newResponse);
onUpdateYaml(selectors, responses);
},
[onUpdateYaml, responses, selectors]
);
const onDuplicateSelector = useCallback(
(selector: Selector) => {
const duplicate = JSON.parse(JSON.stringify(selector));
duplicate.name = incrementName(duplicate.name);
selectors.push(duplicate);
onUpdateYaml(selectors, responses);
},
[incrementName, onUpdateYaml, responses, selectors]
);
const onRemoveSelector = useCallback(
(index: number) => {
const oldName = selectors[index].name;
const newSelectors = [...selectors];
newSelectors.splice(index, 1);
// remove reference from all responses
const updatedResponses = responses.map((r) => {
const response = { ...r };
const matchIndex = response.match.indexOf(oldName);
if (matchIndex !== -1) {
response.match.splice(matchIndex, 1);
}
if (response.exclude) {
const excludeIndex = response.exclude.indexOf(oldName);
if (excludeIndex !== -1) {
response.exclude.splice(excludeIndex, 1);
}
}
return response;
});
onUpdateYaml(newSelectors, updatedResponses);
},
[onUpdateYaml, responses, selectors]
);
const onDuplicateResponse = useCallback(
(response: Response) => {
const duplicate = { ...response };
responses.push(duplicate);
onUpdateYaml(selectors, responses);
},
[onUpdateYaml, responses, selectors]
);
const onRemoveResponse = useCallback(
(index: number) => {
const newResponses = [...responses];
newResponses.splice(index, 1);
onUpdateYaml(selectors, newResponses);
},
[onUpdateYaml, responses, selectors]
);
const onSelectorChange = useCallback(
(updatedSelector: Selector, index: number) => {
const old = selectors[index];
if (updatedSelector.hasErrors === false) {
delete updatedSelector.hasErrors;
}
const updatedSelectors: Selector[] = JSON.parse(JSON.stringify(selectors));
let updatedResponses: Response[] = JSON.parse(JSON.stringify(responses));
if (old.name !== updatedSelector.name) {
// update all references to this selector in responses
updatedResponses = responses.map((response) => {
let oldNameIndex = response.match.indexOf(old.name);
if (oldNameIndex !== -1) {
response.match[oldNameIndex] = updatedSelector.name;
}
if (response.exclude) {
oldNameIndex = response.exclude.indexOf(old.name);
if (oldNameIndex !== -1) {
response.exclude[oldNameIndex] = updatedSelector.name;
}
}
return response;
});
}
updatedSelectors[index] = JSON.parse(JSON.stringify(updatedSelector));
onUpdateYaml(updatedSelectors, updatedResponses);
},
[onUpdateYaml, responses, selectors]
);
const onResponseChange = useCallback(
(updatedResponse: Response, index: number) => {
if (updatedResponse.hasErrors === false) {
delete updatedResponse.hasErrors;
}
const updatedResponses: Response[] = JSON.parse(JSON.stringify(responses));
updatedResponses[index] = JSON.parse(JSON.stringify(updatedResponse));
onUpdateYaml(selectors, updatedResponses);
},
[onUpdateYaml, responses, selectors]
);
return (
<EuiFlexGroup
css={!show && styles.hide}
gutterSize="m"
direction="column"
data-test-subj="cloud-defend-generalview"
>
<EuiFlexItem>
<EuiTitle size="xs">
<h4>{i18n.selectors}</h4>
</EuiTitle>
<EuiText color="subdued" size="s">
{i18n.selectorsHelp}
</EuiText>
</EuiFlexItem>
{selectors.map((selector, i) => {
const usedByResponse = !!responses.find(
(response) =>
response.match.includes(selector.name) || response?.exclude?.includes(selector.name)
);
return (
<EuiFlexItem key={i}>
<ControlGeneralViewSelector
key={i}
index={i}
selector={selector}
selectors={selectors}
usedByResponse={usedByResponse}
onDuplicate={onDuplicateSelector}
onRemove={onRemoveSelector}
onChange={onSelectorChange}
/>
</EuiFlexItem>
);
})}
<AddButton
type="Selector"
onSelectType={onAddSelector}
selectors={selectors}
responses={responses}
/>
<EuiSpacer size="m" />
<EuiFlexItem>
<EuiTitle size="xs">
<h4>{i18n.responses}</h4>
</EuiTitle>
<EuiText size="s" color="subdued">
{i18n.responsesHelp}
</EuiText>
</EuiFlexItem>
{responses.map((response, i) => {
return (
<EuiFlexItem key={i}>
<ControlGeneralViewResponse
index={i}
response={response}
responses={responses}
selectors={selectors}
onRemove={onRemoveResponse}
onDuplicate={onDuplicateResponse}
onChange={onResponseChange}
/>
</EuiFlexItem>
);
})}
<AddButton
type="Response"
onSelectType={onAddResponse}
selectors={selectors}
responses={responses}
/>
<EuiSpacer size="m" />
</EuiFlexGroup>
);
};

View file

@ -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 { useMemo } from 'react';
import { CSSObject } from '@emotion/react';
export const useStyles = () => {
return useMemo(() => {
const panel: CSSObject = {
position: 'relative',
};
const draggable: CSSObject = {
// setting manually as the spacing of selectors doesn't match with built in sizes
padding: 0,
paddingBottom: '12px',
};
const hide: CSSObject = {
display: 'none',
};
return { panel, draggable, hide };
}, []);
};

View file

@ -1,241 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { SelectorCondition, SelectorType } from '../../../common';
export const fileSelector = i18n.translate('xpack.cloudDefend.fileSelector', {
defaultMessage: 'File selector',
});
export const processSelector = i18n.translate('xpack.cloudDefend.processSelector', {
defaultMessage: 'Process selector',
});
export const networkSelector = i18n.translate('xpack.cloudDefend.networkSelector', {
defaultMessage: 'Network (coming soon)',
});
export const fileResponse = i18n.translate('xpack.cloudDefend.fileResponse', {
defaultMessage: 'File response',
});
export const processResponse = i18n.translate('xpack.cloudDefend.processResponse', {
defaultMessage: 'Process response',
});
export const networkResponse = i18n.translate('xpack.cloudDefend.networkResponse', {
defaultMessage: 'Network (coming soon)',
});
export const conditions = i18n.translate('xpack.cloudDefend.conditions', {
defaultMessage: 'Conditions: ',
});
export const duplicate = i18n.translate('xpack.cloudDefend.controlDuplicate', {
defaultMessage: 'Duplicate',
});
export const remove = i18n.translate('xpack.cloudDefend.controlRemove', {
defaultMessage: 'Remove',
});
export const selectors = i18n.translate('xpack.cloudDefend.controlSelectors', {
defaultMessage: 'Selectors',
});
export const selectorsHelp = i18n.translate('xpack.cloudDefend.controlSelectorsHelp', {
defaultMessage:
'Create file or process selectors to match on operations and or conditions of interest.',
});
export const responses = i18n.translate('xpack.cloudDefend.controlResponses', {
defaultMessage: 'Responses',
});
export const responsesHelp = i18n.translate('xpack.cloudDefend.controlResponsesHelp', {
defaultMessage:
'Use responses to map one or more selectors to a set of actions. Selectors can also be used to "exclude" events.',
});
export const matchSelectors = i18n.translate('xpack.cloudDefend.controlMatchSelectors', {
defaultMessage: 'Match selectors',
});
export const excludeSelectors = i18n.translate('xpack.cloudDefend.controlExcludeSelectors', {
defaultMessage: 'Exclude selectors',
});
export const exclude = i18n.translate('xpack.cloudDefend.controlExclude', {
defaultMessage: 'Exclude',
});
export const actions = i18n.translate('xpack.cloudDefend.controlResponseActions', {
defaultMessage: 'Actions',
});
export const actionLog = i18n.translate('xpack.cloudDefend.controlResponseActionLog', {
defaultMessage: 'Log',
});
export const actionAlert = i18n.translate('xpack.cloudDefend.controlResponseActionAlert', {
defaultMessage: 'Alert',
});
export const actionBlock = i18n.translate('xpack.cloudDefend.controlResponseActionBlock', {
defaultMessage: 'Block',
});
export const actionBlockHelp = i18n.translate('xpack.cloudDefend.controlResponseActionBlockHelp', {
defaultMessage: 'Alert action must be enabled to block an event.',
});
export const actionAlertAndBlock = i18n.translate(
'xpack.cloudDefend.controlResponseActionAlertAndBlock',
{
defaultMessage: 'Alert and block',
}
);
export const addResponse = i18n.translate('xpack.cloudDefend.addResponse', {
defaultMessage: 'Add response',
});
export const addSelector = i18n.translate('xpack.cloudDefend.addSelector', {
defaultMessage: 'Add selector',
});
export const addSelectorCondition = i18n.translate('xpack.cloudDefend.addSelectorCondition', {
defaultMessage: 'Add condition',
});
export const name = i18n.translate('xpack.cloudDefend.name', {
defaultMessage: 'Name',
});
export const unusedSelector = i18n.translate('xpack.cloudDefend.unusedSelector', {
defaultMessage: 'Not in use',
});
export const unusedSelectorHelp = i18n.translate('xpack.cloudDefend.unusedSelectorHelp', {
defaultMessage: 'This selector is not in use by any response.',
});
export const errorInvalidTargetFilePath = i18n.translate(
'xpack.cloudDefend.errorInvalidTargetFilePath',
{
defaultMessage:
'"Target file path" values must use absolute paths. A trailing * wildcard may be used to match all files in the target directory. Use double ** to match all files recursively. e.g /etc/**',
}
);
export const errorInvalidProcessExecutable = i18n.translate(
'xpack.cloudDefend.errorInvalidProcessExecutable',
{
defaultMessage:
'"Process executable" values must use absolute paths. A trailing * wildcard may be used to match all files in the target directory. Use double ** to match all files recursively. e.g /usr/bin/**',
}
);
export const errorInvalidPodLabel = i18n.translate('xpack.cloudDefend.errorInvalidPodLabel', {
defaultMessage:
'Kubernetes pod label values must have the format: "key:value". A wildcard "*" can be used at the end of the value. e.g. "key:val*". To match on an empty label value, use "key:".',
});
export const errorInvalidFullContainerImageName = i18n.translate(
'xpack.cloudDefend.errorInvalidFullContainerImageName',
{
defaultMessage:
'"Full container image name" values must be in the format: image_repo/image_name e.g. "docker.io/nginx"',
}
);
export const errorConditionRequired = i18n.translate('xpack.cloudDefend.errorConditionRequired', {
defaultMessage: 'At least one condition per selector is required.',
});
export const errorDuplicateName = i18n.translate('xpack.cloudDefend.errorDuplicateName', {
defaultMessage: 'This name is already used by another selector.',
});
export const errorInvalidName = i18n.translate('xpack.cloudDefend.errorInvalidName', {
defaultMessage: 'Selector names must be alphanumeric and contain no spaces.',
});
export const errorValueRequired = i18n.translate('xpack.cloudDefend.errorValueRequired', {
defaultMessage: 'At least one value is required.',
});
export const errorActionRequired = i18n.translate('xpack.cloudDefend.errorActionRequired', {
defaultMessage: 'At least one action is required.',
});
export const errorBlockActionRequiresTargetFilePath = i18n.translate(
'xpack.cloudDefend.errorBlockActionRequiresTargetFilePath',
{
defaultMessage:
'The "block" action requires targetFilePath be included in all "match" selectors using FIM operations or in at least one "exclude" selector. Note that selectors without operation will match on all operations, including createFile, modifyFile or deleteFile',
}
);
export const warningFIMUsingSlashStarStarTitle = i18n.translate(
'xpack.cloudDefend.warningFIMUsingSlashStarStarTitle',
{
defaultMessage: 'Warning: Blocking FIM operations',
}
);
export const warningFIMUsingSlashStarStarText = i18n.translate(
'xpack.cloudDefend.warningFIMUsingSlashStarStarText',
{
defaultMessage:
'It is dangerous to block FIM operations using a targetFilePath of /**. This can lead to system instability. Note that selectors without operation will match on all operations, including createFile, modifyFile or deleteFile',
}
);
export const getSelectorIconTooltip = (type: SelectorType) => {
switch (type) {
case 'process':
return i18n.translate('xpack.cloudDefend.processSelectorIconTooltip', {
defaultMessage: 'A process selector. Matches only on process operations.',
});
case 'file':
default:
return i18n.translate('xpack.cloudDefend.fileSelectorIconTooltip', {
defaultMessage: 'A file selector. Matches only on file operations.',
});
}
};
export const getResponseIconTooltip = (type: SelectorType) => {
switch (type) {
case 'process':
return i18n.translate('xpack.cloudDefend.processResponseIconTooltip', {
defaultMessage: 'A process response.\nOnly process selectors can be used to match/exclude.',
});
case 'file':
default:
return i18n.translate('xpack.cloudDefend.fileResponseIconTooltip', {
defaultMessage: 'A file response.\nOnly file selectors can be used to match/exclude.',
});
}
};
export const getConditionHelpLabel = (prop: SelectorCondition) => {
switch (prop) {
case 'ignoreVolumeMounts':
return i18n.translate('xpack.cloudDefend.ignoreVolumeMountsHelp', {
defaultMessage: 'Ignore operations on all volume mounts.',
});
case 'ignoreVolumeFiles':
return i18n.translate('xpack.cloudDefend.ignoreVolumeFilesHelp', {
defaultMessage:
'Ignore operations on file mounts only. e.g mounted files, configMaps, secrets etc...',
});
default:
return '';
}
};

View file

@ -1,273 +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 { render, waitFor } from '@testing-library/react';
import { coreMock } from '@kbn/core/public/mocks';
import userEvent from '@testing-library/user-event';
import { showEuiComboBoxOptions } from '@elastic/eui/lib/test/rtl';
import { TestProvider } from '../../test/test_provider';
import { ControlGeneralViewResponse } from '.';
import { Response, Selector } from '../../../common';
import * as i18n from '../control_general_view/translations';
describe('<ControlGeneralViewSelector />', () => {
const onChange = jest.fn();
const onRemove = jest.fn();
const onDuplicate = jest.fn();
// defining this here to avoid a warning in testprovider with params.history changing on rerender.
const params = coreMock.createAppMountParameters();
const mockSelector: Selector = {
type: 'file',
name: 'mock',
operation: ['createFile'],
};
const mockSelector2: Selector = {
type: 'file',
name: 'mock2',
operation: ['modifyExecutable'],
};
const mockSelector3: Selector = {
type: 'file',
name: 'mock3',
operation: ['createFile'],
targetFilePath: ['/**'],
};
const mockExclude: Selector = {
type: 'file',
name: 'mockExclude',
containerImageName: ['nginx'],
};
const mockProcessSelector: Selector = {
type: 'process',
name: 'mockProcess',
operation: ['exec'],
};
const mockResponse: Response = {
type: 'file',
match: [mockSelector.name],
actions: ['alert'],
};
const mockResponse2: Response = {
type: 'file',
match: [mockSelector.name],
actions: ['alert', 'block'],
};
const mockResponse3: Response = {
type: 'file',
match: [mockSelector3.name],
actions: ['alert', 'block'],
};
const mockProcessResponse: Response = {
type: 'process',
match: [mockProcessSelector.name],
actions: ['alert', 'block'],
};
const WrappedComponent = ({
response = { ...mockResponse },
responses,
}: {
response?: Response;
responses?: Response[];
}) => {
return (
<TestProvider params={params}>
<ControlGeneralViewResponse
index={0}
selectors={[mockSelector, mockSelector2, mockSelector3, mockExclude]}
response={response}
responses={responses || [response, mockResponse2, mockResponse3]}
onChange={onChange}
onRemove={onRemove}
onDuplicate={onDuplicate}
/>
</TestProvider>
);
};
beforeEach(() => {
onChange.mockClear();
onRemove.mockClear();
onDuplicate.mockClear();
});
it('renders a response that matches a selector and has alert action enabled', () => {
const { getByTestId, queryByTestId } = render(<WrappedComponent />);
expect(getByTestId('cloud-defend-responsematch').querySelector('.euiBadge__text')).toBeTruthy();
expect(queryByTestId('cloud-defend-responseexclude')).toBeFalsy();
expect(getByTestId('cloud-defend-chkalertaction')).toBeChecked();
expect(getByTestId('cloud-defend-chkblockaction')).not.toBeChecked();
});
it('allows the user to add more selectors to match on', async () => {
const { getByTestId, rerender } = render(<WrappedComponent />);
await showEuiComboBoxOptions();
const options = getByTestId(
'comboBoxOptionsList cloud-defend-responsematch-optionsList'
).querySelectorAll('.euiComboBoxOption__content');
expect(options).toHaveLength(3);
expect(options[0].textContent).toBe('mock2');
await userEvent.click(options[0]);
const updatedResponse: Response = onChange.mock.calls[0][0];
rerender(<WrappedComponent response={updatedResponse} />);
expect(updatedResponse.match).toContain('mock');
expect(updatedResponse.match).toContain('mock2');
// test that 1 option remains
const updatedOptions = getByTestId(
'comboBoxOptionsList cloud-defend-responsematch-optionsList'
).querySelectorAll('.euiComboBoxOption__content');
expect(updatedOptions).toHaveLength(2);
expect(updatedOptions[0].textContent).toContain('mock3');
expect(updatedOptions[1].textContent).toContain('mockExclude');
});
it('ensures there is at least 1 selector to match', async () => {
const { getByText, getByTitle, rerender } = render(<WrappedComponent />);
await userEvent.click(getByTitle('Remove mock from selection in this group'));
const updatedResponse: Response = onChange.mock.calls[0][0];
rerender(<WrappedComponent response={updatedResponse} />);
expect(getByText(i18n.errorValueRequired)).toBeTruthy();
});
it('allows the user to exclude selectors', async () => {
const { getByTestId, getAllByTestId, rerender } = render(<WrappedComponent />);
// first must click button to show combobox
await userEvent.click(getByTestId('cloud-defend-btnshowexclude'));
let updatedResponse: Response = onChange.mock.calls[0][0];
rerender(<WrappedComponent response={updatedResponse} />);
getAllByTestId('comboBoxSearchInput')[1].focus();
let options = await waitFor(() =>
getByTestId('comboBoxOptionsList cloud-defend-responseexclude-optionsList').querySelectorAll(
'.euiComboBoxOption__content'
)
);
expect(options).toHaveLength(3);
expect(options[0].textContent).toBe('mock2');
expect(options[1].textContent).toBe('mock3');
expect(options[2].textContent).toBe('mockExclude');
await userEvent.click(options[2]);
updatedResponse = onChange.mock.calls[0][0];
rerender(<WrappedComponent response={updatedResponse} />);
expect(updatedResponse.exclude).toContain('mockExclude');
// focus 'match' input box, lets ensure selectors can't be re-used across 'match' and 'exclude' fields
getAllByTestId('comboBoxSearchInput')[0].focus();
options = await waitFor(() =>
getByTestId('comboBoxOptionsList cloud-defend-responsematch-optionsList').querySelectorAll(
'.euiComboBoxOption__content'
)
);
expect(options).toHaveLength(2);
expect(options[0].textContent).toBe('mock2');
});
it('allows the user to enable block action (which should force alert action on)', async () => {
const { getByTestId } = render(<WrappedComponent />);
const checkBox = getByTestId('cloud-defend-chkblockaction');
if (checkBox) {
await userEvent.click(checkBox);
}
const response: Response = onChange.mock.calls[0][0];
expect(response.actions).toContain('alert');
expect(response.actions).toContain('block');
});
it('doesnt show block action for process responses', () => {
const { findByTestId } = render(<WrappedComponent response={mockProcessResponse} />);
const checkBox = findByTestId('cloud-defend-chkblockaction');
expect(checkBox).toMatchObject({});
});
it('shows an error if user is using block w/o targetFilePath', () => {
const { getByText } = render(<WrappedComponent response={mockResponse2} />);
expect(getByText(i18n.errorBlockActionRequiresTargetFilePath)).toBeTruthy();
});
it('shows a warning if user is using block w targetFilePath /**', () => {
const { getByText } = render(<WrappedComponent response={mockResponse3} />);
expect(getByText(i18n.warningFIMUsingSlashStarStarTitle)).toBeTruthy();
});
it('allows the user to remove the response', async () => {
const { getByTestId } = render(<WrappedComponent />);
const btnPopover = getByTestId('cloud-defend-btnresponsepopover');
await userEvent.click(btnPopover);
await userEvent.click(getByTestId('cloud-defend-btndeleteresponse'));
expect(onRemove.mock.calls).toHaveLength(1);
expect(onRemove.mock.calls[0][0]).toEqual(0);
});
it('prevents the last response from being removed', async () => {
const { getByTestId } = render(<WrappedComponent responses={[mockResponse]} />);
const btnPopover = getByTestId('cloud-defend-btnresponsepopover');
await userEvent.click(btnPopover);
await userEvent.click(getByTestId('cloud-defend-btndeleteresponse'));
expect(onRemove.mock.calls).toHaveLength(0);
});
it('allows the user to duplicate the response', async () => {
const { getByTestId } = render(<WrappedComponent />);
const btnPopover = getByTestId('cloud-defend-btnresponsepopover');
await userEvent.click(btnPopover);
await userEvent.click(getByTestId('cloud-defend-btnduplicateresponse'));
expect(onDuplicate.mock.calls).toHaveLength(1);
expect(onDuplicate.mock.calls[0][0]).toEqual(mockResponse);
});
it('shows an error if no actions specified', async () => {
const { getByTestId, getByText, rerender } = render(<WrappedComponent />);
const checkBox = getByTestId('cloud-defend-chkalertaction');
if (checkBox) {
await userEvent.click(checkBox);
}
const updatedResponse = onChange.mock.calls[0][0];
rerender(<WrappedComponent response={updatedResponse} />);
expect(getByText(i18n.errorActionRequired)).toBeTruthy();
expect(updatedResponse.hasErrors).toBeTruthy();
});
});

View file

@ -1,419 +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, useState, useCallback, ChangeEvent, useEffect } from 'react';
import {
EuiCallOut,
EuiIcon,
EuiToolTip,
EuiText,
EuiBadge,
EuiAccordion,
EuiFlexGroup,
EuiFlexItem,
EuiButtonIcon,
EuiButtonEmpty,
EuiPopover,
EuiContextMenuPanel,
EuiContextMenuItem,
EuiForm,
EuiFormRow,
EuiComboBox,
EuiCheckbox,
EuiComboBoxOptionOption,
EuiSpacer,
useEuiTheme,
} from '@elastic/eui';
import { useStyles } from './styles';
import { useStyles as useSelectorStyles } from '../control_general_view_selector/styles';
import { ControlGeneralViewResponseDeps, ControlFormErrorMap } from '../../types';
import { Response, ResponseAction } from '../../../common';
import * as i18n from '../control_general_view/translations';
import {
getSelectorTypeIcon,
validateBlockRestrictions,
selectorsIncludeConditionsForFIMOperationsUsingSlashStarStar,
} from '../../common/utils';
// max number of names to show in title (in collapsed state)
// selectorA, selectorB, selectorC, selectorD [+5]
const titleThreshold = 4;
const titleThresholdCollapsed = 2;
const ACTION_ID_REGEX = /response_\d+_(.*)/;
export const ControlGeneralViewResponse = ({
response,
selectors,
responses,
index,
onRemove,
onDuplicate,
onChange,
}: ControlGeneralViewResponseDeps) => {
const {
euiTheme: { colors },
} = useEuiTheme();
const [isPopoverOpen, setPopoverOpen] = useState(false);
const styles = useStyles();
const selectorStyles = useSelectorStyles();
const [accordionState, setAccordionState] = useState<'open' | 'closed'>(
responses.length - 1 === index ? 'open' : 'closed'
);
const logSelected = response.actions?.includes('log');
const alertSelected = response.actions?.includes('alert');
const blockSelected = response.actions?.includes('block');
const warnFIMUsingSlashStarStar = useMemo(
() =>
blockSelected &&
selectorsIncludeConditionsForFIMOperationsUsingSlashStarStar(selectors, response.match),
[blockSelected, response.match, selectors]
);
const errors = useMemo(() => {
const errs: ControlFormErrorMap = {};
if (response.match.length === 0) {
errs.match = [i18n.errorValueRequired];
}
if (response.actions?.length === 0) {
errs.actions = [i18n.errorActionRequired];
}
if (blockSelected) {
const blockErrors = validateBlockRestrictions(selectors, [response]);
if (blockErrors.length > 0) {
errs.response = blockErrors;
}
}
return errs;
}, [response, selectors, blockSelected]);
const errorList = useMemo(() => Object.values(errors), [errors]);
const onResponseChange = useCallback(
(resp: Response, i: number) => {
if (errorList.length) {
resp.hasErrors = true;
}
onChange(resp, i);
},
[errorList.length, onChange]
);
useEffect(() => {
const hasErrors = errorList.length > 0;
const changed = (hasErrors && !response.hasErrors) || (!hasErrors && response.hasErrors);
if (changed) {
response.hasErrors = hasErrors;
onChange(response, index);
}
}, [errorList.length, index, onChange, response]);
const onTogglePopover = useCallback(() => {
setPopoverOpen(!isPopoverOpen);
}, [isPopoverOpen]);
const closePopover = useCallback(() => {
setPopoverOpen(false);
}, []);
const onRemoveClicked = useCallback(() => {
onRemove(index);
closePopover();
}, [closePopover, index, onRemove]);
const onDuplicateClicked = useCallback(() => {
onDuplicate(response);
closePopover();
}, [closePopover, onDuplicate, response]);
const onChangeMatches = useCallback(
(options: any) => {
response.match = options.map((option: EuiComboBoxOptionOption) => option.value);
onResponseChange(response, index);
},
[index, onResponseChange, response]
);
const onChangeExcludes = useCallback(
(options: any) => {
response.exclude = options.map((option: EuiComboBoxOptionOption) => option.value);
if (response.exclude?.length === 0) {
delete response.exclude;
}
onResponseChange(response, index);
},
[index, onResponseChange, response]
);
const selectorOptions = useMemo(() => {
return selectors
.filter(
(selector) =>
!(
selector.type !== response.type ||
response.match.includes(selector.name) ||
response.exclude?.includes(selector.name)
)
)
.map((selector) => ({ label: selector.name, value: selector.name }));
}, [response.exclude, response.match, response.type, selectors]);
const selectedMatches = useMemo(
() =>
response.match.map((selector) => ({
label: selector as unknown as string,
value: selector as unknown as string,
color: colors.backgroundLightSuccess,
})),
[response.match, colors]
);
const selectedExcludes = useMemo(
() =>
response.exclude &&
response.exclude.map((selector) => ({
label: selector as unknown as string,
value: selector as unknown as string,
color: colors.backgroundLightWarning,
})),
[response.exclude, colors]
);
const onShowExclude = useCallback(() => {
const updatedResponse = { ...response };
updatedResponse.exclude = [];
onResponseChange(updatedResponse, index);
}, [index, onResponseChange, response]);
const onToggleAction = useCallback(
(e: ChangeEvent) => {
const action = e.currentTarget?.id?.match(ACTION_ID_REGEX)?.[1] as ResponseAction;
const updatedResponse = JSON.parse(JSON.stringify(response));
const actionIndex = updatedResponse.actions.indexOf(action);
if (actionIndex === -1) {
updatedResponse.actions.push(action);
} else {
// if alert action gets disabled, disable block action
if (action === 'alert') {
const blockIndex = updatedResponse.actions.indexOf('block');
if (blockIndex !== -1) {
updatedResponse.actions.splice(blockIndex, 1);
}
}
updatedResponse.actions.splice(actionIndex, 1);
}
onResponseChange(updatedResponse, index);
},
[index, onResponseChange, response]
);
const onToggleAccordion = useCallback((isOpen: boolean) => {
setAccordionState(isOpen ? 'open' : 'closed');
}, []);
const { title, plusCount, remainingNames } = useMemo(() => {
if (accordionState === 'open') {
return {
title: response.match.slice(0, titleThreshold).join(', '),
plusCount: response.match.length - titleThreshold,
remainingNames: response.match.slice(titleThreshold).join(','),
};
}
return {
title: response.match.slice(0, titleThresholdCollapsed).join(', '),
plusCount: response.match.length - titleThresholdCollapsed,
remainingNames: response.match.slice(titleThresholdCollapsed).join(','),
};
}, [accordionState, response.match]);
return (
<EuiAccordion
id={'response_' + index}
forceState={accordionState}
onToggle={onToggleAccordion}
data-test-subj={`cloud-defend-${response.type}-response`}
paddingSize="m"
buttonContent={
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiToolTip content={i18n.getResponseIconTooltip(response.type)}>
<EuiIcon color="primary" type={getSelectorTypeIcon(response.type)} />
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="s" css={styles.accordionHeader}>
<b>{title}</b>
{plusCount > 0 && <EuiBadge title={remainingNames}>+{plusCount}</EuiBadge>}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
}
css={styles.accordion}
initialIsOpen={index === 0}
extraAction={
<EuiFlexGroup alignItems="center" gutterSize="none" wrap={false}>
{accordionState === 'closed' && (
<EuiText color="subdued" css={selectorStyles.conditionsBadge} size="xs">
{response?.exclude?.length && (
<>
<b>{i18n.exclude}: </b>
<EuiBadge title={response.exclude.join(',')} color="hollow">
{response.exclude.length}
</EuiBadge>
<div css={selectorStyles.verticalDivider} />
</>
)}
<b>{i18n.actions}: </b>
{response.actions?.map((action, i) => (
<span key={action}>
<b css={{ color: action === 'block' ? colors.danger : colors.ink }}>
{action[0].toUpperCase() + action.slice(1)}
</b>
{i !== (response.actions?.length || 0) - 1 && ', '}
</span>
))}
<div css={selectorStyles.verticalDivider} />
</EuiText>
)}
<EuiFlexItem>
<EuiPopover
button={
<EuiButtonIcon
iconType="boxesHorizontal"
onClick={onTogglePopover}
aria-label="Response options"
data-test-subj="cloud-defend-btnresponsepopover"
/>
}
isOpen={isPopoverOpen}
closePopover={closePopover}
panelPaddingSize="none"
anchorPosition="downLeft"
>
<EuiContextMenuPanel
size="s"
items={[
<EuiContextMenuItem
key="duplicate"
icon="copy"
onClick={onDuplicateClicked}
data-test-subj="cloud-defend-btnduplicateresponse"
>
{i18n.duplicate}
</EuiContextMenuItem>,
<EuiContextMenuItem
key="remove"
icon="trash"
disabled={responses.length < 2}
onClick={onRemoveClicked}
data-test-subj="cloud-defend-btndeleteresponse"
>
{i18n.remove}
</EuiContextMenuItem>,
]}
/>
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
}
>
<EuiForm component="form" fullWidth error={errorList} isInvalid={errorList.length > 0}>
{warnFIMUsingSlashStarStar && (
<EuiFormRow fullWidth>
<EuiCallOut color="warning" title={i18n.warningFIMUsingSlashStarStarTitle}>
<p>{i18n.warningFIMUsingSlashStarStarText}</p>
</EuiCallOut>
</EuiFormRow>
)}
<EuiFormRow label={i18n.matchSelectors} fullWidth isInvalid={!!errors.match}>
<EuiComboBox
aria-label={i18n.matchSelectors}
fullWidth
selectedOptions={selectedMatches}
options={selectorOptions}
isClearable={true}
onChange={onChangeMatches}
data-test-subj="cloud-defend-responsematch"
/>
</EuiFormRow>
{response.exclude && (
<EuiFormRow label={i18n.excludeSelectors} fullWidth>
<EuiComboBox
aria-label={i18n.excludeSelectors}
fullWidth
selectedOptions={selectedExcludes}
options={selectorOptions}
onChange={onChangeExcludes}
isClearable={true}
data-test-subj="cloud-defend-responseexclude"
/>
</EuiFormRow>
)}
<EuiSpacer size="s" />
{!response.exclude && (
<EuiButtonEmpty
iconType="plusInCircle"
onClick={onShowExclude}
size="xs"
data-test-subj="cloud-defend-btnshowexclude"
>
{i18n.excludeSelectors}
</EuiButtonEmpty>
)}
<EuiSpacer size="m" />
<EuiFormRow label={i18n.actions} fullWidth isInvalid={!!errors.actions}>
<EuiFlexGroup direction="row" gutterSize="l">
<EuiFlexItem grow={false}>
<EuiCheckbox
id={`response_${index}_log`}
data-test-subj="cloud-defend-chklogaction"
label={i18n.actionLog}
checked={logSelected}
onChange={onToggleAction}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiCheckbox
id={`response_${index}_alert`}
data-test-subj="cloud-defend-chkalertaction"
label={i18n.actionAlert}
checked={alertSelected}
onChange={onToggleAction}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip content={i18n.actionBlockHelp}>
<EuiCheckbox
id={`response_${index}_block`}
data-test-subj="cloud-defend-chkblockaction"
label={i18n.actionBlock}
checked={blockSelected}
onChange={onToggleAction}
disabled={!alertSelected}
/>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
</EuiForm>
</EuiAccordion>
);
};

View file

@ -1,44 +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 { useMemo } from 'react';
import { CSSObject } from '@emotion/react';
import { useEuiTheme, useEuiBackgroundColor } from '@elastic/eui';
export const useStyles = () => {
const { euiTheme } = useEuiTheme();
const { border, size } = euiTheme;
const accordionColor = useEuiBackgroundColor('subdued');
return useMemo(() => {
const options: CSSObject = {
position: 'absolute',
top: size.m,
right: size.m,
};
const accordion: CSSObject = {
borderRadius: border.radius.medium,
'> .euiAccordion__triggerWrapper': {
padding: size.m,
},
backgroundColor: accordionColor,
};
const accordionHeader: CSSObject = {
'> *': {
display: 'inline-block',
verticalAlign: 'middle',
},
'> b': {
marginRight: size.s,
},
};
return { options, accordion, accordionHeader };
}, [accordionColor, border.radius.medium, size.m, size.s]);
};

View file

@ -1,702 +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 { render, fireEvent, within } from '@testing-library/react';
import { showEuiComboBoxOptions } from '@elastic/eui/lib/test/rtl';
import { coreMock } from '@kbn/core/public/mocks';
import userEvent from '@testing-library/user-event';
import { TestProvider } from '../../test/test_provider';
import { ControlGeneralViewSelector } from '.';
import { Selector } from '../../../common';
import { getSelectorConditions } from '../../common/utils';
import * as i18n from '../control_general_view/translations';
describe('<ControlGeneralViewSelector />', () => {
const onChange = jest.fn();
const onRemove = jest.fn();
const onDuplicate = jest.fn();
// defining this here to avoid a warning in testprovider with params.history changing on rerender.
const params = coreMock.createAppMountParameters();
const mockFileSelector: Selector = {
type: 'file',
name: 'mockFile',
operation: ['createExecutable'],
};
const mockFileSelector2: Selector = {
type: 'file',
name: 'mockFile2',
operation: ['createExecutable', 'modifyExecutable'],
};
const mockProcessSelector: Selector = {
type: 'process',
name: 'mockProcess',
operation: ['exec'],
};
const mockProcessSelector2: Selector = {
type: 'process',
name: 'mockProcess2',
operation: [],
};
const WrappedComponent = ({
selector = { ...mockFileSelector },
selectors,
}: {
selector?: Selector;
selectors?: Selector[];
}) => {
return (
<TestProvider params={params}>
<ControlGeneralViewSelector
selectors={selectors || [selector, { ...mockFileSelector2 }]}
selector={selector}
index={0}
onChange={onChange}
onRemove={onRemove}
onDuplicate={onDuplicate}
usedByResponse={false}
/>
</TestProvider>
);
};
beforeEach(() => {
jest.useFakeTimers();
onChange.mockClear();
onRemove.mockClear();
onDuplicate.mockClear();
});
afterEach(() => {
jest.useRealTimers();
});
it('by default has name and operation fields added', () => {
const { getByTestId } = render(<WrappedComponent />);
expect(getByTestId('cloud-defend-selectorcondition-name')).toBeTruthy();
expect(getByTestId('cloud-defend-selectorcondition-operation')).toBeTruthy();
});
it('allows the user to change a selector name', () => {
const { getByTestId } = render(<WrappedComponent />);
const input = getByTestId('cloud-defend-selectorcondition-name');
input.focus();
fireEvent.change(input, { target: { value: 'newName' } });
const updatedSelector: Selector = onChange.mock.calls[0][0];
expect(updatedSelector.name).toEqual('newName');
});
it('renders a badge to show that the selector is unused', () => {
const { getByText } = render(<WrappedComponent />);
expect(getByText(i18n.unusedSelector)).toBeTruthy();
});
it('allows the user to add a limited set of file operations', async () => {
// Workaround for timeout via https://github.com/testing-library/user-event/issues/833#issuecomment-1171452841
const user = userEvent.setup({
advanceTimers: jest.advanceTimersByTime,
pointerEventsCheck: 0,
});
const { getByTestId, rerender } = render(<WrappedComponent />);
await user.click(getByTestId('cloud-defend-selectorcondition-operation'));
await showEuiComboBoxOptions();
const options = getByTestId(
'comboBoxOptionsList cloud-defend-selectorcondition-operation-optionsList'
).querySelectorAll('.euiComboBoxOption__content');
expect(options).toHaveLength(4);
expect(options[0].textContent).toBe('modifyExecutable');
expect(options[1].textContent).toBe('createFile');
expect(options[2].textContent).toBe('modifyFile');
expect(options[3].textContent).toBe('deleteFile');
await user.click(options[3]); // select deleteFile
const updatedSelector: Selector = onChange.mock.calls[0][0];
rerender(<WrappedComponent selector={updatedSelector} />);
expect(updatedSelector.operation).toContain('deleteFile');
// test that only 3 option is remaining
const updatedOptions = getByTestId(
'comboBoxOptionsList cloud-defend-selectorcondition-operation-optionsList'
).querySelectorAll('.euiComboBoxOption__content');
expect(updatedOptions).toHaveLength(3);
});
it('allows the user to add a limited set of process operations', async () => {
// Workaround for timeout via https://github.com/testing-library/user-event/issues/833#issuecomment-1171452841
const user = userEvent.setup({
advanceTimers: jest.advanceTimersByTime,
pointerEventsCheck: 0,
});
const { getByTestId, rerender } = render(<WrappedComponent selector={mockProcessSelector2} />);
await user.click(getByTestId('cloud-defend-selectorcondition-operation'));
await showEuiComboBoxOptions();
const options = getByTestId(
'comboBoxOptionsList cloud-defend-selectorcondition-operation-optionsList'
).querySelectorAll('.euiComboBoxOption__content');
expect(options).toHaveLength(2);
expect(options[0].textContent).toBe('fork');
expect(options[1].textContent).toBe('exec');
await user.click(options[1]); // select exec
const updatedSelector: Selector = onChange.mock.calls[0][0];
rerender(<WrappedComponent selector={updatedSelector} />);
expect(updatedSelector.operation).toContain('exec');
// test that only 1 option is remaining
const updatedOptions = getByTestId(
'comboBoxOptionsList cloud-defend-selectorcondition-operation-optionsList'
).querySelectorAll('.euiComboBoxOption__content');
expect(updatedOptions).toHaveLength(1);
});
it('allows the user add additional conditions', async () => {
// Workaround for timeout via https://github.com/testing-library/user-event/issues/833#issuecomment-1171452841
const user = userEvent.setup({
advanceTimers: jest.advanceTimersByTime,
pointerEventsCheck: 0,
});
const { getByTestId, rerender } = render(<WrappedComponent />);
const addConditionBtn = getByTestId('cloud-defend-btnaddselectorcondition');
await user.click(addConditionBtn);
const options = document.querySelectorAll('.euiContextMenuItem');
const conditions = getSelectorConditions('file');
expect(options).toHaveLength(conditions.length - 1); // -1 since operation is already present
await user.click(options[1]); // add second option "containerImageName"
// rerender and check that containerImageName is not in the list anymore
const updatedSelector: Selector = { ...onChange.mock.calls[0][0] };
rerender(<WrappedComponent selector={updatedSelector} />);
expect(updatedSelector.containerImageName).toHaveLength(0);
await user.click(addConditionBtn);
const updatedOptions = document.querySelectorAll('.euiContextMenuItem');
expect(updatedOptions).toHaveLength(conditions.length - 2); // since operation and containerImageName are already selected
expect(updatedOptions[0]).not.toHaveTextContent('containerImageName');
});
it('allows the user add boolean type conditions', async () => {
// Workaround for timeout via https://github.com/testing-library/user-event/issues/833#issuecomment-1171452841
const user = userEvent.setup({
advanceTimers: jest.advanceTimersByTime,
pointerEventsCheck: 0,
});
const { getByTestId, rerender } = render(<WrappedComponent />);
await user.click(getByTestId('cloud-defend-btnaddselectorcondition'));
await user.click(getByTestId('cloud-defend-addmenu-ignoreVolumeMounts'));
const updatedSelector: Selector = { ...onChange.mock.calls[0][0] };
rerender(<WrappedComponent selector={updatedSelector} />);
expect(updatedSelector.ignoreVolumeMounts).toBeTruthy();
});
it('shows an error if no conditions are added', async () => {
// Workaround for timeout via https://github.com/testing-library/user-event/issues/833#issuecomment-1171452841
const user = userEvent.setup({
advanceTimers: jest.advanceTimersByTime,
pointerEventsCheck: 0,
});
const { getByText, getByTestId, rerender } = render(<WrappedComponent />);
await user.click(getByTestId('cloud-defend-btnremovecondition-operation'));
const updatedSelector: Selector = { ...onChange.mock.calls[0][0] };
rerender(<WrappedComponent selector={updatedSelector} />);
expect(getByText(i18n.errorConditionRequired)).toBeTruthy();
expect(onChange.mock.calls[0][0]).toHaveProperty('hasErrors');
});
it('shows an error if no values provided for condition', async () => {
// Workaround for timeout via https://github.com/testing-library/user-event/issues/833#issuecomment-1171452841
const user = userEvent.setup({
advanceTimers: jest.advanceTimersByTime,
pointerEventsCheck: 0,
});
const { getByText, getByTestId } = render(<WrappedComponent />);
await user.click(getByTestId('cloud-defend-btnremovecondition-operation'));
await user.click(getByTestId('cloud-defend-btnaddselectorcondition'));
await user.click(getByText('Container image name')); // add containerImageName
expect(onChange.mock.calls).toHaveLength(2);
expect(onChange.mock.calls[1][0]).toHaveProperty('containerImageName');
expect(onChange.mock.calls[1][0]).toHaveProperty('hasErrors');
expect(getByText(i18n.errorValueRequired)).toBeTruthy();
});
it('prevents conditions from having values that exceed MAX_CONDITION_VALUE_LENGTH_BYTES', async () => {
// Workaround for timeout via https://github.com/testing-library/user-event/issues/833#issuecomment-1171452841
const user = userEvent.setup({
advanceTimers: jest.advanceTimersByTime,
pointerEventsCheck: 0,
});
const { getByText, getByTestId, rerender } = render(<WrappedComponent />);
await user.click(getByTestId('cloud-defend-btnaddselectorcondition'));
await user.click(getByText('Container image name')); // add containerImageName
const updatedSelector: Selector = onChange.mock.calls[0][0];
rerender(<WrappedComponent selector={updatedSelector} />);
const el = within(getByTestId('cloud-defend-selectorcondition-containerImageName')).getByTestId(
'comboBoxSearchInput'
);
if (el) {
await user.click(el);
// using paste instead of type here because typing 513 chars is too slow and causes a timeout.
await user.paste(new Array(513).join('a'));
await user.type(el, '{enter}');
} else {
throw new Error("Can't find input");
}
expect(getByText('"containerImageName" values cannot exceed 511 bytes')).toBeTruthy();
});
it('prevents targetFilePath conditions from having values that exceed MAX_FILE_PATH_VALUE_LENGTH_BYTES', async () => {
// Workaround for timeout via https://github.com/testing-library/user-event/issues/833#issuecomment-1171452841
const user = userEvent.setup({
advanceTimers: jest.advanceTimersByTime,
pointerEventsCheck: 0,
});
const { getByText, getByTestId, rerender } = render(<WrappedComponent />);
await user.click(getByTestId('cloud-defend-btnaddselectorcondition'));
await user.click(getByText('Target file path'));
const updatedSelector: Selector = onChange.mock.calls[0][0];
rerender(<WrappedComponent selector={updatedSelector} />);
const el = within(getByTestId('cloud-defend-selectorcondition-targetFilePath')).getByTestId(
'comboBoxSearchInput'
);
if (el) {
await user.click(el);
// using paste instead of type here because typing 257 chars is too slow and causes a timeout.
await user.paste(new Array(257).join('a'));
await user.type(el, '{enter}');
} else {
throw new Error("Can't find input");
}
expect(getByText('"targetFilePath" values cannot exceed 255 bytes')).toBeInTheDocument();
});
it('validates targetFilePath conditions values', async () => {
// Workaround for timeout via https://github.com/testing-library/user-event/issues/833#issuecomment-1171452841
const user = userEvent.setup({
advanceTimers: jest.advanceTimersByTime,
pointerEventsCheck: 0,
});
const { getByText, getByTestId, queryByText, rerender } = render(<WrappedComponent />);
await user.click(getByTestId('cloud-defend-btnaddselectorcondition'));
await user.click(getByText('Target file path'));
let updatedSelector: Selector = onChange.mock.calls[0][0];
rerender(<WrappedComponent selector={updatedSelector} />);
const el = within(getByTestId('cloud-defend-selectorcondition-targetFilePath')).getByTestId(
'comboBoxSearchInput'
);
const errorStr = i18n.errorInvalidTargetFilePath;
if (el) {
await user.clear(el);
await user.paste('/usr/bin/**');
await user.type(el, '{enter}');
} else {
throw new Error("Can't find input");
}
updatedSelector = onChange.mock.calls[1][0];
expect(updatedSelector.hasErrors).toBeFalsy();
rerender(<WrappedComponent selector={updatedSelector} />);
expect(queryByText(errorStr)).not.toBeInTheDocument();
await user.type(el, '/*{enter}');
updatedSelector = onChange.mock.calls[2][0];
expect(updatedSelector.hasErrors).toBeFalsy();
rerender(<WrappedComponent selector={updatedSelector} />);
expect(queryByText(errorStr)).not.toBeInTheDocument();
await user.type(el, 'badpath{enter}');
updatedSelector = onChange.mock.calls[3][0];
expect(updatedSelector.hasErrors).toBeTruthy();
rerender(<WrappedComponent selector={updatedSelector} />);
expect(getByText(errorStr)).toBeInTheDocument();
await user.type(el, ' {enter}');
updatedSelector = onChange.mock.calls[4][0];
expect(updatedSelector.hasErrors).toBeTruthy();
rerender(<WrappedComponent selector={updatedSelector} />);
expect(getByText('"targetFilePath" values cannot be empty')).toBeInTheDocument();
});
it('validates processExecutable conditions values', async () => {
// Workaround for timeout via https://github.com/testing-library/user-event/issues/833#issuecomment-1171452841
const user = userEvent.setup({
advanceTimers: jest.advanceTimersByTime,
pointerEventsCheck: 0,
});
const { getByText, getByTestId, queryByText, rerender } = render(
<WrappedComponent selector={mockProcessSelector} />
);
await user.click(getByTestId('cloud-defend-btnaddselectorcondition'));
await user.click(getByText('Process executable'));
let updatedSelector: Selector = onChange.mock.calls[0][0];
rerender(<WrappedComponent selector={updatedSelector} />);
const el = within(getByTestId('cloud-defend-selectorcondition-processExecutable')).getByTestId(
'comboBoxSearchInput'
);
const regexError = i18n.errorInvalidProcessExecutable;
if (el) {
await user.clear(el);
await user.paste('/usr/bin/**');
await user.type(el, '{enter}');
} else {
throw new Error("Can't find input");
}
updatedSelector = onChange.mock.calls[1][0];
expect(updatedSelector.hasErrors).toBeFalsy();
rerender(<WrappedComponent selector={updatedSelector} />);
expect(queryByText(regexError)).not.toBeInTheDocument();
await user.clear(el);
await user.paste('/*');
await user.type(el, '{enter}');
updatedSelector = onChange.mock.calls[2][0];
expect(updatedSelector.hasErrors).toBeFalsy();
rerender(<WrappedComponent selector={updatedSelector} />);
expect(queryByText(regexError)).not.toBeInTheDocument();
await user.clear(el);
await user.paste('/usr/bin/ls');
await user.type(el, '{enter}');
updatedSelector = onChange.mock.calls[3][0];
expect(updatedSelector.hasErrors).toBeFalsy();
rerender(<WrappedComponent selector={updatedSelector} />);
expect(queryByText(regexError)).not.toBeInTheDocument();
await user.clear(el);
await user.paste('badpath');
await user.type(el, '{enter}');
updatedSelector = onChange.mock.calls[4][0];
expect(updatedSelector.hasErrors).toBeTruthy();
rerender(<WrappedComponent selector={updatedSelector} />);
expect(getByText(regexError)).toBeInTheDocument();
await user.type(el, ' {enter}');
updatedSelector = onChange.mock.calls[4][0];
expect(updatedSelector.hasErrors).toBeTruthy();
rerender(<WrappedComponent selector={updatedSelector} />);
expect(getByText('"processExecutable" values cannot be empty')).toBeInTheDocument();
});
it('validates containerImageFullName conditions values', async () => {
// Workaround for timeout via https://github.com/testing-library/user-event/issues/833#issuecomment-1171452841
const user = userEvent.setup({
advanceTimers: jest.advanceTimersByTime,
pointerEventsCheck: 0,
});
const { getByText, getByTestId, queryByText, rerender } = render(<WrappedComponent />);
await user.click(getByTestId('cloud-defend-btnaddselectorcondition'));
await user.click(getByText('Container image full name'));
let updatedSelector: Selector = onChange.mock.calls[0][0];
rerender(<WrappedComponent selector={updatedSelector} />);
const el = within(
getByTestId('cloud-defend-selectorcondition-containerImageFullName')
).getByTestId('comboBoxSearchInput');
const regexError = i18n.errorInvalidFullContainerImageName;
if (el) {
await user.clear(el);
await user.paste('docker.io/nginx');
await user.type(el, '{enter}');
await user.clear(el);
await user.paste('docker.io/nginx-dev');
await user.type(el, '{enter}');
await user.clear(el);
await user.paste('docker.io/nginx.dev');
await user.type(el, '{enter}');
await user.clear(el);
await user.paste('docker.io/nginx_dev');
await user.type(el, '{enter}');
} else {
throw new Error("Can't find input");
}
updatedSelector = onChange.mock.calls[1][0];
rerender(<WrappedComponent selector={updatedSelector} />);
expect(queryByText(regexError)).not.toBeInTheDocument();
await user.clear(el);
await user.paste('nginx');
await user.type(el, '{enter}');
updatedSelector = onChange.mock.calls[5][0];
rerender(<WrappedComponent selector={updatedSelector} />);
expect(getByText(regexError)).toBeInTheDocument();
});
it('validates kubernetesPodLabel conditions values', async () => {
// Workaround for timeout via https://github.com/testing-library/user-event/issues/833#issuecomment-1171452841
const user = userEvent.setup({
advanceTimers: jest.advanceTimersByTime,
pointerEventsCheck: 0,
});
const { getByText, getByTestId, queryByText, rerender } = render(<WrappedComponent />);
await user.click(getByTestId('cloud-defend-btnaddselectorcondition'));
await user.click(getByText('Kubernetes pod label'));
let updatedSelector: Selector = onChange.mock.calls[0][0];
rerender(<WrappedComponent selector={updatedSelector} />);
const el = within(getByTestId('cloud-defend-selectorcondition-kubernetesPodLabel')).getByTestId(
'comboBoxSearchInput'
);
const errorStr = i18n.errorInvalidPodLabel;
if (el) {
await user.clear(el);
await user.paste('key1:value1');
await user.type(el, '{enter}');
} else {
throw new Error("Can't find input");
}
updatedSelector = onChange.mock.calls[1][0];
rerender(<WrappedComponent selector={updatedSelector} />);
expect(queryByText(errorStr)).not.toBeInTheDocument();
await user.clear(el);
await user.paste('key1:value*');
await user.type(el, '{enter}');
updatedSelector = onChange.mock.calls[2][0];
rerender(<WrappedComponent selector={updatedSelector} />);
await user.clear(el);
await user.paste('key1*:value');
await user.type(el, '{enter}');
updatedSelector = onChange.mock.calls[3][0];
rerender(<WrappedComponent selector={updatedSelector} />);
await user.clear(el);
await user.type(el, '{backspace}');
await user.paste('key1');
await user.type(el, '{enter}');
updatedSelector = onChange.mock.calls[5][0];
rerender(<WrappedComponent selector={updatedSelector} />);
expect(getByText(errorStr)).toBeTruthy();
});
it('prevents processName conditions from having values that exceed 15 bytes', async () => {
// Workaround for timeout via https://github.com/testing-library/user-event/issues/833#issuecomment-1171452841
const user = userEvent.setup({
advanceTimers: jest.advanceTimersByTime,
pointerEventsCheck: 0,
});
const { getByText, getByTestId, rerender } = render(
<WrappedComponent selector={mockProcessSelector} />
);
await user.click(getByTestId('cloud-defend-btnaddselectorcondition'));
await user.click(getByText('Process name'));
const updatedSelector: Selector = onChange.mock.calls[0][0];
rerender(<WrappedComponent selector={updatedSelector} />);
const el = within(getByTestId('cloud-defend-selectorcondition-processName')).getByTestId(
'comboBoxSearchInput'
);
if (el) {
await user.type(el, new Array(17).join('a') + '{enter}');
} else {
throw new Error("Can't find input");
}
expect(getByText('"processName" values cannot exceed 15 bytes')).toBeInTheDocument();
});
it('shows an error if condition values fail their pattern regex', async () => {
// Workaround for timeout via https://github.com/testing-library/user-event/issues/833#issuecomment-1171452841
const user = userEvent.setup({
advanceTimers: jest.advanceTimersByTime,
pointerEventsCheck: 0,
});
const { getByText, getByTestId, rerender } = render(<WrappedComponent />);
await user.click(getByTestId('cloud-defend-btnaddselectorcondition'));
await user.click(getByText('Container image name')); // add containerImageName
const updatedSelector: Selector = onChange.mock.calls[0][0];
rerender(<WrappedComponent selector={updatedSelector} />);
const el = within(getByTestId('cloud-defend-selectorcondition-containerImageName')).getByTestId(
'comboBoxSearchInput'
);
if (el) {
await user.type(el, 'bad*imagename{enter}');
} else {
throw new Error("Can't find input");
}
const expectedError =
'"containerImageName" values must match the pattern: /^([a-z0-9]+(?:[._-][a-z0-9]+)*)$/';
expect(getByText(expectedError)).toBeTruthy();
});
it('allows the user to remove conditions', async () => {
// Workaround for timeout via https://github.com/testing-library/user-event/issues/833#issuecomment-1171452841
const user = userEvent.setup({
advanceTimers: jest.advanceTimersByTime,
pointerEventsCheck: 0,
});
const selector: Selector = {
type: 'file',
name: 'mock3',
operation: ['createExecutable'],
containerImageTag: ['test'],
};
const { getByTestId } = render(<WrappedComponent selector={selector} />);
await user.click(getByTestId('cloud-defend-btnremovecondition-operation'));
expect(onChange.mock.calls).toHaveLength(1);
expect(onChange.mock.calls[0][0]).not.toHaveProperty('operation');
});
it('allows the user to remove the selector (unless its the last one)', async () => {
// Workaround for timeout via https://github.com/testing-library/user-event/issues/833#issuecomment-1171452841
const user = userEvent.setup({
advanceTimers: jest.advanceTimersByTime,
pointerEventsCheck: 0,
});
const { getByTestId, rerender } = render(<WrappedComponent />);
const btnSelectorPopover = getByTestId('cloud-defend-btnselectorpopover');
await user.click(btnSelectorPopover);
await user.click(getByTestId('cloud-defend-btndeleteselector'));
expect(onRemove.mock.calls).toHaveLength(1);
expect(onRemove.mock.calls[0][0]).toEqual(0);
onRemove.mockClear();
rerender(<WrappedComponent selector={mockFileSelector} selectors={[mockFileSelector]} />);
// try and delete again, and ensure the last selector can't be deleted.
await user.click(btnSelectorPopover);
await user.click(getByTestId('cloud-defend-btndeleteselector'));
expect(onRemove.mock.calls).toHaveLength(0);
});
it('allows the user to expand/collapse selector', async () => {
// Workaround for timeout via https://github.com/testing-library/user-event/issues/833#issuecomment-1171452841
const user = userEvent.setup({
advanceTimers: jest.advanceTimersByTime,
pointerEventsCheck: 0,
});
const { getByText, getByTestId, queryByTestId } = render(<WrappedComponent />);
const selector = getByTestId('cloud-defend-selector');
// should start as closed.
expect(
await within(selector).findAllByRole('button', {
expanded: false,
})
).toHaveLength(1);
expect(
within(selector).queryByRole('button', {
expanded: true,
})
).not.toBeInTheDocument();
const count = getByTestId('cloud-defend-conditions-count');
expect(count).toBeTruthy();
expect(count).toHaveTextContent('1');
expect(count.title).toEqual('operation');
const title = getByText(mockFileSelector.name);
await user.click(title);
expect(
within(selector).queryByRole('button', {
expanded: false,
})
).not.toBeInTheDocument();
expect(
await within(selector).findAllByRole('button', {
expanded: true,
})
).toHaveLength(1);
expect(queryByTestId('cloud-defend-conditions-count')).not.toBeInTheDocument();
});
});

View file

@ -1,557 +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, { useState, useMemo, useCallback, FormEvent } from 'react';
import {
EuiBadge,
EuiIcon,
EuiToolTip,
EuiAccordion,
EuiButtonIcon,
EuiPopover,
EuiContextMenuPanel,
EuiContextMenuItem,
EuiForm,
EuiFormRow,
EuiFieldText,
EuiComboBox,
EuiButtonEmpty,
EuiSpacer,
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiCheckbox,
EuiCheckboxProps,
} from '@elastic/eui';
import { useStyles } from './styles';
import {
ControlGeneralViewSelectorDeps,
ControlFormErrorMap,
SelectorConditionsMap,
} from '../../types';
import { Selector, SelectorCondition } from '../../../common';
import {
getSelectorConditions,
camelToSentenceCase,
getSelectorTypeIcon,
conditionCombinationInvalid,
getRestrictedValuesForCondition,
validateStringValuesForCondition,
} from '../../common/utils';
import * as i18n from '../control_general_view/translations';
import { VALID_SELECTOR_NAME_REGEX, MAX_SELECTOR_NAME_LENGTH } from '../../common/constants';
interface ConditionProps {
label: string;
prop: SelectorCondition;
onRemoveCondition(prop: SelectorCondition): void;
}
interface BooleanConditionProps extends ConditionProps {
selector: Selector;
onChangeBooleanCondition(prop: SelectorCondition, value: boolean): void;
}
interface StringArrayConditionProps extends ConditionProps {
selector: Selector;
errorMap: ControlFormErrorMap;
onAddValueToCondition(prop: SelectorCondition, value: string): void;
onChangeStringArrayCondition(prop: SelectorCondition, value: string[]): void;
}
const BooleanCondition = ({
label,
prop,
selector,
onChangeBooleanCondition,
onRemoveCondition,
}: BooleanConditionProps) => {
const value = selector[prop as keyof Selector] as boolean;
const onChange = useCallback<EuiCheckboxProps['onChange']>(
(e) => {
onChangeBooleanCondition(prop, e.target.checked);
},
[onChangeBooleanCondition, prop]
);
return (
<EuiFormRow label={label} fullWidth={true} key={prop}>
<EuiFlexGroup alignItems="center" gutterSize="m">
<EuiFlexItem>
<EuiCheckbox id={prop} label={label} checked={value} onChange={onChange} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType="cross"
onClick={() => onRemoveCondition(prop)}
aria-label="Remove condition"
data-test-subj={'cloud-defend-btnremovecondition-' + prop}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
);
};
const FlagCondition = ({ label, prop, onRemoveCondition }: ConditionProps) => {
return (
<EuiFormRow label={label} fullWidth={true} key={prop}>
<EuiFlexGroup alignItems="center" gutterSize="m">
<EuiFlexItem>
<EuiText size="s">
<p>
<small>{i18n.getConditionHelpLabel(prop)}</small>
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType="cross"
onClick={() => onRemoveCondition(prop)}
aria-label="Remove condition"
data-test-subj={'cloud-defend-btnremovecondition-' + prop}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
);
};
const StringArrayCondition = ({
label,
prop,
selector,
errorMap,
onRemoveCondition,
onAddValueToCondition,
onChangeStringArrayCondition,
}: StringArrayConditionProps) => {
const values = selector[prop as keyof Selector] as string[];
const selectedOptions =
values?.map((option) => {
return { label: option, value: option };
}) || [];
const restrictedValues = getRestrictedValuesForCondition(selector.type, prop);
return (
<EuiFormRow
label={label}
fullWidth={true}
key={prop}
isInvalid={!!Object.hasOwn(errorMap, prop)}
>
<EuiFlexGroup alignItems="center" gutterSize="m">
<EuiFlexItem>
<EuiComboBox
aria-label={label}
fullWidth={true}
onCreateOption={
!restrictedValues
? (searchValue) => onAddValueToCondition(prop, searchValue)
: undefined
}
selectedOptions={selectedOptions}
options={
restrictedValues
? restrictedValues.map((value: string) => ({ label: value, value }))
: selectedOptions
}
onChange={(options) =>
onChangeStringArrayCondition(prop, options.map((option) => option.value) as string[])
}
isClearable
data-test-subj={'cloud-defend-selectorcondition-' + prop}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType="cross"
onClick={() => onRemoveCondition(prop)}
aria-label="Remove condition"
data-test-subj={'cloud-defend-btnremovecondition-' + prop}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
);
};
/** main component */
export const ControlGeneralViewSelector = ({
selector,
selectors,
usedByResponse,
index,
onRemove,
onDuplicate,
onChange,
}: ControlGeneralViewSelectorDeps) => {
// ensure most recently added is open by default
const [accordionState, setAccordionState] = useState<'open' | 'closed'>(
selectors.length - 1 === index ? 'open' : 'closed'
);
const [isPopoverOpen, setPopoverOpen] = useState(false);
const [isAddConditionOpen, setAddConditionOpen] = useState(false);
const [errorMap, setErrorMap] = useState<ControlFormErrorMap>({});
const styles = useStyles();
const onTogglePopover = useCallback(() => {
setPopoverOpen(!isPopoverOpen);
}, [isPopoverOpen]);
const closePopover = useCallback(() => {
setPopoverOpen(false);
}, []);
const onToggleAddCondition = useCallback(() => {
setAddConditionOpen(!isAddConditionOpen);
}, [isAddConditionOpen]);
const closeAddCondition = useCallback(() => {
setAddConditionOpen(false);
}, []);
const availableConditions = useMemo(() => getSelectorConditions(selector.type), [selector]);
const remainingConditions = useMemo(() => {
return availableConditions.filter((condition) => !Object.hasOwn(selector, condition));
}, [availableConditions, selector]);
const conditionsAdded = useMemo(() => {
return Object.keys(selector).filter(
(key) => !['type', 'hasErrors', 'name'].includes(key)
) as SelectorCondition[];
}, [selector]);
const onRemoveClicked = useCallback(() => {
// we prevent the removal of the last selector to avoid an empty state
if (selectors.length > 1) {
onRemove(index);
}
closePopover();
}, [closePopover, index, onRemove, selectors.length]);
const onDuplicateClicked = useCallback(() => {
onDuplicate(selector);
closePopover();
}, [closePopover, onDuplicate, selector]);
const onNameChange = useCallback(
(event: FormEvent<HTMLInputElement>) => {
const errors: string[] = [];
const value = event.currentTarget.value;
// look for duplicate names (selector names should be unique)
const found = selectors.find((sel) => sel.name === value);
if (found) {
errors.push(i18n.errorDuplicateName);
}
// ensure name is valid
if (!VALID_SELECTOR_NAME_REGEX.test(value)) {
errors.push(i18n.errorInvalidName);
}
if (errors.length) {
errorMap.name = errors;
} else {
delete errorMap.name;
}
setErrorMap({ ...errorMap });
const updatedSelector = { ...selector };
updatedSelector.name = value;
updatedSelector.hasErrors = Object.keys(errorMap).length > 0 || conditionsAdded.length === 0;
onChange(updatedSelector, index);
},
[errorMap, index, conditionsAdded, onChange, selector, selectors]
);
const onChangeStringArrayCondition = useCallback(
(prop: SelectorCondition, values: string[]) => {
const updatedSelector = { ...selector, [prop]: values };
const errors = [];
if (values.length === 0) {
errors.push(i18n.errorValueRequired);
}
const stringValueErrors = validateStringValuesForCondition(prop, values);
if (stringValueErrors.length > 0) {
errors.push(...stringValueErrors);
}
if (errors.length) {
errorMap[prop] = errors;
} else {
delete errorMap[prop];
}
updatedSelector.hasErrors = Object.keys(errorMap).length > 0 || conditionsAdded.length === 0;
setErrorMap({ ...errorMap });
onChange(updatedSelector, index);
},
[errorMap, index, conditionsAdded, onChange, selector]
);
const onChangeBooleanCondition = useCallback(
(prop: string, value: boolean) => {
const updatedSelector = { ...selector, [prop]: value };
onChange(updatedSelector, index);
},
[index, onChange, selector]
);
const onAddCondition = useCallback(
(prop: SelectorCondition) => {
const valueType = SelectorConditionsMap[prop].type;
if (valueType === 'flag' || valueType === 'boolean') {
onChangeBooleanCondition(prop, true);
} else {
onChangeStringArrayCondition(prop, []);
}
closeAddCondition();
},
[closeAddCondition, onChangeBooleanCondition, onChangeStringArrayCondition]
);
const onRemoveCondition = useCallback(
(prop: string) => {
const updatedSelector = { ...selector };
delete (updatedSelector as any)[prop];
delete errorMap[prop];
setErrorMap({ ...errorMap });
updatedSelector.hasErrors = Object.keys(errorMap).length > 0 || conditionsAdded.length === 1;
onChange(updatedSelector, index);
closeAddCondition();
},
[closeAddCondition, conditionsAdded, errorMap, index, onChange, selector]
);
const onAddValueToCondition = useCallback(
(prop: SelectorCondition, searchValue: string) => {
const value = searchValue.trim();
const values = selector[prop as keyof Selector] as string[];
if (values && values.indexOf(value) === -1) {
onChangeStringArrayCondition(prop, [...values, value]);
}
},
[onChangeStringArrayCondition, selector]
);
const errors = useMemo(() => {
const errs = Object.keys(errorMap).reduce<string[]>((prev, current) => {
return prev.concat(errorMap[current]);
}, []);
if (conditionsAdded.length === 0) {
errs.push(i18n.errorConditionRequired);
}
return errs;
}, [errorMap, conditionsAdded]);
const onToggleAccordion = useCallback((isOpen: boolean) => {
setAccordionState(isOpen ? 'open' : 'closed');
}, []);
return (
<EuiAccordion
id={selector.name}
forceState={accordionState}
onToggle={onToggleAccordion}
data-test-subj="cloud-defend-selector"
paddingSize="m"
buttonContent={
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem>
<EuiToolTip content={i18n.getSelectorIconTooltip(selector.type)}>
<EuiIcon color="primary" type={getSelectorTypeIcon(selector.type)} />
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="s">
<b>{selector.name}</b>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
}
css={styles.accordion}
extraAction={
<EuiFlexGroup alignItems="center" gutterSize="none">
<div>
{accordionState === 'closed' && (
<>
<EuiText css={styles.conditionsBadge} size="xs">
<b>{i18n.conditions}</b>
</EuiText>
<EuiBadge
title={conditionsAdded.join(',')}
color="hollow"
data-test-subj="cloud-defend-conditions-count"
>
{conditionsAdded.length}
</EuiBadge>
</>
)}
{!usedByResponse && (
<EuiBadge title={i18n.unusedSelectorHelp} color="warning">
{i18n.unusedSelector}
</EuiBadge>
)}
<div css={styles.verticalDivider} />
</div>
<EuiFlexItem>
<EuiPopover
id={selector.name}
button={
<EuiButtonIcon
iconType="boxesHorizontal"
onClick={onTogglePopover}
aria-label="Selector options"
data-test-subj="cloud-defend-btnselectorpopover"
/>
}
isOpen={isPopoverOpen}
closePopover={closePopover}
panelPaddingSize="none"
anchorPosition="downLeft"
>
<EuiContextMenuPanel
size="s"
items={[
<EuiContextMenuItem
key="duplicate"
icon="copy"
onClick={onDuplicateClicked}
data-test-subj="cloud-defend-btnduplicateselector"
>
{i18n.duplicate}
</EuiContextMenuItem>,
<EuiContextMenuItem
key="remove"
icon="trash"
disabled={selectors.length < 2}
onClick={onRemoveClicked}
data-test-subj="cloud-defend-btndeleteselector"
>
{i18n.remove}
</EuiContextMenuItem>,
]}
/>
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
}
>
<EuiForm component="form" error={errors} isInvalid={errors.length > 0}>
<EuiFormRow
label={i18n.name}
fullWidth={true}
isInvalid={!!Object.hasOwn(errorMap, 'name')}
>
<EuiFieldText
fullWidth={true}
name="name"
value={selector.name}
onChange={onNameChange}
isInvalid={Object.hasOwn(errorMap, 'name')}
data-test-subj="cloud-defend-selectorcondition-name"
maxLength={MAX_SELECTOR_NAME_LENGTH}
/>
</EuiFormRow>
{conditionsAdded.map((prop) => {
const label = camelToSentenceCase(prop);
const valueType = SelectorConditionsMap[prop].type;
if (valueType === 'flag') {
return (
<FlagCondition
key={prop}
label={label}
prop={prop}
onRemoveCondition={onRemoveCondition}
/>
);
} else if (valueType === 'boolean') {
return (
<BooleanCondition
key={prop}
label={label}
selector={selector}
prop={prop}
onChangeBooleanCondition={onChangeBooleanCondition}
onRemoveCondition={onRemoveCondition}
/>
);
} else {
return (
<StringArrayCondition
key={prop}
label={label}
prop={prop}
selector={selector}
errorMap={errorMap}
onAddValueToCondition={onAddValueToCondition}
onChangeStringArrayCondition={onChangeStringArrayCondition}
onRemoveCondition={onRemoveCondition}
/>
);
}
})}
</EuiForm>
<EuiSpacer size="m" />
<EuiPopover
id="cloudDefendControlAddCondition"
data-test-subj="cloud-defend-addconditionpopover"
button={
<EuiButtonEmpty
onClick={onToggleAddCondition}
iconType="plusInCircle"
data-test-subj="cloud-defend-btnaddselectorcondition"
>
{i18n.addSelectorCondition}
</EuiButtonEmpty>
}
isOpen={isAddConditionOpen}
closePopover={closeAddCondition}
panelPaddingSize="none"
anchorPosition="downLeft"
>
<EuiContextMenuPanel
size="s"
items={remainingConditions.map((prop) => {
const label = camelToSentenceCase(prop);
const disabled = conditionCombinationInvalid(conditionsAdded, prop);
return (
<EuiContextMenuItem
data-test-subj={`cloud-defend-addmenu-${prop}`}
key={prop}
onClick={() => onAddCondition(prop)}
disabled={disabled}
>
{label}
</EuiContextMenuItem>
);
})}
/>
</EuiPopover>
</EuiAccordion>
);
};

View file

@ -1,44 +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 { useMemo } from 'react';
import { CSSObject } from '@emotion/react';
import { useEuiTheme } from '@elastic/eui';
export const useStyles = () => {
const { euiTheme } = useEuiTheme();
const { size, border } = euiTheme;
return useMemo(() => {
const accordion: CSSObject = {
borderRadius: border.radius.medium,
border: border.thin,
'> .euiAccordion__triggerWrapper': {
padding: size.m,
},
};
const conditionsBadge: CSSObject = {
display: 'inline',
};
const verticalDivider: CSSObject = {
display: 'inline-block',
verticalAlign: 'middle',
width: '1px',
height: '20px',
border: border.thin,
borderRight: 0,
borderTop: 0,
borderBottom: 0,
marginLeft: size.base,
marginRight: size.base,
};
return { accordion, conditionsBadge, verticalDivider };
}, [border.radius.medium, border.thin, size.base, size.m]);
};

View file

@ -1,70 +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 { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@kbn/code-editor-mock/jest_helper';
import { TestProvider } from '../../test/test_provider';
import { getCloudDefendNewPolicyMock } from '../../test/mocks';
import { ControlSettings } from '.';
import { coreMock } from '@kbn/core/public/mocks';
describe('<ControlSettings />', () => {
const onChange = jest.fn();
// defining this here to avoid a warning in testprovider with params.history changing on rerender.
const params = coreMock.createAppMountParameters();
const WrappedComponent = ({ policy = getCloudDefendNewPolicyMock() }) => {
return (
<TestProvider params={params}>
<ControlSettings policy={policy} onChange={onChange} />;
</TestProvider>
);
};
beforeEach(() => {
onChange.mockClear();
});
it('renders a toggle to switch between yaml and general views', () => {
const { getByTestId } = render(<WrappedComponent />);
let el = getByTestId('cloud-defend-btngeneralview');
expect(el).toBeInTheDocument();
el = getByTestId('cloud-defend-btnyamlview');
expect(el).toBeInTheDocument();
});
it('renders a yaml editor when the user switches to yaml view', async () => {
render(<WrappedComponent />);
await userEvent.click(screen.getByText('YAML view'));
await waitFor(() => expect(screen.getByTestId('mockedCodeEditor')).toBeTruthy());
});
it('renders a friendly UI when the user switches to general view', async () => {
render(<WrappedComponent />);
await userEvent.click(screen.getByText('General view'));
await waitFor(() => expect(screen.findByTestId('cloud-defend-generalview')).toBeTruthy());
});
it('should prevent ability to switch views if there are errors', async () => {
const { rerender, getAllByTestId } = render(<WrappedComponent />);
const btnClear = await waitFor(() => getAllByTestId('comboBoxClearButton')[0]);
await userEvent.click(btnClear);
const updated = onChange.mock.calls[0][0].updatedPolicy;
rerender(<WrappedComponent policy={updated} />);
expect(screen.getByTestId('cloud-defend-btngeneralview')).toBeDisabled();
expect(screen.getByTestId('cloud-defend-btnyamlview')).toBeDisabled();
});
});

View file

@ -1,86 +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, { useState, useCallback } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiTabs, EuiTab } from '@elastic/eui';
import * as i18n from './translations';
import { ControlGeneralView } from '../control_general_view';
import { ControlYamlView } from '../control_yaml_view';
import { SettingsDeps, OnChangeDeps } from '../../types';
const VIEW_MODE_GENERAL = 'general';
const VIEW_MODE_YAML = 'yaml';
export const ControlSettings = ({ policy, onChange }: SettingsDeps) => {
const [viewMode, setViewMode] = useState(VIEW_MODE_GENERAL);
const [isValid, setIsValid] = useState(true);
const onViewModeGeneral = useCallback(() => {
setViewMode(VIEW_MODE_GENERAL);
}, []);
const onViewModeYaml = useCallback(() => {
setViewMode(VIEW_MODE_YAML);
}, []);
const isGeneralViewSelected = viewMode === VIEW_MODE_GENERAL;
const isYamlViewSelected = viewMode === VIEW_MODE_YAML;
const onGeneralChanges = useCallback(
(opts: OnChangeDeps) => {
opts.updatedPolicy = policy;
onChange(opts);
setIsValid(opts.isValid);
},
[onChange, policy]
);
const onYamlChanges = useCallback(
(opts: OnChangeDeps) => {
opts.updatedPolicy = policy;
onChange(opts);
setIsValid(opts.isValid);
},
[onChange, policy]
);
return (
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiTabs>
<EuiTab
id={VIEW_MODE_GENERAL}
onClick={onViewModeGeneral}
isSelected={isGeneralViewSelected}
data-test-subj="cloud-defend-btngeneralview"
disabled={!isValid}
>
{i18n.viewModeGeneral}
</EuiTab>
<EuiTab
id={VIEW_MODE_YAML}
onClick={onViewModeYaml}
isSelected={isYamlViewSelected}
data-test-subj="cloud-defend-btnyamlview"
disabled={!isValid}
>
{i18n.viewModeYaml}
</EuiTab>
</EuiTabs>
</EuiFlexItem>
<EuiFlexItem>
{isGeneralViewSelected && (
<ControlGeneralView
show={isGeneralViewSelected}
policy={policy}
onChange={onGeneralChanges}
/>
)}
<ControlYamlView show={isYamlViewSelected} policy={policy} onChange={onYamlChanges} />
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -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.
*/
import { i18n } from '@kbn/i18n';
export const viewModeGeneral = i18n.translate('xpack.cloudDefend.controlGeneralView', {
defaultMessage: 'General view',
});
export const viewModeYaml = i18n.translate('xpack.cloudDefend.controlYamlView', {
defaultMessage: 'YAML view',
});

View file

@ -1,405 +0,0 @@
{
"$id": "https://elastic.co/cloud-defend/policy-schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"$comment": "policy-version=1.0.0",
"type": "object",
"anyOf": [
{
"required": ["file"]
},
{
"required": ["process"]
}
],
"additionalProperties": true,
"properties": {
"version": {
"type": "string"
},
"file": {
"type": "object",
"required": ["selectors", "responses"],
"properties": {
"selectors": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/fileSelector"
}
},
"responses": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/fileResponse"
}
}
}
},
"process": {
"type": "object",
"required": ["selectors", "responses"],
"properties": {
"selectors": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/processSelector"
}
},
"responses": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/processResponse"
}
}
}
}
},
"$defs": {
"fileSelector": {
"type": "object",
"required": ["name"],
"additionalProperties": true,
"anyOf": [
{
"required": ["operation"]
},
{
"required": ["containerImageName"]
},
{
"required": ["containerImageTag"]
},
{
"required": ["containerImageFullName"]
},
{
"required": ["kubernetesClusterId"]
},
{
"required": ["kubernetesClusterName"]
},
{
"required": ["kubernetesNamespace"]
},
{
"required": ["kubernetesPodLabel"]
},
{
"required": ["kubernetesPodName"]
},
{
"required": ["targetFilePath"]
},
{
"required": ["ignoreVolumeMounts"]
},
{
"required": ["ignoreVolumeFiles"]
}
],
"properties": {
"name": {
"type": "string"
},
"operation": {
"type": "array",
"minItems": 1,
"items": {
"enum": [
"createExecutable",
"modifyExecutable",
"createFile",
"modifyFile",
"deleteFile"
]
}
},
"containerImageName": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"pattern": "^([a-z0-9]+(?:[._-][a-z0-9]+)*)$"
}
},
"containerImageTag": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
},
"containerImageFullName": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"pattern": "^(?:\\[[a-fA-F0-9:]+\\]|(?:[a-zA-Z0-9-](?:\\.[a-z0-9]+)*)+)(?::[0-9]+)?(?:\\/[a-z0-9]+(?:[._-][a-z0-9]+)*)+$"
}
},
"kubernetesClusterId": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
},
"kubernetesClusterName": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
},
"kubernetesNamespace": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
},
"kubernetesPodLabel": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"pattern": "^([a-zA-Z0-9\\.\\-]+\\/)?[a-zA-Z0-9\\.\\-]+:[a-zA-Z0-9\\.\\-\\_]*\\*?$"
}
},
"kubernetesPodName": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
},
"targetFilePath": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"pattern": "^(?:\\/[^\\/\\*]+)*(?:\\/\\*|\\/\\*\\*)?$",
"minLength": 1
}
},
"ignoreVolumeMounts": {
"type": "boolean",
"description": "Ignore all volume mounts. e.g directories, files, configMaps, secrets etc...\nNote: should not be used with ignoreVolumeFiles"
},
"ignoreVolumeFiles": {
"type": "boolean",
"description": "Ignore file mounts. e.g files, configMaps, secrets\nNote: should not be used with ignoreVolumeMounts"
}
},
"dependencies": {
"ignoreVolumeMounts": {
"not": {
"required": ["ignoreVolumeFiles"]
}
},
"containerImageFullName": {
"not": {
"required": ["containerImageName"]
}
}
}
},
"processSelector": {
"type": "object",
"required": ["name"],
"additionalProperties": true,
"anyOf": [
{
"required": ["operation"]
},
{
"required": ["containerImageName"]
},
{
"required": ["containerImageTag"]
},
{
"required": ["containerImageFullName"]
},
{
"required": ["kubernetesClusterId"]
},
{
"required": ["kubernetesClusterName"]
},
{
"required": ["kubernetesNamespace"]
},
{
"required": ["kubernetesPodLabel"]
},
{
"required": ["kubernetesPodName"]
},
{
"required": ["processExecutable"]
},
{
"required": ["processName"]
},
{
"required": ["sessionLeaderInteractive"]
}
],
"properties": {
"name": {
"type": "string"
},
"operation": {
"type": "array",
"minItems": 1,
"items": {
"enum": ["fork", "exec"]
}
},
"containerImageName": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"pattern": "^[a-z0-9]+(?:[._-][a-z0-9]+)*$"
}
},
"containerImageTag": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
},
"containerImageFullName": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"pattern": "^(?:\\[[a-fA-F0-9:]+\\]|(?:[a-zA-Z0-9-](?:\\.[a-z0-9]+)*)+)(?::[0-9]+)?(?:\\/[a-z0-9]+(?:[._-][a-z0-9]+)*)+$"
}
},
"kubernetesClusterId": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
},
"kubernetesClusterName": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
},
"kubernetesNamespace": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
},
"kubernetesPodLabel": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"pattern": "^([a-zA-Z0-9\\.\\-]+\\/)?[a-zA-Z0-9\\.\\-]+:[a-zA-Z0-9\\.\\-\\_]*\\*?$"
}
},
"kubernetesPodName": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
},
"processExecutable": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"pattern": "^(?:\\/[^\\/\\*]+)*(?:\\/\\*|\\/\\*\\*)?$",
"minLength": 1
}
},
"processName": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
},
"sessionLeaderInteractive": {
"type": "boolean"
}
},
"dependencies": {
"containerImageFullName": {
"not": {
"required": ["containerImageName"]
}
}
}
},
"fileResponse": {
"type": "object",
"required": ["match", "actions"],
"additionalProperties": true,
"properties": {
"match": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
},
"exclude": {
"type": "array",
"items": {
"type": "string"
}
},
"actions": {
"type": "array",
"minItems": 1,
"items": {
"enum": ["alert", "block", "log"]
}
}
}
},
"processResponse": {
"type": "object",
"required": ["match", "actions"],
"additionalProperties": true,
"properties": {
"match": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
},
"exclude": {
"type": "array",
"items": {
"type": "string"
}
},
"actions": {
"type": "array",
"minItems": 1,
"items": {
"enum": ["alert", "block", "log"]
}
}
}
}
}
}

View file

@ -1,81 +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 { useMemo, useState, useEffect } from 'react';
import { monaco, configureMonacoYamlSchema, YAML_LANG_ID } from '@kbn/monaco';
import { getSelectorsAndResponsesFromYaml } from '../../../../common/utils/helpers';
/**
* In order to keep this json in sync with https://github.com/elastic/cloud-defend/blob/main/modules/config/policy-schema.json
* Do NOT commit edits to policy_schema.json as part of a PR. Please make the changes in the cloud-defend repo.
* Buildkite will take care of creating a PR in kibana.
*/
import policySchemaJson from './policy_schema.json';
const { Uri, editor } = monaco;
const SCHEMA_URI = 'http://elastic.co/cloud_defend.json';
const modelUri = Uri.parse(SCHEMA_URI);
export const useConfigModel = (configuration: string) => {
const [configModel, setConfigModel] = useState<monaco.editor.ITextModel | null>(null);
const schema = useMemo(() => {
const _schema: any = { ...policySchemaJson };
const { selectors } = getSelectorsAndResponsesFromYaml(configuration);
// dynamically setting enum values for response match and exclude properties.
if (_schema.$defs.fileResponse.properties.match.items) {
const responseProps = _schema.$defs.fileResponse.properties;
const selectorEnum = {
enum: selectors
.filter((selector) => selector.type === 'file')
.map((selector) => selector.name),
};
responseProps.match.items = selectorEnum;
responseProps.exclude.items = selectorEnum;
}
if (_schema.$defs.processResponse.properties.match.items) {
const responseProps = _schema.$defs.processResponse.properties;
const selectorEnum = {
enum: selectors
.filter((selector) => selector.type === 'process')
.map((selector) => selector.name),
};
responseProps.match.items = selectorEnum;
responseProps.exclude.items = selectorEnum;
}
return _schema;
}, [configuration]);
useEffect(() => {
async function configureMonacoYaml(...args: Parameters<typeof configureMonacoYamlSchema>) {
const { dispose } = await configureMonacoYamlSchema(...args);
let model = editor.getModel(modelUri);
if (model === null) {
model = editor.createModel('', YAML_LANG_ID, modelUri);
}
setConfigModel(model);
return () => dispose();
}
configureMonacoYaml([
{
uri: SCHEMA_URI,
fileMatch: [String(modelUri)],
schema,
},
]);
}, [schema]);
return configModel;
};

View file

@ -1,77 +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 { render } from '@testing-library/react';
import '@kbn/code-editor-mock/jest_helper';
import { TestProvider } from '../../test/test_provider';
import {
getCloudDefendNewPolicyMock,
MOCK_YAML_INVALID_CONFIGURATION,
MOCK_YAML_INVALID_ACTIONS,
MOCK_YAML_TOO_MANY_FILE_SELECTORS_RESPONSES,
MOCK_YAML_INVALID_STRING_ARRAY_CONDITION,
} from '../../test/mocks';
import { ControlYamlView } from '.';
import * as i18n from './translations';
import { MAX_SELECTORS_AND_RESPONSES_PER_TYPE } from '../../common/constants';
describe('<ControlYamlView />', () => {
const onChange = jest.fn();
const WrappedComponent = ({ policy = getCloudDefendNewPolicyMock() }) => {
return (
<TestProvider>
<ControlYamlView policy={policy} onChange={onChange} show />;
</TestProvider>
);
};
beforeEach(() => {
onChange.mockClear();
});
it('handles invalid yaml', async () => {
render(
<WrappedComponent policy={getCloudDefendNewPolicyMock(MOCK_YAML_INVALID_CONFIGURATION)} />
);
});
it('handles additionalErrors: max selectors+responses exceeded ', async () => {
const { getByText, getByTestId } = render(
<WrappedComponent
policy={getCloudDefendNewPolicyMock(MOCK_YAML_TOO_MANY_FILE_SELECTORS_RESPONSES)}
/>
);
expect(getByTestId('cloudDefendAdditionalErrors')).toBeTruthy();
expect(
getByText(
`You cannot exceed ${MAX_SELECTORS_AND_RESPONSES_PER_TYPE} selectors + responses for a given type e.g file, process`
)
).toBeTruthy();
});
it('handles additionalErrors: block action error', async () => {
const { getByText, getByTestId } = render(
<WrappedComponent policy={getCloudDefendNewPolicyMock(MOCK_YAML_INVALID_ACTIONS)} />
);
expect(getByTestId('cloudDefendAdditionalErrors')).toBeTruthy();
expect(getByText(i18n.errorAlertActionRequired)).toBeTruthy();
});
it('handles additionalErrors: selector condition value byte length', async () => {
const { getByText, getByTestId } = render(
<WrappedComponent
policy={getCloudDefendNewPolicyMock(MOCK_YAML_INVALID_STRING_ARRAY_CONDITION)}
/>
);
expect(getByTestId('cloudDefendAdditionalErrors')).toBeTruthy();
expect(getByText('"targetFilePath" values cannot exceed 255 bytes')).toBeTruthy();
});
});

View file

@ -1,176 +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, { useCallback, useEffect, useState } from 'react';
import { EuiSpacer, EuiText, EuiFlexGroup, EuiFlexItem, EuiForm } from '@elastic/eui';
import { CodeEditor, YAML_LANG_ID } from '@kbn/code-editor';
import { monaco } from '@kbn/monaco';
import { uniq } from 'lodash';
import { INPUT_CONTROL } from '../../../common/constants';
import { useStyles } from './styles';
import { useConfigModel } from './hooks/use_config_model';
import {
validateStringValuesForCondition,
validateMaxSelectorsAndResponses,
validateBlockRestrictions,
} from '../../common/utils';
import {
getInputFromPolicy,
getSelectorsAndResponsesFromYaml,
} from '../../../common/utils/helpers';
import * as i18n from './translations';
import { ViewDeps, SelectorConditionsMap } from '../../types';
import { SelectorCondition } from '../../../common';
const { editor } = monaco;
const TEXT_EDITOR_PADDING = 10;
interface EditorError {
line: number;
message: string;
}
export const ControlYamlView = ({ policy, onChange, show }: ViewDeps) => {
const styles = useStyles();
const [editorErrors, setEditorErrors] = useState<EditorError[]>([]);
const [additionalErrors, setAdditionalErrors] = useState<string[]>([]);
const input = getInputFromPolicy(policy, INPUT_CONTROL);
const configuration = input?.vars?.configuration?.value || '';
const currentModel = useConfigModel(configuration);
// not all validations can be done via json-schema
const validateAdditional = useCallback((value: any) => {
const errors: string[] = [];
const { selectors, responses } = getSelectorsAndResponsesFromYaml(value);
errors.push(...validateMaxSelectorsAndResponses(selectors, responses));
errors.push(...validateBlockRestrictions(selectors, responses));
// validate selectors
selectors.forEach((selector) => {
Object.keys(selector).map((prop) => {
const condition = prop as SelectorCondition;
if (SelectorConditionsMap[condition]?.type === 'stringArray') {
const values = selector[condition] as string[];
errors.push(...validateStringValuesForCondition(condition, values));
}
});
});
// validate responses
responses.forEach((response) => {
// for now we force 'alert' action if 'block' action added.
if (
response.actions &&
response.actions.includes('block') &&
!response.actions.includes('alert')
) {
errors.push(i18n.errorAlertActionRequired);
}
});
return uniq(errors);
}, []);
useEffect(() => {
if (!show) return;
// for on mount
const otherErrors = validateAdditional(configuration);
if (otherErrors.length !== additionalErrors.length) {
setAdditionalErrors(otherErrors);
}
const listener = editor.onDidChangeMarkers(([resource]) => {
const markers = editor.getModelMarkers({ resource });
const errs = markers.map((marker) => {
const error: EditorError = {
line: marker.startLineNumber,
message: marker.message,
};
return error;
});
// prevents infinite loop
if (
otherErrors.length !== additionalErrors.length ||
JSON.stringify(errs) !== JSON.stringify(editorErrors)
) {
onChange({
isValid: otherErrors.length === 0 && errs.length === 0,
updatedPolicy: policy,
});
setEditorErrors(errs);
}
});
return () => {
listener.dispose();
};
}, [
editorErrors,
onChange,
policy,
additionalErrors.length,
validateAdditional,
configuration,
show,
]);
const onYamlChange = useCallback(
(value: any) => {
if (show && input?.vars) {
input.vars.configuration.value = value;
const errs = validateAdditional(value);
setAdditionalErrors(errs);
onChange({
isValid: errs.length === 0 && editorErrors.length === 0,
updatedPolicy: policy,
});
}
},
[editorErrors.length, input?.vars, onChange, policy, show, validateAdditional]
);
return (
<EuiFlexGroup direction="column" css={!show && styles.hide}>
<EuiFlexItem>
<EuiText color="subdued" size="s">
{i18n.controlYamlHelp}
</EuiText>
<EuiSpacer size="s" />
{additionalErrors.length > 0 && (
<EuiForm
data-test-subj="cloudDefendAdditionalErrors"
isInvalid={true}
error={additionalErrors}
/>
)}
<div css={styles.yamlEditor}>
<CodeEditor
width="100%"
languageId={YAML_LANG_ID}
options={{
wordWrap: 'off',
model: currentModel,
automaticLayout: true,
padding: { top: TEXT_EDITOR_PADDING, bottom: TEXT_EDITOR_PADDING },
}}
onChange={onYamlChange}
value={configuration}
/>
</div>
<EuiSpacer size="s" />
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -1,32 +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 { useMemo } from 'react';
import { CSSObject } from '@emotion/react';
import { useEuiTheme } from '@elastic/eui';
export const useStyles = () => {
const { euiTheme } = useEuiTheme();
const { border } = euiTheme;
return useMemo(() => {
const yamlEditor: CSSObject = {
height: '500px',
border: border.thin,
};
// for some reason, switching back to monaco (by virtue of including the editor when yaml view selector, causes the editor not not update properly when switching views.
// instead I just hide it visually, and show when we switch back which seems
// to fix the issue.
const hide: CSSObject = {
visibility: 'hidden',
position: 'absolute',
};
return { yamlEditor, hide };
}, [border.thin]);
};

View file

@ -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 { i18n } from '@kbn/i18n';
export const errorAlertActionRequired = i18n.translate('xpack.cloudDefend.alertActionRequired', {
defaultMessage: 'The alert action is required when "block" action used.',
});
export const controlYamlHelp = i18n.translate('xpack.cloudDefend.controlYamlHelp', {
defaultMessage:
'Configure your policy by creating "file" or "process" selectors and responses below.',
});

View file

@ -1,24 +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, { memo } from 'react';
import type { PackagePolicyReplaceDefineStepExtensionComponentProps } from '@kbn/fleet-plugin/public/types';
import { PolicySettings } from '../policy_settings';
export const CloudDefendFleetPolicyReplaceDefineStepExtension =
memo<PackagePolicyReplaceDefineStepExtensionComponentProps>(
({ newPolicy, onChange }: PackagePolicyReplaceDefineStepExtensionComponentProps) => {
const policy = JSON.parse(JSON.stringify(newPolicy));
return <PolicySettings policy={policy} onChange={onChange} />;
}
);
CloudDefendFleetPolicyReplaceDefineStepExtension.displayName =
'CloudDefendFleetPolicyReplaceDefineStepExtension';
// eslint-disable-next-line import/no-default-export
export { CloudDefendFleetPolicyReplaceDefineStepExtension as default };

View file

@ -1,28 +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 { EuiFlexGroup, type CommonProps } from '@elastic/eui';
import { css } from '@emotion/react';
import React from 'react';
// Keep this component lean as it is part of the main app bundle
export const FullSizeCenteredPage = ({
children,
...rest
}: { children: React.ReactNode } & CommonProps) => (
<EuiFlexGroup
css={css`
// 140px is roughly the Kibana chrome with a bit of space to spare
min-height: calc(100vh - 140px);
`}
justifyContent="center"
alignItems="center"
direction="column"
{...rest}
>
{children}
</EuiFlexGroup>
);

View file

@ -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 { EuiLoadingSpinner, EuiSpacer } from '@elastic/eui';
import React, { FC, PropsWithChildren } from 'react';
import { FullSizeCenteredPage } from '../full_size_page';
// Keep this component lean as it is part of the main app bundle
export const LoadingState: FC<
PropsWithChildren<{
['data-test-subj']?: string;
}>
> = ({ children, ...rest }) => {
return (
<FullSizeCenteredPage data-test-subj={rest['data-test-subj']}>
<EuiLoadingSpinner size="xl" />
<EuiSpacer />
{children}
</FullSizeCenteredPage>
);
};

View file

@ -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 React from 'react';
import Chance from 'chance';
import { render, screen } from '@testing-library/react';
import moment from 'moment';
import { createCloudDefendIntegrationFixture } from '../../test/fixtures/cloud_defend_integration';
import { PoliciesTable } from '.';
import { TestProvider } from '../../test/test_provider';
describe('<PoliciesTable />', () => {
const chance = new Chance();
const tableProps = {
pageIndex: 1,
pageSize: 10,
error: undefined,
loading: false,
setQuery: jest.fn(),
};
it('renders integration name', () => {
const item = createCloudDefendIntegrationFixture();
const policies = [item];
render(
<TestProvider>
<PoliciesTable
{...{
...tableProps,
policies,
totalItemCount: policies.length,
}}
/>
</TestProvider>
);
expect(screen.getByText(item.package_policy.name)).toBeInTheDocument();
});
it('renders agent policy name', () => {
const agentPolicy = {
id: chance.guid(),
name: chance.sentence(),
agents: chance.integer({ min: 1 }),
};
const policies = [createCloudDefendIntegrationFixture({ agent_policy: agentPolicy })];
render(
<TestProvider>
<PoliciesTable
{...{
...tableProps,
policies,
totalItemCount: policies.length,
}}
/>
</TestProvider>
);
expect(screen.getByText(agentPolicy.name)).toBeInTheDocument();
});
it('renders number of agents', () => {
const item = createCloudDefendIntegrationFixture();
const policies = [item];
render(
<TestProvider>
<PoliciesTable
{...{
...tableProps,
policies,
totalItemCount: policies.length,
}}
/>
</TestProvider>
);
// TODO too loose
expect(screen.getByText(item.agent_policy.agents as number)).toBeInTheDocument();
});
it('renders created by', () => {
const item = createCloudDefendIntegrationFixture();
const policies = [item];
render(
<TestProvider>
<PoliciesTable
{...{
...tableProps,
policies,
totalItemCount: policies.length,
}}
/>
</TestProvider>
);
expect(screen.getByText(item.package_policy.created_by)).toBeInTheDocument();
});
it('renders created at', () => {
const item = createCloudDefendIntegrationFixture();
const policies = [item];
render(
<TestProvider>
<PoliciesTable
{...{
...tableProps,
policies,
totalItemCount: policies.length,
}}
/>
</TestProvider>
);
expect(screen.getByText(moment(item.package_policy.created_at).fromNow())).toBeInTheDocument();
});
});

View file

@ -1,157 +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 {
EuiBasicTable,
type EuiBasicTableColumn,
type EuiBasicTableProps,
type Pagination,
type CriteriaWithPagination,
EuiLink,
} from '@elastic/eui';
import React from 'react';
import { pagePathGetters } from '@kbn/fleet-plugin/public';
import { i18n } from '@kbn/i18n';
import { TimestampTableCell } from '../timestamp_table_cell';
import type { CloudDefendPolicy } from '../../../common';
import { useKibana } from '../../common/hooks/use_kibana';
import * as TEST_SUBJ from '../../pages/policies/test_subjects';
interface PoliciesTableProps
extends Pick<
EuiBasicTableProps<CloudDefendPolicy>,
'loading' | 'error' | 'noItemsMessage' | 'sorting'
>,
Pagination {
policies: CloudDefendPolicy[];
setQuery(pagination: CriteriaWithPagination<CloudDefendPolicy>): void;
'data-test-subj'?: string;
}
const AgentPolicyButtonLink = ({ name, id: policyId }: { name: string; id: string }) => {
const { http } = useKibana().services;
const [fleetBase, path] = pagePathGetters.policy_details({ policyId });
return <EuiLink href={http.basePath.prepend([fleetBase, path].join(''))}>{name}</EuiLink>;
};
const IntegrationButtonLink = ({
packageName,
policyId,
packagePolicyId,
}: {
packageName: string;
packagePolicyId: string;
policyId: string;
}) => {
const editIntegrationLink = pagePathGetters
.edit_integration({
packagePolicyId,
policyId,
})
.join('');
return <EuiLink href={editIntegrationLink}>{packageName}</EuiLink>;
};
const POLICIES_TABLE_COLUMNS: Array<EuiBasicTableColumn<CloudDefendPolicy>> = [
{
field: 'package_policy.name',
name: i18n.translate('xpack.cloudDefend.policies.policiesTable.integrationNameColumnTitle', {
defaultMessage: 'Integration Name',
}),
render: (packageName, policy) => (
<IntegrationButtonLink
packageName={packageName}
packagePolicyId={policy.package_policy.id}
policyId={policy.package_policy.policy_ids[0]}
/>
),
truncateText: true,
sortable: true,
'data-test-subj': TEST_SUBJ.POLICIES_TABLE_COLUMNS.INTEGRATION_NAME,
},
{
field: 'agent_policy.name',
name: i18n.translate('xpack.cloudDefend.policies.policiesTable.agentPolicyColumnTitle', {
defaultMessage: 'Agent Policy',
}),
render: (name, policy) => <AgentPolicyButtonLink name={name} id={policy.agent_policy.id} />,
truncateText: true,
'data-test-subj': TEST_SUBJ.POLICIES_TABLE_COLUMNS.AGENT_POLICY,
},
{
field: 'agent_policy.agents',
name: i18n.translate('xpack.cloudDefend.policies.policiesTable.numberOfAgentsColumnTitle', {
defaultMessage: 'Number of Agents',
}),
truncateText: true,
'data-test-subj': TEST_SUBJ.POLICIES_TABLE_COLUMNS.NUMBER_OF_AGENTS,
},
{
field: 'package_policy.created_by',
name: i18n.translate('xpack.cloudDefend.policies.policiesTable.createdByColumnTitle', {
defaultMessage: 'Created by',
}),
dataType: 'string',
truncateText: true,
sortable: true,
'data-test-subj': TEST_SUBJ.POLICIES_TABLE_COLUMNS.CREATED_BY,
},
{
field: 'package_policy.created_at',
name: i18n.translate('xpack.cloudDefend.policies.policiesTable.createdAtColumnTitle', {
defaultMessage: 'Created at',
}),
dataType: 'date',
truncateText: true,
render: (timestamp: CloudDefendPolicy['package_policy']['created_at']) => (
<TimestampTableCell timestamp={timestamp} />
),
sortable: true,
'data-test-subj': TEST_SUBJ.POLICIES_TABLE_COLUMNS.CREATED_AT,
},
];
export const PoliciesTable = ({
policies,
pageIndex,
pageSize,
totalItemCount,
loading,
error,
setQuery,
noItemsMessage,
sorting,
...rest
}: PoliciesTableProps) => {
const pagination: Pagination = {
pageIndex: Math.max(pageIndex - 1, 0),
pageSize,
totalItemCount,
};
const onChange = ({ page, sort }: CriteriaWithPagination<CloudDefendPolicy>) => {
setQuery({ page: { ...page, index: page.index + 1 }, sort });
};
return (
<EuiBasicTable
data-test-subj={rest['data-test-subj']}
items={policies}
columns={POLICIES_TABLE_COLUMNS}
itemId={(item) => [item.agent_policy.id, item.package_policy.id].join('/')}
pagination={pagination}
onChange={onChange}
tableLayout="fixed"
loading={loading}
noItemsMessage={noItemsMessage}
error={error}
sorting={sorting}
/>
);
};

View file

@ -1,79 +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 { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@kbn/code-editor-mock/jest_helper';
import { TestProvider } from '../../test/test_provider';
import { getCloudDefendNewPolicyMock } from '../../test/mocks';
import { PolicySettings } from '.';
import { getInputFromPolicy } from '../../../common/utils/helpers';
import { INPUT_CONTROL } from '../../../common/constants';
describe('<PolicySettings />', () => {
const onChange = jest.fn();
const WrappedComponent = ({ policy = getCloudDefendNewPolicyMock() }) => {
return (
<TestProvider>
<PolicySettings policy={policy} onChange={onChange} />;
</TestProvider>
);
};
beforeEach(() => {
onChange.mockClear();
});
it('allows user to set name of integration', async () => {
const { getByTestId } = render(<WrappedComponent />);
const input = getByTestId('cloud-defend-policy-name');
if (input) {
await userEvent.type(input, '1');
} else {
throw new Error("Can't find input");
}
const { updatedPolicy } = onChange.mock.calls[0][0];
expect(updatedPolicy.name).toEqual('some-cloud_defend-policy1');
});
it('allows user to set description of integration', async () => {
const { getByTestId } = render(<WrappedComponent />);
const input = getByTestId('cloud-defend-policy-description');
if (input) {
await userEvent.type(input, '1');
} else {
throw new Error("Can't find input");
}
const { updatedPolicy } = onChange.mock.calls[0][0];
expect(updatedPolicy.description).toEqual('1');
});
it('renders a checkbox to toggle BPF/LSM control mechanism', () => {
const { getByTestId } = render(<WrappedComponent />);
const input = getByTestId('cloud-defend-controltoggle');
expect(input).toBeInTheDocument();
expect(input).toBeEnabled();
});
it('User can disable control features', async () => {
const { getByTestId } = render(<WrappedComponent />);
await userEvent.click(getByTestId('cloud-defend-controltoggle'));
const policy = onChange.mock.calls[0][0].updatedPolicy;
const controlInput = getInputFromPolicy(policy, INPUT_CONTROL);
expect(controlInput?.enabled).toBeFalsy();
});
});

View file

@ -1,117 +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, { useCallback, FormEvent, useState } from 'react';
import {
EuiTextArea,
EuiSwitch,
EuiSpacer,
EuiText,
EuiFlexGroup,
EuiFlexItem,
EuiForm,
EuiFormRow,
EuiFieldText,
EuiHorizontalRule,
EuiSwitchProps,
} from '@elastic/eui';
import { INPUT_CONTROL } from '../../../common/constants';
import { getInputFromPolicy } from '../../../common/utils/helpers';
import * as i18n from './translations';
import { ControlSettings } from '../control_settings';
import { SettingsDeps, OnChangeDeps } from '../../types';
export const PolicySettings = ({ policy, onChange }: SettingsDeps) => {
const [policyHasErrors, setPolicyHasErrors] = useState(false);
const controlInput = getInputFromPolicy(policy, INPUT_CONTROL);
const controlEnabled = !!controlInput?.enabled;
const onToggleEnabled = useCallback<EuiSwitchProps['onChange']>(
(e) => {
if (controlInput) {
controlInput.enabled = e.target.checked;
onChange({ isValid: !policyHasErrors, updatedPolicy: { ...policy } });
}
},
[controlInput, onChange, policyHasErrors, policy]
);
const onNameChange = useCallback(
(event: FormEvent<HTMLInputElement>) => {
const name = event.currentTarget.value;
onChange({ isValid: !policyHasErrors, updatedPolicy: { ...policy, name } });
},
[onChange, policyHasErrors, policy]
);
const onDescriptionChange = useCallback(
(event: FormEvent<HTMLTextAreaElement>) => {
const description = event.currentTarget.value;
onChange({ isValid: !policyHasErrors, updatedPolicy: { ...policy, description } });
},
[onChange, policyHasErrors, policy]
);
const onPolicyChange = useCallback(
(props: OnChangeDeps) => {
setPolicyHasErrors(!props.isValid);
onChange(props);
},
[onChange]
);
return (
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiForm component="form">
<EuiFormRow label={i18n.name} fullWidth={true}>
<EuiFieldText
fullWidth={true}
name="name"
value={policy.name}
onChange={onNameChange}
data-test-subj="cloud-defend-policy-name"
/>
</EuiFormRow>
<EuiFormRow label={i18n.description} fullWidth={true}>
<EuiTextArea
fullWidth={true}
name="name"
value={policy.description}
onChange={onDescriptionChange}
data-test-subj="cloud-defend-policy-description"
compressed
/>
</EuiFormRow>
<EuiHorizontalRule />
<EuiFormRow fullWidth>
<EuiFlexItem>
<EuiSwitch
data-test-subj="cloud-defend-controltoggle"
label={i18n.enableControl}
checked={controlEnabled}
onChange={onToggleEnabled}
/>
<EuiSpacer size="s" />
<EuiText color="subdued" size="s">
{i18n.enableControlHelp}
</EuiText>
</EuiFlexItem>
</EuiFormRow>
</EuiForm>
</EuiFlexItem>
{controlEnabled && (
<ControlSettings
data-test-subj="cloud-defend-controlsettings"
policy={policy}
onChange={onPolicyChange}
/>
)}
</EuiFlexGroup>
);
};

View file

@ -1,24 +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 name = i18n.translate('xpack.cloudDefend.name', {
defaultMessage: 'Name',
});
export const description = i18n.translate('xpack.cloudDefend.description', {
defaultMessage: 'Description',
});
export const enableControl = i18n.translate('xpack.cloudDefend.enableControl', {
defaultMessage: 'Enable policy',
});
export const enableControlHelp = i18n.translate('xpack.cloudDefend.enableControlHelp', {
defaultMessage: 'Enables drift prevention, alert, and logging policy shown below.',
});

View file

@ -1,52 +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, EuiPageSection, EuiLink } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import React from 'react';
import { useKibana } from '../../common/hooks/use_kibana';
export const SubscriptionNotAllowed = () => {
const { application } = useKibana().services;
return (
<EuiPageSection color="danger" alignment="center">
<EuiEmptyPrompt
iconType="warning"
title={
<h2>
<FormattedMessage
id="xpack.cloudDefend.subscriptionNotAllowed.promptTitle"
defaultMessage="Upgrade for subscription features"
/>
</h2>
}
body={
<p>
<FormattedMessage
id="xpack.cloudDefend.subscriptionNotAllowed.promptDescription"
defaultMessage="To use these cloud security features, you must {link}."
values={{
link: (
<EuiLink
href={application.getUrlForApp('management', {
path: 'stack/license_management/home',
})}
>
<FormattedMessage
id="xpack.cloudDefend.subscriptionNotAllowed.promptLinkText"
defaultMessage="start a trial or upgrade your subscription"
/>
</EuiLink>
),
}}
/>
</p>
}
/>
</EuiPageSection>
);
};

View file

@ -1,24 +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 moment, { type MomentInput } from 'moment';
import { EuiToolTip, formatDate } from '@elastic/eui';
import { useUiSetting } from '@kbn/kibana-react-plugin/public';
const DEFAULT_DATE_FORMAT = 'dateFormat';
export const TimestampTableCell = ({ timestamp }: { timestamp: MomentInput }) => {
const dateFormat = useUiSetting<string>(DEFAULT_DATE_FORMAT);
const formatted = formatDate(timestamp, dateFormat);
return (
<EuiToolTip position="top" content={formatted}>
<span>{moment(timestamp).fromNow()}</span>
</EuiToolTip>
);
};

View file

@ -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 { CloudDefendPlugin } from './plugin';
export type { CloudDefendSecuritySolutionContext } from './types';
export { getSecuritySolutionLink } from './common/navigation/security_solution_links';
export type { CloudDefendPageId } from './common/navigation/types';
// This exports static code and TypeScript types,
// as well as, Kibana Platform `plugin()` initializer.
export function plugin() {
return new CloudDefendPlugin();
}
export type { CloudDefendPluginSetup, CloudDefendPluginStart } from './types';

View file

@ -1,8 +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 { Policies } from './policies';

View file

@ -1,93 +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 Chance from 'chance';
import { render, screen } from '@testing-library/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { createCloudDefendIntegrationFixture } from '../../test/fixtures/cloud_defend_integration';
import { createReactQueryResponse } from '../../test/fixtures/react_query';
import { TestProvider } from '../../test/test_provider';
import { Policies } from '.';
import * as TEST_SUBJ from './test_subjects';
import { useCloudDefendPolicies } from './use_cloud_defend_policies';
import { useCloudDefendSetupStatusApi } from '../../common/api/use_setup_status_api';
import { useSubscriptionStatus } from '../../common/hooks/use_subscription_status';
import { useCloudDefendIntegrationLinks } from '../../common/navigation/use_cloud_defend_integration_links';
jest.mock('./use_cloud_defend_policies');
jest.mock('../../common/api/use_setup_status_api');
jest.mock('../../common/hooks/use_subscription_status');
jest.mock('../../common/navigation/use_cloud_defend_integration_links');
const chance = new Chance();
describe('<Policies />', () => {
beforeEach(() => {
jest.resetAllMocks();
(useCloudDefendSetupStatusApi as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: { status: 'indexed' },
})
);
(useSubscriptionStatus as jest.Mock).mockImplementation(() =>
createReactQueryResponse({
status: 'success',
data: true,
})
);
(useCloudDefendIntegrationLinks as jest.Mock).mockImplementation(() => ({
addIntegrationLink: chance.url(),
docsLink: chance.url(),
}));
});
const renderPolicies = (queryResponse: Partial<UseQueryResult> = createReactQueryResponse()) => {
(useCloudDefendPolicies as jest.Mock).mockImplementation(() => queryResponse);
return render(
<TestProvider>
<Policies />
</TestProvider>
);
};
it('renders the page header', () => {
renderPolicies();
expect(screen.getByTestId(TEST_SUBJ.POLICIES_PAGE_HEADER)).toBeInTheDocument();
});
it('renders the "add integration" button', () => {
renderPolicies();
expect(screen.getByTestId(TEST_SUBJ.ADD_INTEGRATION_TEST_SUBJ)).toBeInTheDocument();
});
it('renders error state while there is an error', () => {
const error = new Error('message');
renderPolicies(createReactQueryResponse({ status: 'error', error }));
expect(screen.getByText(error.message)).toBeInTheDocument();
});
it('renders the policies table', () => {
renderPolicies(
createReactQueryResponse({
status: 'success',
data: { total: 1, items: [createCloudDefendIntegrationFixture()] },
})
);
expect(screen.getByTestId(TEST_SUBJ.POLICIES_TABLE_DATA_TEST_SUBJ)).toBeInTheDocument();
Object.values(TEST_SUBJ.POLICIES_TABLE_COLUMNS).forEach((testId) =>
expect(screen.getAllByTestId(testId)[0]).toBeInTheDocument()
);
});
});

View file

@ -1,197 +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, { useState } from 'react';
import {
EuiButton,
EuiFieldSearch,
EuiFieldSearchProps,
EuiFlexGroup,
EuiFlexItem,
EuiPageHeader,
EuiSpacer,
EuiText,
EuiTextColor,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import useDebounce from 'react-use/lib/useDebounce';
import { i18n } from '@kbn/i18n';
import { CloudDefendPageTitle } from '../../components/cloud_defend_page_title';
import { CloudDefendPage } from '../../components/cloud_defend_page';
import { PoliciesTable } from '../../components/policies_table';
import { useCloudDefendPolicies, UseCloudDefendPoliciesProps } from './use_cloud_defend_policies';
import { extractErrorMessage } from '../../../common/utils/helpers';
import * as TEST_SUBJ from './test_subjects';
import { LOCAL_STORAGE_PAGE_SIZE } from '../../common/constants';
import { usePageSize } from '../../common/hooks/use_page_size';
import { useCloudDefendIntegrationLinks } from '../../common/navigation/use_cloud_defend_integration_links';
const SEARCH_DEBOUNCE_MS = 300;
const AddIntegrationButton = () => {
const { addIntegrationLink } = useCloudDefendIntegrationLinks();
return (
<EuiButton
data-test-subj={TEST_SUBJ.ADD_INTEGRATION_TEST_SUBJ}
fill
iconType="plusInCircle"
href={addIntegrationLink}
>
<FormattedMessage
id="xpack.cloudDefend.policies.policiesPageHeader.addIntegrationButtonLabel"
defaultMessage="Add Integration"
/>
</EuiButton>
);
};
const EmptyState = ({ name }: { name: string }) => (
<div>
<EuiSpacer size="l" />
{
<EuiText>
<strong>
<FormattedMessage
id="xpack.cloudDefend.policies.policyEmptyState.integrationsNotFoundTitle"
defaultMessage="No policies found"
/>
{name && (
<FormattedMessage
id="xpack.cloudDefend.policies.policyEmptyState.integrationsNotFoundForNameTitle"
defaultMessage=' for "{name}"'
values={{ name }}
/>
)}
</strong>
</EuiText>
}
<EuiSpacer size="s" />
<EuiText>
<EuiTextColor color="subdued">
<FormattedMessage
id="xpack.cloudDefend.policies.policyEmptyState.integrationsNotFoundWithFiltersTitle"
defaultMessage="We weren't able to find any policies with the above filters."
/>
</EuiTextColor>
</EuiText>
<EuiSpacer size="l" />
</div>
);
const TotalIntegrationsCount = ({
pageCount,
totalCount,
}: Record<'pageCount' | 'totalCount', number>) => (
<EuiText size="xs" css={{ marginLeft: 8 }}>
<EuiTextColor color="subdued">
<FormattedMessage
id="xpack.cloudDefend.policies.totalIntegrationsCountMessage"
defaultMessage="Showing {pageCount} of {totalCount, plural, one {# integration} other {# integrations}}"
values={{ pageCount, totalCount }}
/>
</EuiTextColor>
</EuiText>
);
const SearchField = ({
onSearch,
isLoading,
}: Required<Pick<EuiFieldSearchProps, 'isLoading' | 'onSearch'>>) => {
const [localValue, setLocalValue] = useState('');
useDebounce(() => onSearch(localValue), SEARCH_DEBOUNCE_MS, [localValue]);
return (
<EuiFlexGroup>
<EuiFlexItem grow={true} css={{ alignItems: 'flex-end' }}>
<EuiFieldSearch
fullWidth
onSearch={setLocalValue}
isLoading={isLoading}
placeholder={i18n.translate(
'xpack.cloudDefend.policies.policySearchField.searchPlaceholder',
{ defaultMessage: 'Search integration name' }
)}
incremental
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};
export const Policies = () => {
const { pageSize, setPageSize } = usePageSize(LOCAL_STORAGE_PAGE_SIZE);
const [query, setQuery] = useState<UseCloudDefendPoliciesProps>({
name: '',
page: 1,
perPage: pageSize,
sortField: 'package_policy.name',
sortOrder: 'asc',
});
const queryResult = useCloudDefendPolicies(query);
const totalItemCount = queryResult.data?.total || 0;
return (
<CloudDefendPage>
<EuiPageHeader
data-test-subj={TEST_SUBJ.POLICIES_PAGE_HEADER}
pageTitle={
<CloudDefendPageTitle
title={i18n.translate('xpack.cloudDefend.policies.policiesPageHeader', {
defaultMessage: 'Defend for containers (D4C)',
})}
/>
}
rightSideItems={[<AddIntegrationButton />]}
bottomBorder
/>
<EuiSpacer />
<SearchField
isLoading={queryResult.isFetching}
onSearch={(name) => setQuery((current) => ({ ...current, name }))}
/>
<EuiSpacer />
<TotalIntegrationsCount
pageCount={(queryResult.data?.items || []).length}
totalCount={totalItemCount}
/>
<EuiSpacer size="s" />
<PoliciesTable
policies={queryResult.data?.items || []}
data-test-subj={TEST_SUBJ.POLICIES_TABLE_DATA_TEST_SUBJ}
error={queryResult.error ? extractErrorMessage(queryResult.error) : undefined}
loading={queryResult.isFetching}
pageIndex={query.page}
pageSize={pageSize || query.perPage}
sorting={{
// @ts-expect-error - EUI types currently do not support sorting by nested fields
sort: { field: query.sortField, direction: query.sortOrder },
allowNeutralSort: false,
}}
totalItemCount={totalItemCount}
setQuery={({ page, sort }) => {
setPageSize(page.size);
setQuery((current) => ({
...current,
page: page.index,
perPage: page.size,
sortField:
(sort?.field as UseCloudDefendPoliciesProps['sortField']) || current.sortField,
sortOrder: sort?.direction || current.sortOrder,
}));
}}
noItemsMessage={
queryResult.isSuccess && !queryResult.data.total ? (
<EmptyState name={query.name} />
) : undefined
}
/>
</CloudDefendPage>
);
};

View file

@ -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.
*/
export const POLICIES_PAGE_HEADER = 'policies-page-header';
export const POLICIES_TABLE_DATA_TEST_SUBJ = 'cloud_defend_policies_table';
export const ADD_INTEGRATION_TEST_SUBJ = 'cloud_defend_add_integration';
export const POLICIES_TABLE_COLUMNS = {
INTEGRATION_NAME: 'policies-table-column-integration-name',
AGENT_POLICY: 'policies-table-column-agent-policy',
NUMBER_OF_AGENTS: 'policies-table-column-number-of-agents',
CREATED_BY: 'policies-table-column-created-by',
CREATED_AT: 'policies-table-column-created-at',
};

View file

@ -1,50 +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 { useQuery } from '@tanstack/react-query';
import type { ListResult } from '@kbn/fleet-plugin/common';
import { CURRENT_API_VERSION, POLICIES_ROUTE_PATH } from '../../../common/constants';
import type { PoliciesQueryParams } from '../../../common';
import { useKibana } from '../../common/hooks/use_kibana';
import type { CloudDefendPolicy } from '../../../common';
const QUERY_KEY = 'cloud_defend_policies';
export interface UseCloudDefendPoliciesProps {
name: string;
page: number;
perPage: number;
sortField: PoliciesQueryParams['sort_field'];
sortOrder: PoliciesQueryParams['sort_order'];
}
export const useCloudDefendPolicies = ({
name,
perPage,
page,
sortField,
sortOrder,
}: UseCloudDefendPoliciesProps) => {
const { http } = useKibana().services;
const query: PoliciesQueryParams = {
policy_name: name,
per_page: perPage,
page,
sort_field: sortField,
sort_order: sortOrder,
};
return useQuery(
[QUERY_KEY, query],
() =>
http.get<ListResult<CloudDefendPolicy>>(POLICIES_ROUTE_PATH, {
version: CURRENT_API_VERSION,
query,
}),
{ keepPreviousData: true }
);
};

View file

@ -1,79 +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 { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
import React, { lazy, Suspense } from 'react';
import type { CloudDefendRouterProps } from './application/router';
import {
CloudDefendPluginSetup,
CloudDefendPluginStart,
CloudDefendPluginStartDeps,
CloudDefendPluginSetupDeps,
} from './types';
import { INTEGRATION_PACKAGE_NAME } from '../common/constants';
import { LoadingState } from './components/loading_state';
import { SetupContext } from './application/setup_context';
const LazyPolicyReplaceDefineStepExtension = lazy(
() => import('./components/fleet_extensions/package_policy_replace_define_step_extension')
);
const RouterLazy = lazy(() => import('./application/router'));
const Router = (props: CloudDefendRouterProps) => (
<Suspense fallback={<LoadingState />}>
<RouterLazy {...props} />
</Suspense>
);
export class CloudDefendPlugin
implements
Plugin<
CloudDefendPluginSetup,
CloudDefendPluginStart,
CloudDefendPluginSetupDeps,
CloudDefendPluginStartDeps
>
{
private isCloudEnabled?: boolean;
public setup(
core: CoreSetup<CloudDefendPluginStartDeps, CloudDefendPluginStart>,
plugins: CloudDefendPluginSetupDeps
): CloudDefendPluginSetup {
this.isCloudEnabled = plugins.cloud.isCloudEnabled;
// Return methods that should be available to other plugins
return {};
}
public start(core: CoreStart, plugins: CloudDefendPluginStartDeps): CloudDefendPluginStart {
plugins.fleet.registerExtension({
package: INTEGRATION_PACKAGE_NAME,
view: 'package-policy-replace-define-step',
Component: LazyPolicyReplaceDefineStepExtension,
});
const CloudDefendRouter = (props: CloudDefendRouterProps) => (
<KibanaContextProvider services={{ ...core, ...plugins }}>
<RedirectAppLinks coreStart={core}>
<div css={{ width: '100%', height: '100%' }}>
<SetupContext.Provider value={{ isCloudEnabled: this.isCloudEnabled }}>
<Router {...props} />
</SetupContext.Provider>
</div>
</RedirectAppLinks>
</KibanaContextProvider>
);
return {
getCloudDefendRouter: () => CloudDefendRouter,
};
}
public stop() {}
}

View file

@ -1,22 +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.
*/
class Worker {
constructor(stringUrl) {
this.url = stringUrl;
this.onmessage = () => {};
}
postMessage(msg) {
this.onmessage(msg);
}
terminate() {}
}
window.Worker = Worker;
export default Worker;

View file

@ -1,61 +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.
*/
/* eslint-disable @typescript-eslint/naming-convention */
import Chance from 'chance';
import type { CloudDefendPolicy } from '../../../common';
type CreateCloudDefendIntegrationFixtureInput = {
chance?: Chance.Chance;
} & Partial<CloudDefendPolicy>;
export const createCloudDefendIntegrationFixture = ({
chance = new Chance(),
package_policy = {
revision: chance?.integer(),
enabled: true,
id: chance.guid(),
name: chance.string(),
policy_ids: [chance.guid()],
namespace: chance.string(),
updated_at: chance.date().toISOString(),
updated_by: chance.word(),
created_at: chance.date().toISOString(),
created_by: chance.word(),
inputs: [
{
type: 'cloud_defend/control',
policy_template: 'cloud_defend',
enabled: true,
streams: [
{
id: chance?.guid(),
enabled: true,
data_stream: {
type: 'logs',
dataset: 'cloud_defend.alerts',
},
},
],
},
],
package: {
name: chance.string(),
title: chance.string(),
version: chance.string(),
},
},
agent_policy = {
id: chance.guid(),
name: chance.sentence(),
agents: chance.integer({ min: 0 }),
},
}: CreateCloudDefendIntegrationFixtureInput = {}): CloudDefendPolicy => ({
package_policy,
agent_policy,
});

View file

@ -1,24 +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 Chance from 'chance';
import type { CloudDefendPageNavigationItem } from '../../common/navigation/types';
type CreateNavigationItemFixtureInput = {
chance?: Chance.Chance;
} & Partial<CloudDefendPageNavigationItem>;
export const createPageNavigationItemFixture = ({
chance = new Chance(),
name = chance.word(),
path = `/${chance.word()}`,
disabled = undefined,
id = 'cloud_defend-policies',
}: CreateNavigationItemFixtureInput = {}): CloudDefendPageNavigationItem => ({
name,
path,
disabled,
id,
});

View file

@ -1,48 +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 { UseQueryResult } from '@tanstack/react-query';
interface CreateReactQueryResponseInput<TData = unknown, TError = unknown> {
status?: UseQueryResult['status'] | 'idle';
data?: TData;
error?: TError;
}
// TODO: Consider alternatives to using `Partial` over `UseQueryResult` for the return type:
// 1. Fully mock `UseQueryResult`
// 2. Mock the network layer instead of `useQuery` - see: https://tkdodo.eu/blog/testing-react-query
export const createReactQueryResponse = <TData = unknown, TError = unknown>({
status = 'loading',
error = undefined,
data = undefined,
}: CreateReactQueryResponseInput<TData, TError> = {}): Partial<UseQueryResult<TData, TError>> => {
if (status === 'success') {
return { status, data, isSuccess: true, isLoading: false, isError: false };
}
if (status === 'error') {
return { status, error, isSuccess: false, isLoading: false, isError: true };
}
if (status === 'loading') {
return { status, data: undefined, isSuccess: false, isLoading: true, isError: false };
}
if (status === 'idle') {
return {
status: 'loading',
data: undefined,
isSuccess: false,
isLoading: true,
isError: false,
fetchStatus: 'idle',
};
}
return { status };
};

View file

@ -1,169 +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 { NewPackagePolicy } from '@kbn/fleet-plugin/public';
import type { PackagePolicy } from '@kbn/fleet-plugin/common';
import { INTEGRATION_PACKAGE_NAME, INPUT_CONTROL, ALERTS_DATASET } from '../../common/constants';
import { MAX_SELECTORS_AND_RESPONSES_PER_TYPE } from '../common/constants';
export const MOCK_YAML_CONFIGURATION = `file:
selectors:
- name: default
operation:
- createExecutable
- modifyExecutable
- name: nginxOnly
containerImageName:
- nginx
operation:
- createExecutable
- modifyExecutable
- name: excludeCustomNginxBuild
containerImageTag:
- staging
responses:
- match:
- nginxOnly
exclude:
- excludeCustomNginxBuild
actions:
- alert
- block
- match:
- default
actions:
- alert
`;
// block on it's own should be prevented
export const MOCK_YAML_INVALID_ACTIONS = `file:
selectors:
- name: default
operation:
- createExecutable
- modifyExecutable
responses:
- match:
- default
actions:
- block
`;
export const MOCK_YAML_INVALID_STRING_ARRAY_CONDITION = `file:
selectors:
- name: default
operation:
- createExecutable
- modifyExecutable
targetFilePath:
- /bin/${new Array(256).fill('a').join()}
responses:
- match:
- default
actions:
- log
`;
export const MOCK_YAML_INVALID_CONFIGURATION = `
s
`;
export const MOCK_YAML_TOO_MANY_FILE_SELECTORS_RESPONSES = `file:
selectors:
- name: default
operation:
- createExecutable
- modifyExecutable
responses:
${new Array(MAX_SELECTORS_AND_RESPONSES_PER_TYPE + 1)
.fill(0)
.map(() => {
return ` - match: [default]
actions: [alert]
`;
})
.join('')}
`;
export const getCloudDefendNewPolicyMock = (yaml = MOCK_YAML_CONFIGURATION): NewPackagePolicy => ({
name: 'some-cloud_defend-policy',
description: '',
namespace: 'default',
policy_id: '',
policy_ids: [''],
enabled: true,
inputs: [
{
type: INPUT_CONTROL,
policy_template: INTEGRATION_PACKAGE_NAME,
enabled: true,
vars: {
configuration: {
type: 'yaml',
value: yaml,
},
},
streams: [
{
enabled: true,
data_stream: {
type: 'logs',
dataset: ALERTS_DATASET,
},
},
],
},
],
package: {
name: 'cloud_defend',
title: 'Container drift prevention',
version: '1.0.0',
},
});
export const getCloudDefendPolicyMock = (yaml = MOCK_YAML_CONFIGURATION): PackagePolicy => ({
id: 'c6d16e42-c32d-4dce-8a88-113cfe276ad1',
version: 'abcd',
revision: 1,
updated_at: '2020-06-25T16:03:38.159292',
updated_by: 'kibana',
created_at: '2020-06-25T16:03:38.159292',
created_by: 'kibana',
name: 'some-cloud_defend-policy',
description: '',
namespace: 'default',
policy_id: '',
policy_ids: [''],
enabled: true,
inputs: [
{
type: INPUT_CONTROL,
policy_template: INTEGRATION_PACKAGE_NAME,
enabled: true,
vars: {
configuration: {
type: 'yaml',
value: yaml,
},
},
streams: [
{
id: '1234',
enabled: true,
data_stream: {
type: 'logs',
dataset: ALERTS_DATASET,
},
},
],
},
],
package: {
name: 'cloud_defend',
title: 'Container drift prevention',
version: '1.0.0',
},
});

View file

@ -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 type { AppMountParameters, CoreStart } from '@kbn/core/public';
import React, { useMemo, FC, PropsWithChildren } from 'react';
import { I18nProvider } from '@kbn/i18n-react';
import { Router, Routes, Route } from '@kbn/shared-ux-router';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { coreMock } from '@kbn/core/public/mocks';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { fleetMock } from '@kbn/fleet-plugin/public/mocks';
import type { CloudDefendPluginStartDeps } from '../types';
import './__mocks__/worker';
import '@kbn/code-editor-mock/jest_helper';
// @ts-ignore-next
window.Worker = Worker;
interface CloudDefendAppDeps {
core: CoreStart;
deps: CloudDefendPluginStartDeps;
params: AppMountParameters;
}
export const TestProvider: FC<PropsWithChildren<Partial<CloudDefendAppDeps>>> = ({
core = coreMock.createStart(),
deps = {
data: dataPluginMock.createStartContract(),
fleet: fleetMock.createStartMock(),
},
params = coreMock.createAppMountParameters(),
children,
} = {}) => {
const queryClient = useMemo(() => new QueryClient(), []);
return (
<KibanaContextProvider services={{ ...core, ...deps }}>
<QueryClientProvider client={queryClient}>
<Router history={params.history}>
<I18nProvider>
<Routes>
<Route path="*" render={() => <>{children}</>} />
</Routes>
</I18nProvider>
</Router>
</QueryClientProvider>
</KibanaContextProvider>
);
};

View file

@ -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 { screen } from '@testing-library/react';
export const expectIdsInDoc = ({ be = [], notToBe = [] }: { be: string[]; notToBe?: string[] }) => {
be.forEach((testId) => {
expect(screen.getByTestId(testId)).toBeInTheDocument();
});
notToBe.forEach((testId) => {
expect(screen.queryByTestId(testId)).not.toBeInTheDocument();
});
};

View file

@ -1,199 +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 { CloudSetup } from '@kbn/cloud-plugin/public';
import type { LicensingPluginStart } from '@kbn/licensing-plugin/public';
import type { FleetSetup, FleetStart } from '@kbn/fleet-plugin/public';
import { NewPackagePolicy } from '@kbn/fleet-plugin/public';
import type { ComponentType, ReactNode } from 'react';
import type {
UsageCollectionSetup,
UsageCollectionStart,
} from '@kbn/usage-collection-plugin/public';
import type { CloudDefendRouterProps } from './application/router';
import type { CloudDefendPageId } from './common/navigation/types';
import * as i18n from './components/control_general_view/translations';
import { SelectorType, SelectorCondition, Selector, Response } from '../common';
/**
* cloud_defend plugin types
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface CloudDefendPluginSetup {}
export interface CloudDefendPluginStart {
/** Gets the cloud defend router component for embedding in the security solution. */
getCloudDefendRouter(): ComponentType<CloudDefendRouterProps>;
}
export interface CloudDefendPluginSetupDeps {
fleet: FleetSetup;
cloud: CloudSetup;
usageCollection?: UsageCollectionSetup;
}
export interface CloudDefendPluginStartDeps {
fleet: FleetStart;
licensing: LicensingPluginStart;
usageCollection?: UsageCollectionStart;
}
export interface CloudDefendSecuritySolutionContext {
/** Gets the `FiltersGlobal` component for embedding a filter bar in the security solution application. */
getFiltersGlobalComponent: () => ComponentType<{ children: ReactNode }>;
/** Gets the `SpyRoute` component for navigation highlighting and breadcrumbs. */
getSpyRouteComponent: () => ComponentType<{
pageName: CloudDefendPageId;
state?: Record<string, string | undefined>;
}>;
}
/**
* cloud_defend/control types
*/
/*
* 'stringArray' uses a EuiComboBox
* 'flag' is a boolean value which is always 'true'
* 'boolean' can be true or false
*/
export type SelectorConditionType = 'stringArray' | 'flag' | 'boolean';
export interface SelectorConditionOptions {
type: SelectorConditionType;
pattern?: string;
patternError?: string;
selectorType?: SelectorType;
maxValueBytes?: number; // defaults to const MAX_FILE_PATH_VALUE_LENGTH_BYTES
not?: SelectorCondition[];
values?:
| {
file?: string[];
process?: string[];
}
| string[];
}
export type SelectorConditionsMapProps = {
[key in SelectorCondition]: SelectorConditionOptions;
};
// used to determine UX control and allowed values for each condition
export const SelectorConditionsMap: SelectorConditionsMapProps = {
containerImageFullName: {
type: 'stringArray',
pattern:
'^(?:\\[[a-fA-F0-9:]+\\]|(?:[a-zA-Z0-9-](?:\\.[a-z0-9]+)*)+)(?::[0-9]+)?(?:\\/[a-z0-9]+(?:[._-][a-z0-9]+)*)+$',
patternError: i18n.errorInvalidFullContainerImageName,
not: ['containerImageName'],
},
containerImageName: {
type: 'stringArray',
pattern: '^([a-z0-9]+(?:[._-][a-z0-9]+)*)$',
not: ['containerImageFullName'],
},
containerImageTag: { type: 'stringArray' },
kubernetesClusterId: { type: 'stringArray' },
kubernetesClusterName: { type: 'stringArray' },
kubernetesNamespace: { type: 'stringArray' },
kubernetesPodName: { type: 'stringArray' },
kubernetesPodLabel: {
type: 'stringArray',
pattern: '^([a-zA-Z0-9\\.\\-]+\\/)?[a-zA-Z0-9\\.\\-]+:[a-zA-Z0-9\\.\\-\\_]*\\*?$',
patternError: i18n.errorInvalidPodLabel,
},
operation: {
type: 'stringArray',
values: {
file: ['createExecutable', 'modifyExecutable', 'createFile', 'modifyFile', 'deleteFile'],
process: ['fork', 'exec'],
},
},
targetFilePath: {
selectorType: 'file',
type: 'stringArray',
maxValueBytes: 255,
pattern: '^(?:\\/[^\\/\\*]+)*(?:\\/\\*|\\/\\*\\*)?$',
patternError: i18n.errorInvalidTargetFilePath,
},
ignoreVolumeFiles: { selectorType: 'file', type: 'flag', not: ['ignoreVolumeMounts'] },
ignoreVolumeMounts: { selectorType: 'file', type: 'flag', not: ['ignoreVolumeFiles'] },
processExecutable: {
selectorType: 'process',
type: 'stringArray',
not: ['processName'],
pattern: '^(?:\\/[^\\/\\*]+)*(?:\\/\\*|\\/\\*\\*)?$',
patternError: i18n.errorInvalidProcessExecutable,
},
processName: {
selectorType: 'process',
type: 'stringArray',
not: ['processExecutable'],
maxValueBytes: 15,
},
sessionLeaderInteractive: { selectorType: 'process', type: 'boolean' },
};
export const DefaultFileSelector: Selector = {
type: 'file',
name: 'Untitled',
operation: ['createExecutable', 'modifyExecutable'],
};
export const DefaultProcessSelector: Selector = {
type: 'process',
name: 'Untitled',
operation: ['fork', 'exec'],
};
export const DefaultFileResponse: Response = {
type: 'file',
match: [],
actions: ['alert'],
};
export const DefaultProcessResponse: Response = {
type: 'process',
match: [],
actions: ['alert'],
};
export interface OnChangeDeps {
isValid: boolean;
updatedPolicy: NewPackagePolicy;
}
export interface SettingsDeps {
policy: NewPackagePolicy;
onChange(opts: OnChangeDeps): void;
}
export interface ViewDeps extends SettingsDeps {
show: boolean;
}
export interface ControlGeneralViewSelectorDeps {
selector: Selector;
selectors: Selector[];
usedByResponse: boolean;
index: number;
onChange(selector: Selector, index: number): void;
onRemove(index: number): void;
onDuplicate(selector: Selector): void;
}
export interface ControlGeneralViewResponseDeps {
response: Response;
selectors: Selector[];
responses: Response[];
index: number;
onChange(response: Response, index: number): void;
onRemove(index: number): void;
onDuplicate(response: Response): void;
}
export interface ControlFormErrorMap {
[key: string]: string[];
}

View file

@ -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 { PluginInitializerContext } from '@kbn/core/server';
// This exports static code and TypeScript types,
// as well as, Kibana Platform `plugin()` initializer.
export async function plugin(initializerContext: PluginInitializerContext) {
const { CloudDefendPlugin } = await import('./plugin');
return new CloudDefendPlugin(initializerContext);
}
export type { CloudDefendPluginSetup, CloudDefendPluginStart } from './types';

View file

@ -1,36 +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 { ElasticsearchClient, type Logger } from '@kbn/core/server';
import { IndexStatus } from '../../common';
export const checkIndexStatus = async (
esClient: ElasticsearchClient,
index: string,
logger: Logger
): Promise<IndexStatus> => {
try {
const queryResult = await esClient.search({
index,
query: {
match_all: {},
},
size: 1,
});
return queryResult.hits.hits.length ? 'not-empty' : 'empty';
} catch (e) {
logger.debug(e);
if (e?.meta?.body?.error?.type === 'security_exception') {
logger.info(e);
return 'unprivileged';
}
// Assuming index doesn't exist
return 'empty';
}
};

View file

@ -1,153 +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 { flatMap, uniq } from 'lodash';
import type { SavedObjectsClientContract, Logger } from '@kbn/core/server';
import type {
AgentPolicyServiceInterface,
AgentService,
PackagePolicyClient,
} from '@kbn/fleet-plugin/server';
import type {
AgentPolicy,
GetAgentStatusResponse,
ListResult,
PackagePolicy,
} from '@kbn/fleet-plugin/common';
import { errors } from '@elastic/elasticsearch';
import { DataViewSavedObjectAttrs } from '@kbn/data-views-plugin/common';
import {
INPUT_CONTROL,
CLOUD_DEFEND_FLEET_PACKAGE_KUERY,
INTEGRATION_PACKAGE_NAME,
} from '../../common/constants';
import { POLICIES_PACKAGE_POLICY_PREFIX } from '../../common/constants';
import type { PoliciesQueryParams } from '../../common';
export const PACKAGE_POLICY_SAVED_OBJECT_TYPE = 'ingest-package-policies';
const isFleetMissingAgentHttpError = (error: unknown) =>
error instanceof errors.ResponseError && error.statusCode === 404;
const isPolicyTemplate = (input: any) => input === INPUT_CONTROL;
const getPackageNameQuery = (packageName: string, benchmarkFilter?: string): string => {
const integrationNameQuery = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${packageName}`;
const kquery = benchmarkFilter
? `${integrationNameQuery} AND ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.name: *${benchmarkFilter}*`
: integrationNameQuery;
return kquery;
};
export type AgentStatusByAgentPolicyMap = Record<string, GetAgentStatusResponse['results']>;
export const getAgentStatusesByAgentPolicies = async (
agentService: AgentService,
agentPolicies: AgentPolicy[] | undefined,
logger: Logger
): Promise<AgentStatusByAgentPolicyMap> => {
if (!agentPolicies?.length) return {};
const internalAgentService = agentService.asInternalUser;
const result: AgentStatusByAgentPolicyMap = {};
try {
for (const agentPolicy of agentPolicies) {
result[agentPolicy.id] = await internalAgentService.getAgentStatusForAgentPolicy(
agentPolicy.id
);
}
} catch (error) {
if (isFleetMissingAgentHttpError(error)) {
logger.debug('failed to get agent status for agent policy');
} else {
throw error;
}
}
return result;
};
export const onPackagePolicyPostCreateCallback = async (
logger: Logger,
packagePolicy: PackagePolicy,
savedObjectsClient: SavedObjectsClientContract
): Promise<void> => {
return addDataViewToAllSpaces(savedObjectsClient);
};
async function addDataViewToAllSpaces(savedObjectsClient: SavedObjectsClientContract) {
const cloudDefendDataViews = await savedObjectsClient.find<DataViewSavedObjectAttrs>({
type: 'index-pattern',
fields: ['title'],
search: INTEGRATION_PACKAGE_NAME + '*',
searchFields: ['title'],
perPage: 100,
});
await Promise.all(
cloudDefendDataViews.saved_objects.map((dataView) =>
savedObjectsClient.updateObjectsSpaces(
[{ id: dataView.id, type: 'index-pattern' }],
['*'],
[]
)
)
);
}
export const getCloudDefendAgentPolicies = async (
soClient: SavedObjectsClientContract,
packagePolicies: PackagePolicy[],
agentPolicyService: AgentPolicyServiceInterface
): Promise<AgentPolicy[]> =>
agentPolicyService.getByIds(soClient, uniq(flatMap(packagePolicies, 'policy_ids')), {
withPackagePolicies: true,
ignoreMissing: true,
});
export const getCloudDefendPackagePolicies = (
soClient: SavedObjectsClientContract,
packagePolicyService: PackagePolicyClient,
packageName: string,
queryParams: Partial<PoliciesQueryParams>
): Promise<ListResult<PackagePolicy>> => {
const sortField = queryParams.sort_field?.replaceAll(POLICIES_PACKAGE_POLICY_PREFIX, '');
return packagePolicyService.list(soClient, {
kuery: getPackageNameQuery(packageName, queryParams.policy_name),
page: queryParams.page,
perPage: queryParams.per_page,
sortField,
sortOrder: queryParams.sort_order,
});
};
export const getInstalledPolicyTemplates = async (
packagePolicyClient: PackagePolicyClient,
soClient: SavedObjectsClientContract
) => {
try {
// getting all installed cloud_defend package policies
const queryResult = await packagePolicyClient.list(soClient, {
kuery: CLOUD_DEFEND_FLEET_PACKAGE_KUERY,
perPage: 1000,
});
// getting installed policy templates
const enabledPolicyTemplates = queryResult.items
.map((policy) => {
return policy.inputs.find((input) => input.enabled)?.policy_template;
})
.filter(isPolicyTemplate);
// removing duplicates
return [...new Set(enabledPolicyTemplates)];
} catch (e) {
return [];
}
};

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