diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5fcb619af6570..c91d1a702b7ec 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -315,7 +315,6 @@ /src/plugins/es_ui_shared/ @elastic/kibana-stack-management /x-pack/plugins/cross_cluster_replication/ @elastic/kibana-stack-management /x-pack/plugins/index_lifecycle_management/ @elastic/kibana-stack-management -/x-pack/plugins/console_extensions/ @elastic/kibana-stack-management /x-pack/plugins/grokdebugger/ @elastic/kibana-stack-management /x-pack/plugins/index_management/ @elastic/kibana-stack-management /x-pack/plugins/license_api_guard/ @elastic/kibana-stack-management @@ -330,7 +329,6 @@ /x-pack/plugins/ingest_pipelines/ @elastic/kibana-stack-management /packages/kbn-ace/ @elastic/kibana-stack-management /packages/kbn-monaco/ @elastic/kibana-stack-management -#CC# /x-pack/plugins/console_extensions/ @elastic/kibana-stack-management #CC# /x-pack/plugins/cross_cluster_replication/ @elastic/kibana-stack-management # Security Solution diff --git a/.github/workflows/project-assigner.yml b/.github/workflows/project-assigner.yml index 4966a0b506317..f4e62648a9741 100644 --- a/.github/workflows/project-assigner.yml +++ b/.github/workflows/project-assigner.yml @@ -8,8 +8,16 @@ jobs: name: Assign issue or PR to project based on label steps: - name: Assign to project - uses: elastic/github-actions/project-assigner@v2.0.0 + uses: elastic/github-actions/project-assigner@v2.1.0 id: project_assigner with: - issue-mappings: '[{"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"}, {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"}, {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Input Controls", "projectNumber": 72, "columnName": "Inbox"}]' + issue-mappings: | + [ + {"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"}, + {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"}, + {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"}, + {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"}, + {"label": "Feature:Input Controls", "projectNumber": 72, "columnName": "Inbox"}, + {"label": "Team:Security", "projectNumber": 320, "columnName": "Awaiting triage", "projectScope": "org"} + ] ghToken: ${{ secrets.PROJECT_ASSIGNER_TOKEN }} diff --git a/config/kibana.yml b/config/kibana.yml index eefb6bb8bacda..dea9849f17b28 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -42,6 +42,10 @@ #elasticsearch.username: "kibana_system" #elasticsearch.password: "pass" +# Kibana can also authenticate to Elasticsearch via "service account tokens". +# If may use this token instead of a username/password. +# elasticsearch.serviceAccountToken: "my_token" + # Enables SSL and paths to the PEM-format SSL certificate and SSL key files, respectively. # These settings enable SSL for outgoing requests from the Kibana server to the browser. #server.ssl.enabled: false diff --git a/dev_docs/tutorials/debugging.mdx b/dev_docs/tutorials/debugging.mdx new file mode 100644 index 0000000000000..c0efd249be066 --- /dev/null +++ b/dev_docs/tutorials/debugging.mdx @@ -0,0 +1,61 @@ +--- +id: kibDevTutorialDebugging +slug: /kibana-dev-docs/tutorial/debugging +title: Debugging in development +summary: Learn how to debug Kibana while running from source +date: 2021-04-26 +tags: ['kibana', 'onboarding', 'dev', 'tutorials', 'debugging'] +--- + +There are multiple ways to go about debugging Kibana when running from source. + +## Debugging using Chrome DevTools + +You will need to run Node using `--inspect` or `--inspect-brk` in order to enable the inspector. Additional information can be found in the [Node.js docs](https://nodejs.org/en/docs/guides/debugging-getting-started/). + +Once Node is running with the inspector enabled, you can open `chrome://inspect` in your Chrome browser. You should see a remote target for the inspector running. Click "inspect". You can now begin using the debugger. + +Next we will go over how to exactly enable the inspector for different aspects of the codebase. + +### Jest Unit Tests + +You will need to run Jest directly from the Node script: + +`node --inspect-brk scripts/jest [TestPathPattern]` + +### Functional Test Runner + +`node --inspect-brk scripts/functional_test_runner` + +### Development Server + +`node --inspect-brk scripts/kibana` + +## Debugging using logging + +When running Kibana, it's sometimes helpful to enable verbose logging. + +`yarn start --verbose` + +Using verbose logging usually results in much more information than you're interested in. The [logging documentation](https://www.elastic.co/guide/en/kibana/current/logging-settings.html) covers ways to change the log level of certain types. + +In the following example of a configuration stored in `config/kibana.dev.yml` we are logging all Elasticsearch queries and any logs created by the Management plugin. + +``` +logging: + appenders: + console: + type: console + layout: + type: pattern + highlight: true + root: + appenders: [default, console] + level: info + + loggers: + - name: plugins.management + level: debug + - name: elasticsearch.query + level: debug +``` \ No newline at end of file diff --git a/docs/apm/agent-configuration.asciidoc b/docs/apm/agent-configuration.asciidoc index 2574d254ac14c..f2e07412c4a38 100644 --- a/docs/apm/agent-configuration.asciidoc +++ b/docs/apm/agent-configuration.asciidoc @@ -43,6 +43,7 @@ Supported configurations are also tagged with the image:./images/dynamic-config. [horizontal] Go Agent:: {apm-go-ref}/configuration.html[Configuration reference] +iOS agent:: _Not yet supported_ Java Agent:: {apm-java-ref}/configuration.html[Configuration reference] .NET Agent:: {apm-dotnet-ref}/configuration.html[Configuration reference] Node.js Agent:: {apm-node-ref}/configuration.html[Configuration reference] diff --git a/docs/apm/apm-alerts.asciidoc b/docs/apm/apm-alerts.asciidoc index 3e3e2b178ff10..42016ac08bfc7 100644 --- a/docs/apm/apm-alerts.asciidoc +++ b/docs/apm/apm-alerts.asciidoc @@ -1,69 +1,57 @@ [role="xpack"] [[apm-alerts]] -=== Alerts +=== Alerts and rules ++++ Create an alert ++++ +The APM app allows you to define **rules** to detect complex conditions within your APM data +and trigger built-in **actions** when those conditions are met. -The APM app integrates with Kibana's {kibana-ref}/alerting-getting-started.html[alerting and actions] feature. -It provides a set of built-in **actions** and APM specific threshold **alerts** for you to use -and enables central management of all alerts from <>. +The following **rules** are supported: + +* Latency anomaly rule: +Alert when latency of a service is abnormal +* Transaction error rate threshold rule: +Alert when the service's transaction error rate is above the defined threshold +* Error count threshold rule: +Alert when the number of errors in a service exceeds a defined threshold [role="screenshot"] image::apm/images/apm-alert.png[Create an alert in the APM app] -For a walkthrough of the alert flyout panel, including detailed information on each configurable property, -see Kibana's <>. - -The APM app supports four different types of alerts: - -* Transaction duration anomaly: -alerts when the service's transaction duration reaches a certain anomaly score -* Transaction duration threshold: -alerts when the service's transaction duration exceeds a given time limit over a given time frame -* Transaction error rate threshold: -alerts when the service's transaction error rate is above the selected rate over a given time frame -* Error count threshold: -alerts when service exceeds a selected number of errors over a given time frame +For a complete walkthrough of the **Create rule** flyout panel, including detailed information on each configurable property, +see Kibana's <>. -Below, we'll walk through the creation of two of these alerts. +Below, we'll walk through the creation of two APM rules. [float] [[apm-create-transaction-alert]] -=== Example: create a transaction duration alert +=== Example: create a latency anomaly rule -Transaction duration alerts trigger when the duration of a specific transaction type in a service exceeds a defined threshold. -This guide will create an alert for the `opbeans-java` service based on the following criteria: +Latency anomaly rules trigger when the latency of a service is abnormal. +This guide will create an alert for all services based on the following criteria: -* Environment: Production -* Transaction type: `transaction.type:request` -* Average request is above `1500ms` for the last 5 minutes -* Check every 10 minutes, and repeat the alert every 30 minutes -* Send the alert via Slack +* Environment: production +* Severity level: critical +* Run every five minutes +* Send an alert to a Slack channel only when the rule status changes -From the APM app, navigate to the `opbeans-java` service and select -**Alerts** > **Create threshold alert** > **Transaction duration**. +From any page in the APM app, select **Alerts and rules** > **Latency** > **Create anomaly rule**. +Change the name of the alert, but do not edit the tags. -`Transaction duration | opbeans-java` is automatically set as the name of the alert, -and `apm` and `service.name:opbeans-java` are added as tags. -It's fine to change the name of the alert, but do not edit the tags. +Based on the criteria above, define the following rule details: -Based on the alert criteria, define the following alert details: +* **Check every** - `5 minutes` +* **Notify** - "Only on status change" +* **Environment** - `all` +* **Has anomaly with severity** - `critical` -* **Check every** - `10 minutes` -* **Notify every** - `30 minutes` -* **TYPE** - `request` -* **WHEN** - `avg` -* **IS ABOVE** - `1500ms` -* **FOR THE LAST** - `5 minutes` - -Select an action type. -Multiple action types can be selected, but in this example, we want to post to a Slack channel. +Next, add a connector. Multiple connectors can be selected, but in this example we're interested in Slack. Select **Slack** > **Create a connector**. Enter a name for the connector, -and paste the webhook URL. +and paste your Slack webhook URL. See Slack's webhook documentation if you need to create one. A default message is provided as a starting point for your alert. @@ -72,35 +60,32 @@ to pass additional alert values at the time a condition is detected to an action A list of available variables can be accessed by selecting the **add variable** button image:apm/images/add-variable.png[add variable button]. -Select **Save**. The alert has been created and is now active! +Click **Save**. The rule has been created and is now active! [float] [[apm-create-error-alert]] -=== Example: create an error rate alert +=== Example: create an error count threshold alert -Error rate alerts trigger when the number of errors in a service exceeds a defined threshold. -This guide creates an alert for the `opbeans-python` service based on the following criteria: +The error count threshold alert triggers when the number of errors in a service exceeds a defined threshold. +This guide will create an alert for all services based on the following criteria: -* Environment: Production +* All environments * Error rate is above 25 for the last minute -* Check every 1 minute, and repeat the alert every 10 minutes -* Send the alert via email to the `opbeans-python` team - -From the APM app, navigate to the `opbeans-python` service and select -**Alerts** > **Create threshold alert** > **Error rate**. +* Check every 1 minute, and alert every time the rule is active +* Send the alert via email to the site reliability team -`Error rate | opbeans-python` is automatically set as the name of the alert, -and `apm` and `service.name:opbeans-python` are added as tags. -It's fine to change the name of the alert, but do not edit the tags. +From any page in the APM app, select **Alerts and rules** > **Error count** > **Create threshold rule**. +Change the name of the alert, but do not edit the tags. -Based on the alert criteria, define the following alert details: +Based on the criteria above, define the following rule details: * **Check every** - `1 minute` -* **Notify every** - `10 minutes` -* **IS ABOVE** - `25 errors` -* **FOR THE LAST** - `1 minute` +* **Notify** - "Every time alert is active" +* **Environment** - `all` +* **Is above** - `25 errors` +* **For the last** - `1 minute` -Select the **Email** action type and click **Create a connector**. +Select the **Email** connector and click **Create a connector**. Fill out the required details: sender, host, port, etc., and click **save**. A default message is provided as a starting point for your alert. @@ -109,14 +94,14 @@ to pass additional alert values at the time a condition is detected to an action A list of available variables can be accessed by selecting the **add variable** button image:apm/images/add-variable.png[add variable button]. -Select **Save**. The alert has been created and is now active! +Click **Save**. The alert has been created and is now active! [float] [[apm-alert-manage]] -=== Manage alerts and actions +=== Manage alerts and rules -From the APM app, select **Alerts** > **View active alerts** to be taken to the Kibana alerts and actions management page. -From this page, you can create, edit, disable, mute, and delete alerts, and create, edit, and disable connectors. +From the APM app, select **Alerts and rules** > **Manage rules** to be taken to the Kibana **Rules and Connectors** page. +From this page, you can disable, mute, and delete APM alerts. [float] [[apm-alert-more-info]] @@ -126,4 +111,4 @@ See {kibana-ref}/alerting-getting-started.html[alerting and actions] for more in NOTE: If you are using an **on-premise** Elastic Stack deployment with security, communication between Elasticsearch and Kibana must have TLS configured. -More information is in the alerting {kibana-ref}/alerting-setup.html#alerting-prerequisites[prerequisites]. \ No newline at end of file +More information is in the alerting {kibana-ref}/alerting-setup.html#alerting-prerequisites[prerequisites]. diff --git a/docs/apm/filters.asciidoc b/docs/apm/filters.asciidoc index 56602ab7c05c9..c0ea81c87378b 100644 --- a/docs/apm/filters.asciidoc +++ b/docs/apm/filters.asciidoc @@ -36,6 +36,7 @@ It's vital to be consistent when naming environments in your agents. To learn how to configure service environments, see the specific agent documentation: * *Go:* {apm-go-ref}/configuration.html#config-environment[`ELASTIC_APM_ENVIRONMENT`] +* *iOS agent:* _Not yet supported_ * *Java:* {apm-java-ref}/config-core.html#config-environment[`environment`] * *.NET:* {apm-dotnet-ref}/config-core.html#config-environment[`Environment`] * *Node.js:* {apm-node-ref}/configuration.html#environment[`environment`] diff --git a/docs/apm/images/apm-agent-configuration.png b/docs/apm/images/apm-agent-configuration.png index 07398f0609187..22fd9d75c3d73 100644 Binary files a/docs/apm/images/apm-agent-configuration.png and b/docs/apm/images/apm-agent-configuration.png differ diff --git a/docs/apm/images/apm-alert.png b/docs/apm/images/apm-alert.png index 2ac91b6b19219..a845d65dd24a5 100644 Binary files a/docs/apm/images/apm-alert.png and b/docs/apm/images/apm-alert.png differ diff --git a/docs/apm/images/apm-error-group.png b/docs/apm/images/apm-error-group.png index 359bdc6b704e9..1326e97f757d6 100644 Binary files a/docs/apm/images/apm-error-group.png and b/docs/apm/images/apm-error-group.png differ diff --git a/docs/apm/images/apm-logs-tab.png b/docs/apm/images/apm-logs-tab.png index 77aecf744bc7f..891d2b7a1dd69 100644 Binary files a/docs/apm/images/apm-logs-tab.png and b/docs/apm/images/apm-logs-tab.png differ diff --git a/docs/apm/images/apm-services-overview.png b/docs/apm/images/apm-services-overview.png index 1c16ac5b572c3..7aeb5f1ac379f 100644 Binary files a/docs/apm/images/apm-services-overview.png and b/docs/apm/images/apm-services-overview.png differ diff --git a/docs/apm/images/apm-settings.png b/docs/apm/images/apm-settings.png index c821b7fb76e79..2201ed5fcaa72 100644 Binary files a/docs/apm/images/apm-settings.png and b/docs/apm/images/apm-settings.png differ diff --git a/docs/apm/images/apm-span-detail.png b/docs/apm/images/apm-span-detail.png index bacb2d372c166..c9f55575b2232 100644 Binary files a/docs/apm/images/apm-span-detail.png and b/docs/apm/images/apm-span-detail.png differ diff --git a/docs/apm/images/apm-traces.png b/docs/apm/images/apm-traces.png index 0e9062ee448b4..ee16f9ed16a18 100644 Binary files a/docs/apm/images/apm-traces.png and b/docs/apm/images/apm-traces.png differ diff --git a/docs/apm/images/apm-transaction-duration-dist.png b/docs/apm/images/apm-transaction-duration-dist.png index 863f493f20db4..91ae6c3a59ad2 100644 Binary files a/docs/apm/images/apm-transaction-duration-dist.png and b/docs/apm/images/apm-transaction-duration-dist.png differ diff --git a/docs/apm/images/apm-transaction-response-dist.png b/docs/apm/images/apm-transaction-response-dist.png index 2f3e69f263a28..70e5ad7041287 100644 Binary files a/docs/apm/images/apm-transaction-response-dist.png and b/docs/apm/images/apm-transaction-response-dist.png differ diff --git a/docs/apm/images/apm-transaction-sample.png b/docs/apm/images/apm-transaction-sample.png index 0e4bc5f3f878a..54eea902f0311 100644 Binary files a/docs/apm/images/apm-transaction-sample.png and b/docs/apm/images/apm-transaction-sample.png differ diff --git a/docs/apm/images/apm-transactions-overview.png b/docs/apm/images/apm-transactions-overview.png index be292c37e24e0..66cf739a861b7 100644 Binary files a/docs/apm/images/apm-transactions-overview.png and b/docs/apm/images/apm-transactions-overview.png differ diff --git a/docs/apm/images/service-maps-java.png b/docs/apm/images/service-maps-java.png index d7c0786e406d9..25600b690a5bd 100644 Binary files a/docs/apm/images/service-maps-java.png and b/docs/apm/images/service-maps-java.png differ diff --git a/docs/apm/images/service-maps.png b/docs/apm/images/service-maps.png index 190b7af3c560e..511d8401b22f3 100644 Binary files a/docs/apm/images/service-maps.png and b/docs/apm/images/service-maps.png differ diff --git a/docs/apm/service-maps.asciidoc b/docs/apm/service-maps.asciidoc index 99a6205ae010e..f43253d819429 100644 --- a/docs/apm/service-maps.asciidoc +++ b/docs/apm/service-maps.asciidoc @@ -108,6 +108,7 @@ Service maps are supported for the following Agent versions: [horizontal] Go agent:: ≥ v1.7.0 +iOS agent:: _Not yet supported_ Java agent:: ≥ v1.13.0 .NET agent:: ≥ v1.3.0 Node.js agent:: ≥ v3.6.0 diff --git a/docs/apm/transactions.asciidoc b/docs/apm/transactions.asciidoc index c2a3e0bc2502d..76006d375d075 100644 --- a/docs/apm/transactions.asciidoc +++ b/docs/apm/transactions.asciidoc @@ -100,22 +100,22 @@ the selected transaction group. image::apm/images/apm-transaction-response-dist.png[Example view of response time distribution] [[transaction-duration-distribution]] -==== Transactions duration distribution +==== Latency distribution -This chart plots all transaction durations for the given time period. +A plot of all transaction durations for the given time period. The screenshot below shows a typical distribution, and indicates most of our requests were served quickly -- awesome! -It's the requests on the right, the ones taking longer than average, that we probably want to focus on. +It's the requests on the right, the ones taking longer than average, that we probably need to focus on. [role="screenshot"] -image::apm/images/apm-transaction-duration-dist.png[Example view of transactions duration distribution graph] +image::apm/images/apm-transaction-duration-dist.png[Example view of latency distribution graph] -Select a transaction duration _bucket_ to display up to ten trace samples. +Select a latency duration _bucket_ to display up to ten trace samples. [[transaction-trace-sample]] ==== Trace sample -Trace samples are based on the _bucket_ selection in the *Transactions duration distribution* chart; +Trace samples are based on the _bucket_ selection in the *Latency distribution* chart; update the samples by selecting a new _bucket_. The number of requests per bucket is displayed when hovering over the graph, and the selected bucket is highlighted to stand out. diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc index 8cab7bb03da75..4a62f71528676 100644 --- a/docs/apm/troubleshooting.asciidoc +++ b/docs/apm/troubleshooting.asciidoc @@ -15,6 +15,7 @@ don't forget to check our other troubleshooting guides or discussion forum: * {apm-server-ref}/troubleshooting.html[APM Server troubleshooting] * {apm-dotnet-ref}/troubleshooting.html[.NET agent troubleshooting] * {apm-go-ref}/troubleshooting.html[Go agent troubleshooting] +* {apm-ios-ref}/troubleshooting.html[iOS agent troubleshooting] * {apm-java-ref}/trouble-shooting.html[Java agent troubleshooting] * {apm-node-ref}/troubleshooting.html[Node.js agent troubleshooting] * {apm-php-ref}/troubleshooting.html[PHP agent troubleshooting] diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index eee92ba433721..5f49360c926bf 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -354,10 +354,6 @@ The plugin exposes the static DefaultEditorController class to consume. The client-side plugin configures following values: -|{kib-repo}blob/{branch}/x-pack/plugins/console_extensions/README.md[consoleExtensions] -|This plugin provides autocomplete definitions of licensed APIs to the OSS Console plugin. - - |{kib-repo}blob/{branch}/x-pack/plugins/cross_cluster_replication/README.md[crossClusterReplication] |You can run a local cluster and simulate a remote cluster within a single Kibana directory. @@ -393,7 +389,7 @@ security and spaces filtering as well as performing audit logging. |{kib-repo}blob/{branch}/x-pack/plugins/enterprise_search/README.md[enterpriseSearch] -|This plugin's goal is to provide a Kibana user interface to the Enterprise Search solution's products (App Search and Workplace Search). In it's current MVP state, the plugin provides the following with the goal of gathering user feedback and raising product awareness: +|This plugin provides beta Kibana user interfaces for managing the Enterprise Search solution and its products, App Search and Workplace Search. |{kib-repo}blob/{branch}/x-pack/plugins/event_log/README.md[eventLog] diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchclientconfig.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchclientconfig.md index a854e5ddad19a..208e0e0175d71 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchclientconfig.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchclientconfig.md @@ -9,7 +9,7 @@ Configuration options to be used to create a [cluster client](./kibana-plugin-co Signature: ```typescript -export declare type ElasticsearchClientConfig = Pick & { +export declare type ElasticsearchClientConfig = Pick & { pingTimeout?: ElasticsearchConfig['pingTimeout'] | ClientOptions['pingTimeout']; requestTimeout?: ElasticsearchConfig['requestTimeout'] | ClientOptions['requestTimeout']; ssl?: Partial; diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.md index d87ea63d59b8d..a9ed614ba7552 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.md @@ -31,10 +31,11 @@ export declare class ElasticsearchConfig | [pingTimeout](./kibana-plugin-core-server.elasticsearchconfig.pingtimeout.md) | | Duration | Timeout after which PING HTTP request will be aborted and retried. | | [requestHeadersWhitelist](./kibana-plugin-core-server.elasticsearchconfig.requestheaderswhitelist.md) | | string[] | List of Kibana client-side headers to send to Elasticsearch when request scoped cluster client is used. If this is an empty array then \*no\* client-side will be sent. | | [requestTimeout](./kibana-plugin-core-server.elasticsearchconfig.requesttimeout.md) | | Duration | Timeout after which HTTP request will be aborted and retried. | +| [serviceAccountToken](./kibana-plugin-core-server.elasticsearchconfig.serviceaccounttoken.md) | | string | If Elasticsearch security features are enabled, this setting provides the service account token that the Kibana server users to perform its administrative functions.This is an alternative to specifying a username and password. | | [shardTimeout](./kibana-plugin-core-server.elasticsearchconfig.shardtimeout.md) | | Duration | Timeout for Elasticsearch to wait for responses from shards. Set to 0 to disable. | | [sniffInterval](./kibana-plugin-core-server.elasticsearchconfig.sniffinterval.md) | | false | Duration | Interval to perform a sniff operation and make sure the list of nodes is complete. If false then sniffing is disabled. | | [sniffOnConnectionFault](./kibana-plugin-core-server.elasticsearchconfig.sniffonconnectionfault.md) | | boolean | Specifies whether the client should immediately sniff for a more current list of nodes when a connection dies. | | [sniffOnStart](./kibana-plugin-core-server.elasticsearchconfig.sniffonstart.md) | | boolean | Specifies whether the client should attempt to detect the rest of the cluster when it is first instantiated. | | [ssl](./kibana-plugin-core-server.elasticsearchconfig.ssl.md) | | Pick<SslConfigSchema, Exclude<keyof SslConfigSchema, 'certificateAuthorities' | 'keystore' | 'truststore'>> & {
certificateAuthorities?: string[];
} | Set of settings configure SSL connection between Kibana and Elasticsearch that are required when xpack.ssl.verification_mode in Elasticsearch is set to either certificate or full. | -| [username](./kibana-plugin-core-server.elasticsearchconfig.username.md) | | string | If Elasticsearch is protected with basic authentication, this setting provides the username that the Kibana server uses to perform its administrative functions. | +| [username](./kibana-plugin-core-server.elasticsearchconfig.username.md) | | string | If Elasticsearch is protected with basic authentication, this setting provides the username that the Kibana server uses to perform its administrative functions. Cannot be used in conjunction with serviceAccountToken. | diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.serviceaccounttoken.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.serviceaccounttoken.md new file mode 100644 index 0000000000000..5934e83de17a4 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.serviceaccounttoken.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchConfig](./kibana-plugin-core-server.elasticsearchconfig.md) > [serviceAccountToken](./kibana-plugin-core-server.elasticsearchconfig.serviceaccounttoken.md) + +## ElasticsearchConfig.serviceAccountToken property + +If Elasticsearch security features are enabled, this setting provides the service account token that the Kibana server users to perform its administrative functions. + +This is an alternative to specifying a username and password. + +Signature: + +```typescript +readonly serviceAccountToken?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.username.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.username.md index 14db9f2e36ccf..959870ff43a0f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.username.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchconfig.username.md @@ -4,7 +4,7 @@ ## ElasticsearchConfig.username property -If Elasticsearch is protected with basic authentication, this setting provides the username that the Kibana server uses to perform its administrative functions. +If Elasticsearch is protected with basic authentication, this setting provides the username that the Kibana server uses to perform its administrative functions. Cannot be used in conjunction with serviceAccountToken. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearchclientconfig.md b/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearchclientconfig.md index b028a09bee453..a80ebe2fee493 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearchclientconfig.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyelasticsearchclientconfig.md @@ -11,7 +11,7 @@ Signature: ```typescript -export declare type LegacyElasticsearchClientConfig = Pick & Pick & { +export declare type LegacyElasticsearchClientConfig = Pick & Pick & { pingTimeout?: ElasticsearchConfig['pingTimeout'] | ConfigOptions['pingTimeout']; requestTimeout?: ElasticsearchConfig['requestTimeout'] | ConfigOptions['requestTimeout']; sniffInterval?: ElasticsearchConfig['sniffInterval'] | ConfigOptions['sniffInterval']; diff --git a/docs/discover/search-sessions.asciidoc b/docs/discover/search-sessions.asciidoc index b503e8cfba3b4..652583db785ad 100644 --- a/docs/discover/search-sessions.asciidoc +++ b/docs/discover/search-sessions.asciidoc @@ -72,15 +72,28 @@ behaves differently: [float] ==== Limitations -Certain visualization features do not fully support background search sessions yet. If a dashboard using these features gets restored, -all panels using unsupported features won't load immediately, but instead send out additional data requests which can take a while to complete. -In this case a warning *Your search session is still running* will be shown. +Certain visualization features do not fully support background search sessions. If a dashboard +using these features is restored, +all panels using unsupported features won't load immediately, but instead send out additional +data requests, which can take a while to complete. +The warning *Your search session is still running* is shown. -You can either wait for these additional requests to complete or come back to the dashboard later when all data requests have been finished. +You can either wait for these additional requests to complete or come back to the dashboard later +when all data requests have finished. A panel on a dashboard can behave like this if one of the following features is used: -* *Lens* - A *top values* dimension with an enabled setting *Group other values as "Other"* (configurable in the *Advanced* section of the dimension) -* *Lens* - An *intervals* dimension is used -* *Aggregation based* visualizations - A *terms* aggregation is used with an enabled setting *Group other values in separate bucket* -* *Aggregation based* visualizations - A *histogram* aggregation is used -* *Maps* - Layers using joins, blended layers or tracks layers are used + +**Lens** + +* A *top values* dimension with an enabled *Group other values as "Other"* setting. +This is configurable in the *Advanced* section of the dimension. +* An *intervals* dimension. + +**Aggregation based** visualizations + +* A *terms* aggregation with an enabled *Group other values in separate bucket* setting. +* A *histogram* aggregation. + +**Maps** + +* Layers using joins, blended layers, or tracks layers. diff --git a/docs/maps/vector-layer.asciidoc b/docs/maps/vector-layer.asciidoc index 2115c16a889c6..5017ecf91dffd 100644 --- a/docs/maps/vector-layer.asciidoc +++ b/docs/maps/vector-layer.asciidoc @@ -20,10 +20,10 @@ The index must contain at least one field mapped as {ref}/geo-point.html[geo_poi Results are limited to the `index.max_result_window` index setting, which defaults to 10000. Select the appropriate *Scaling* option for your use case. + -* *Limit results to 10000.* The layer displays features from the first `index.max_result_window` documents. +* *Limit results to 10,000* The layer displays features from the first `index.max_result_window` documents. Results exceeding `index.max_result_window` are not displayed. -* *Show clusters when results exceed 10000.* When results exceed `index.max_result_window`, the layer uses {ref}/search-aggregations-bucket-geotilegrid-aggregation.html[GeoTile grid aggregation] to group your documents into clusters and displays metrics for each cluster. When results are less then `index.max_result_window`, the layer displays features from individual documents. +* *Show clusters when results exceed 10,000* When results exceed `index.max_result_window`, the layer uses {ref}/search-aggregations-bucket-geotilegrid-aggregation.html[GeoTile grid aggregation] to group your documents into clusters and displays metrics for each cluster. When results are less then `index.max_result_window`, the layer displays features from individual documents. * *Use vector tiles.* Vector tiles partition your map into 6 to 8 tiles. Each tile request is limited to the `index.max_result_window` index setting. diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc index 79fa9a642428a..dfb239f0e26c0 100644 --- a/docs/settings/apm-settings.asciidoc +++ b/docs/settings/apm-settings.asciidoc @@ -18,7 +18,7 @@ It is enabled by default. // Any changes made in this file will be seen there as well. // tag::apm-indices-settings[] -Index defaults can be changed in Kibana. Open the main menu, then click *APM > Settings > Indices*. +Index defaults can be changed in the APM app. Select **Settings** > **Indices**. Index settings in the APM app take precedence over those set in `kibana.yml`. [role="screenshot"] diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index ba333deeb1609..15abd0fa4ad96 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -284,6 +284,11 @@ the username and password that the {kib} server uses to perform maintenance on the {kib} index at startup. {kib} users still need to authenticate with {es}, which is proxied through the {kib} server. +|[[elasticsearch-service-account-token]] `elasticsearch.serviceAccountToken:` + | beta[]. If your {es} is protected with basic authentication, this token provides the credentials +that the {kib} server uses to perform maintenance on the {kib} index at startup. This setting +is an alternative to `elasticsearch.username` and `elasticsearch.password`. + | `enterpriseSearch.host` | The URL of your Enterprise Search instance diff --git a/docs/user/dashboard/lens-advanced.asciidoc b/docs/user/dashboard/lens-advanced.asciidoc index 33e0e362058f4..374d36e7586c4 100644 --- a/docs/user/dashboard/lens-advanced.asciidoc +++ b/docs/user/dashboard/lens-advanced.asciidoc @@ -301,9 +301,6 @@ image::images/lens_advanced_5_2.png[Line chart with cumulative sum of orders mad *Lens* allows you to compare the currently selected time range with historical data using the *Time shift* option. To calculate the percent change, use *Formula*. -Time shifts can be used on any metric. The special shift *previous* will show the time window preceding the currently selected one, spanning the same duration. -For example, if *Last 7 days* is selected in the time filter, *previous* will show data from 14 days ago to 7 days ago. - If multiple time shifts are used in a single chart, a multiple of the date histogram interval should be chosen - otherwise data points might not line up in the chart and empty spots can occur. For example, if a daily interval is used, shifting one series by *36h*, and another one by *1d*, is not recommended. In this scenario, either reduce the interval to *12h*, or create two separate charts. diff --git a/docs/user/dashboard/tsvb.asciidoc b/docs/user/dashboard/tsvb.asciidoc index 89da3f7285924..11fe71b7639bb 100644 --- a/docs/user/dashboard/tsvb.asciidoc +++ b/docs/user/dashboard/tsvb.asciidoc @@ -148,6 +148,27 @@ The *Markdown* visualization supports Markdown with Handlebar (mustache) syntax For answers to frequently asked *TSVB* question, review the following. +[float] +===== How do I create dashboard drilldowns for Top N and Table visualizations? + +You can create dashboard drilldowns that include the specified time range for *Top N* and *Table* visualizations. + +. Open the dashboard that you want to link to, then copy the URL. + +. Open the dashboard with the *Top N* and *Table* visualization panel, then click *Edit* in the toolbar. + +. Open the *Top N* or *Table* panel menu, then select *Edit visualization*. + +. Click *Panel options*. + +. In the *Item URL* field, enter the URL. ++ +For example `dashboards#/view/f193ca90-c9f4-11eb-b038-dd3270053a27`. + +. Click *Save and return*. + +. In the toolbar, cick *Save as*, then make sure *Store time with dashboard* is deselected. + [float] ===== Why is my TSVB visualization missing data? diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index b86fa82c30381..2f9f1fe371dc3 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -82,9 +82,10 @@ connectors>> for triggering actions. | Monitor the generation of reports—PDF, PNG, and CSV—and download reports that you previously generated. A report can contain a dashboard, visualization, saved search, or Canvas workpad. -| {ml-docs}/ml-jobs.html[Machine Learning Jobs] -| View your {anomaly-jobs} and {dfanalytics-jobs}. Open the Single Metric -Viewer or Anomaly Explorer to see your {ml} results. +| Machine Learning Jobs +| View your <> and +<> jobs. Open the Single Metric +Viewer or Anomaly Explorer to see your {anomaly-detect} results. | <> | Detect changes in your data by creating, managing, and monitoring alerts. diff --git a/docs/user/ml/index.asciidoc b/docs/user/ml/index.asciidoc index b3606b122d750..a05ff1eeec4a6 100644 --- a/docs/user/ml/index.asciidoc +++ b/docs/user/ml/index.asciidoc @@ -48,8 +48,9 @@ pane: image::user/ml/images/ml-job-management.png[Job Management] You can use the *Settings* pane to create and edit -{ml-docs}/ml-calendars.html[calendars] and the filters that are used in -{ml-docs}/ml-rules.html[custom rules]: +{ml-docs}/ml-ad-finding-anomalies.html#ml-ad-calendars[calendars] and the +filters that are used in +{ml-docs}/ml-ad-finding-anomalies.html#ml-ad-rules[custom rules]: [role="screenshot"] image::user/ml/images/ml-settings.png[Calendar Management] diff --git a/examples/expressions_explorer/public/render_expressions.tsx b/examples/expressions_explorer/public/render_expressions.tsx index 20a8b56edb3f8..d91db964c5352 100644 --- a/examples/expressions_explorer/public/render_expressions.tsx +++ b/examples/expressions_explorer/public/render_expressions.tsx @@ -34,7 +34,9 @@ interface Props { } export function RenderExpressionsExample({ expressions, inspector }: Props) { - const [expression, updateExpression] = useState('markdown "## expressions explorer rendering"'); + const [expression, updateExpression] = useState( + 'markdownVis "## expressions explorer rendering"' + ); const expressionChanged = (value: string) => { updateExpression(value); diff --git a/examples/expressions_explorer/public/run_expressions.tsx b/examples/expressions_explorer/public/run_expressions.tsx index a635fab7ec8ae..93cab0e9f2b6f 100644 --- a/examples/expressions_explorer/public/run_expressions.tsx +++ b/examples/expressions_explorer/public/run_expressions.tsx @@ -35,7 +35,7 @@ interface Props { } export function RunExpressionsExample({ expressions, inspector }: Props) { - const [expression, updateExpression] = useState('markdown "## expressions explorer"'); + const [expression, updateExpression] = useState('markdownVis "## expressions explorer"'); const [result, updateResult] = useState({}); const expressionChanged = (value: string) => { diff --git a/package.json b/package.json index 688c73f44a4ef..5cf72e2110982 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "dependencies": { "@elastic/apm-rum": "^5.8.0", "@elastic/apm-rum-react": "^1.2.11", - "@elastic/charts": "31.1.0", + "@elastic/charts": "32.0.0", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.13", "@elastic/ems-client": "7.14.0", @@ -114,6 +114,7 @@ "@elastic/safer-lodash-set": "link:bazel-bin/packages/elastic-safer-lodash-set", "@elastic/search-ui-app-search-connector": "^1.6.0", "@elastic/ui-ace": "0.2.3", + "@emotion/react": "^11.4.0", "@hapi/accept": "^5.0.2", "@hapi/boom": "^9.1.1", "@hapi/cookie": "^11.0.2", @@ -351,7 +352,7 @@ "react-moment-proptypes": "^1.7.0", "react-monaco-editor": "^0.41.2", "react-popper-tooltip": "^2.10.1", - "react-query": "^3.13.10", + "react-query": "^3.18.1", "react-redux": "^7.2.0", "react-resizable": "^1.7.5", "react-resize-detector": "^4.2.0", @@ -454,6 +455,8 @@ "@elastic/eslint-plugin-eui": "0.0.2", "@elastic/github-checks-reporter": "0.0.20b3", "@elastic/makelogs": "^6.0.0", + "@emotion/babel-preset-css-prop": "^11.2.0", + "@emotion/jest": "^11.3.0", "@istanbuljs/schema": "^0.1.2", "@jest/reporters": "^26.6.2", "@kbn/babel-code-parser": "link:bazel-bin/packages/kbn-babel-code-parser", diff --git a/packages/elastic-eslint-config-kibana/.eslintrc.js b/packages/elastic-eslint-config-kibana/.eslintrc.js index 3220a01184004..d3cf7cf964a60 100644 --- a/packages/elastic-eslint-config-kibana/.eslintrc.js +++ b/packages/elastic-eslint-config-kibana/.eslintrc.js @@ -1,3 +1,5 @@ +const { USES_STYLED_COMPONENTS } = require('@kbn/dev-utils'); + module.exports = { extends: [ './javascript.js', @@ -79,7 +81,13 @@ module.exports = { from: 'react-intl', to: '@kbn/i18n/react', disallowedMessage: `import from @kbn/i18n/react instead` - } + }, + { + from: 'styled-components', + to: false, + exclude: USES_STYLED_COMPONENTS, + disallowedMessage: `Prefer using @emotion/react instead. To use styled-components, ensure you plugin is enabled in @kbn/dev-utils/src/babel.ts.` + }, ], ], }, diff --git a/packages/kbn-babel-preset/BUILD.bazel b/packages/kbn-babel-preset/BUILD.bazel index f5ebc153b9e1a..11eae8bc55ca9 100644 --- a/packages/kbn-babel-preset/BUILD.bazel +++ b/packages/kbn-babel-preset/BUILD.bazel @@ -32,6 +32,7 @@ DEPS = [ "@npm//@babel/preset-env", "@npm//@babel/preset-react", "@npm//@babel/preset-typescript", + "@npm//@emotion/babel-preset-css-prop", "@npm//babel-plugin-add-module-exports", "@npm//babel-plugin-styled-components", "@npm//babel-plugin-transform-react-remove-prop-types", diff --git a/packages/kbn-babel-preset/webpack_preset.js b/packages/kbn-babel-preset/webpack_preset.js index ca7ea40ff0fe1..186ce87478828 100644 --- a/packages/kbn-babel-preset/webpack_preset.js +++ b/packages/kbn-babel-preset/webpack_preset.js @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +const { USES_STYLED_COMPONENTS } = require.resolve('@kbn/dev-utils'); + module.exports = () => { return { presets: [ @@ -21,14 +23,6 @@ module.exports = () => { ], require('./common_preset'), ], - plugins: [ - [ - require.resolve('babel-plugin-styled-components'), - { - fileName: false, - }, - ], - ], env: { production: { plugins: [ @@ -42,5 +36,29 @@ module.exports = () => { ], }, }, + overrides: [ + { + include: USES_STYLED_COMPONENTS, + plugins: [ + [ + require.resolve('babel-plugin-styled-components'), + { + fileName: false, + }, + ], + ], + }, + { + exclude: USES_STYLED_COMPONENTS, + presets: [ + [ + require.resolve('@emotion/babel-preset-css-prop'), + { + labelFormat: '[local]', + }, + ], + ], + }, + ], }; }; diff --git a/packages/kbn-crypto/BUILD.bazel b/packages/kbn-crypto/BUILD.bazel index 20793e27de629..bf1ed3f778975 100644 --- a/packages/kbn-crypto/BUILD.bazel +++ b/packages/kbn-crypto/BUILD.bazel @@ -38,7 +38,8 @@ TYPES_DEPS = [ "@npm//@types/node", "@npm//@types/node-forge", "@npm//@types/testing-library__jest-dom", - "@npm//resize-observer-polyfill" + "@npm//resize-observer-polyfill", + "@npm//@emotion/react", ] DEPS = SRC_DEPS + TYPES_DEPS diff --git a/packages/kbn-dev-utils/src/babel.ts b/packages/kbn-dev-utils/src/babel.ts index 9daa7d9fe8d7a..5570055a21d15 100644 --- a/packages/kbn-dev-utils/src/babel.ts +++ b/packages/kbn-dev-utils/src/babel.ts @@ -46,3 +46,14 @@ export async function transformFileWithBabel(file: File) { file.extname = '.js'; transformedFiles.add(file); } + +/** + * Synchronized regex list of files that use `styled-components`. + * Used by `kbn-babel-preset` and `elastic-eslint-config-kibana`. + */ +export const USES_STYLED_COMPONENTS = [ + /packages[\/\\]kbn-ui-shared-deps[\/\\]/, + /src[\/\\]plugins[\/\\](data|kibana_react)[\/\\]/, + /x-pack[\/\\]plugins[\/\\](apm|beats_management|cases|fleet|infra|lists|observability|osquery|security_solution|timelines|uptime)[\/\\]/, + /x-pack[\/\\]test[\/\\]plugin_functional[\/\\]plugins[\/\\]resolver_test[\/\\]/, +]; diff --git a/packages/kbn-eslint-plugin-eslint/rules/module_migration.js b/packages/kbn-eslint-plugin-eslint/rules/module_migration.js index 87a1bae8eac1a..3175210eccb10 100644 --- a/packages/kbn-eslint-plugin-eslint/rules/module_migration.js +++ b/packages/kbn-eslint-plugin-eslint/rules/module_migration.js @@ -78,6 +78,12 @@ module.exports = { disallowedMessage: { type: 'string', }, + include: { + type: 'array', + }, + exclude: { + type: 'array', + }, }, anyOf: [ { @@ -95,7 +101,22 @@ module.exports = { ], }, create: (context) => { - const mappings = context.options[0]; + const filename = path.relative(KIBANA_ROOT, context.getFilename()); + + const mappings = context.options[0].filter((mapping) => { + // exclude mapping rule if it is explicitly excluded from this file + if (mapping.exclude && mapping.exclude.some((p) => p.test(filename))) { + return false; + } + + // if this mapping rule is only included in specific files, optionally include it + if (mapping.include) { + return mapping.include.some((p) => p.test(filename)); + } + + // include all mapping rules by default + return true; + }); return { ImportDeclaration(node) { diff --git a/src/plugins/kibana_legacy/public/notify/toasts/index.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/packages/kbn-ui-shared-deps/src/public_path_module_creator.ts similarity index 85% rename from src/plugins/kibana_legacy/public/notify/toasts/index.ts rename to packages/kbn-optimizer/src/__fixtures__/mock_repo/packages/kbn-ui-shared-deps/src/public_path_module_creator.ts index cdd7df04548fb..b03ee16d2f746 100644 --- a/src/plugins/kibana_legacy/public/notify/toasts/index.ts +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/packages/kbn-ui-shared-deps/src/public_path_module_creator.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { ToastNotifications } from './toast_notifications'; +// stub diff --git a/packages/kbn-optimizer/src/common/__snapshots__/parse_path.test.ts.snap b/packages/kbn-optimizer/src/common/__snapshots__/parse_path.test.ts.snap index f537674c3fff7..2a30694afb826 100644 --- a/packages/kbn-optimizer/src/common/__snapshots__/parse_path.test.ts.snap +++ b/packages/kbn-optimizer/src/common/__snapshots__/parse_path.test.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`parseDirPath() parses / 1`] = ` -Object { +ParsedPath { "dirs": Array [], "filename": undefined, "query": undefined, @@ -10,7 +10,7 @@ Object { `; exports[`parseDirPath() parses /foo 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", ], @@ -21,7 +21,7 @@ Object { `; exports[`parseDirPath() parses /foo/bar/baz 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", "bar", @@ -34,7 +34,7 @@ Object { `; exports[`parseDirPath() parses /foo/bar/baz/ 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", "bar", @@ -47,7 +47,7 @@ Object { `; exports[`parseDirPath() parses c:\\ 1`] = ` -Object { +ParsedPath { "dirs": Array [], "filename": undefined, "query": undefined, @@ -56,7 +56,7 @@ Object { `; exports[`parseDirPath() parses c:\\foo 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", ], @@ -67,7 +67,7 @@ Object { `; exports[`parseDirPath() parses c:\\foo\\bar\\baz 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", "bar", @@ -80,7 +80,7 @@ Object { `; exports[`parseDirPath() parses c:\\foo\\bar\\baz\\ 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", "bar", @@ -93,7 +93,7 @@ Object { `; exports[`parseFilePath() parses /foo 1`] = ` -Object { +ParsedPath { "dirs": Array [], "filename": "foo", "query": undefined, @@ -102,7 +102,7 @@ Object { `; exports[`parseFilePath() parses /foo/bar/baz 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", "bar", @@ -114,7 +114,7 @@ Object { `; exports[`parseFilePath() parses /foo/bar/baz.json 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", "bar", @@ -126,7 +126,7 @@ Object { `; exports[`parseFilePath() parses /foo/bar/baz.json?light 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", "bar", @@ -140,7 +140,7 @@ Object { `; exports[`parseFilePath() parses /foo/bar/baz.json?light=true&dark=false 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", "bar", @@ -155,7 +155,7 @@ Object { `; exports[`parseFilePath() parses c:/foo/bar/baz.json 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", "bar", @@ -167,7 +167,7 @@ Object { `; exports[`parseFilePath() parses c:\\foo 1`] = ` -Object { +ParsedPath { "dirs": Array [], "filename": "foo", "query": undefined, @@ -176,7 +176,7 @@ Object { `; exports[`parseFilePath() parses c:\\foo\\bar\\baz 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", "bar", @@ -188,7 +188,7 @@ Object { `; exports[`parseFilePath() parses c:\\foo\\bar\\baz.json 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", "bar", @@ -200,7 +200,7 @@ Object { `; exports[`parseFilePath() parses c:\\foo\\bar\\baz.json?dark 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", "bar", @@ -214,7 +214,7 @@ Object { `; exports[`parseFilePath() parses c:\\foo\\bar\\baz.json?dark=true&light=false 1`] = ` -Object { +ParsedPath { "dirs": Array [ "foo", "bar", diff --git a/packages/kbn-optimizer/src/common/parse_path.ts b/packages/kbn-optimizer/src/common/parse_path.ts index 7ea0042db25c9..da3744ba477bd 100644 --- a/packages/kbn-optimizer/src/common/parse_path.ts +++ b/packages/kbn-optimizer/src/common/parse_path.ts @@ -9,17 +9,61 @@ import normalizePath from 'normalize-path'; import Qs from 'querystring'; +class ParsedPath { + constructor( + public readonly root: string, + public readonly dirs: string[], + public readonly query?: Record, + public readonly filename?: string + ) {} + + private indexOfDir(match: string | RegExp, fromIndex: number = 0) { + for (let i = fromIndex; i < this.dirs.length; i++) { + if (this.matchDir(i, match)) { + return i; + } + } + + return -1; + } + + private matchDir(i: number, match: string | RegExp) { + return typeof match === 'string' ? this.dirs[i] === match : match.test(this.dirs[i]); + } + + matchDirs(...segments: Array) { + const [first, ...rest] = segments; + let fromIndex = 0; + while (true) { + // do the dirs include the first segment to match? + const startIndex = this.indexOfDir(first, fromIndex); + if (startIndex === -1) { + return; + } + + // are all of the ...rest segments also matched at this point? + if (!rest.length || rest.every((seg, i) => this.matchDir(startIndex + 1 + i, seg))) { + return { startIndex, endIndex: startIndex + rest.length }; + } + + // no match, search again, this time looking at instances after the matched instance + fromIndex = startIndex + 1; + } + } +} + /** * Parse an absolute path, supporting normalized paths from webpack, * into a list of directories and root */ export function parseDirPath(path: string) { const filePath = parseFilePath(path); - return { - ...filePath, - dirs: [...filePath.dirs, ...(filePath.filename ? [filePath.filename] : [])], - filename: undefined, - }; + return new ParsedPath( + filePath.root, + [...filePath.dirs, ...(filePath.filename ? [filePath.filename] : [])], + filePath.query, + undefined + ); } export function parseFilePath(path: string) { @@ -32,10 +76,10 @@ export function parseFilePath(path: string) { } const [root, ...others] = normalized.split('/'); - return { - root: root === '' ? '/' : root, - dirs: others.slice(0, -1), + return new ParsedPath( + root === '' ? '/' : root, + others.slice(0, -1), query, - filename: others[others.length - 1] || undefined, - }; + others[others.length - 1] || undefined + ); } diff --git a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts index 97a7f33be673d..48d36b706b831 100644 --- a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts +++ b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts @@ -15,7 +15,7 @@ import cpy from 'cpy'; import del from 'del'; import { tap, filter } from 'rxjs/operators'; import { REPO_ROOT } from '@kbn/utils'; -import { ToolingLog, createReplaceSerializer } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; import { runOptimizer, OptimizerConfig, OptimizerUpdate, logOptimizerState } from '../index'; import { allValuesFrom } from '../common'; @@ -29,8 +29,6 @@ expect.addSnapshotSerializer({ test: (value: any) => typeof value === 'string' && value.includes(REPO_ROOT), }); -expect.addSnapshotSerializer(createReplaceSerializer(/\w+-fastbuild/, '-fastbuild')); - const log = new ToolingLog({ level: 'error', writeTo: { @@ -132,7 +130,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { expect(foo.cache.getModuleCount()).toBe(6); expect(foo.cache.getReferencedFiles()).toMatchInlineSnapshot(` Array [ - /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/bazel-out/-fastbuild/bin/packages/kbn-ui-shared-deps/target/public_path_module_creator.js, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/packages/kbn-ui-shared-deps/src/public_path_module_creator.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/kibana.json, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/async_import.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/ext.ts, @@ -155,7 +153,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { /node_modules/@kbn/optimizer/postcss.config.js, /node_modules/css-loader/package.json, /node_modules/style-loader/package.json, - /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/bazel-out/-fastbuild/bin/packages/kbn-ui-shared-deps/target/public_path_module_creator.js, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/packages/kbn-ui-shared-deps/src/public_path_module_creator.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/kibana.json, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.scss, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.ts, @@ -175,7 +173,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { expect(baz.cache.getReferencedFiles()).toMatchInlineSnapshot(` Array [ - /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/bazel-out/-fastbuild/bin/packages/kbn-ui-shared-deps/target/public_path_module_creator.js, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/packages/kbn-ui-shared-deps/src/public_path_module_creator.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/kibana.json, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/public/index.ts, /packages/kbn-optimizer/src/worker/entry_point_creator.ts, diff --git a/packages/kbn-optimizer/src/worker/populate_bundle_cache_plugin.ts b/packages/kbn-optimizer/src/worker/populate_bundle_cache_plugin.ts index 8d890b31b639d..a3455d7ddf2b9 100644 --- a/packages/kbn-optimizer/src/worker/populate_bundle_cache_plugin.ts +++ b/packages/kbn-optimizer/src/worker/populate_bundle_cache_plugin.ts @@ -6,11 +6,11 @@ * Side Public License, v 1. */ -import webpack from 'webpack'; - import Path from 'path'; import { inspect } from 'util'; +import webpack from 'webpack'; + import { Bundle, WorkerConfig, ascending, parseFilePath } from '../common'; import { BundleRefModule } from './bundle_ref_module'; import { @@ -21,6 +21,20 @@ import { getModulePath, } from './webpack_helpers'; +function tryToResolveRewrittenPath(from: string, toResolve: string) { + try { + return require.resolve(toResolve); + } catch (error) { + if (error.code === 'MODULE_NOT_FOUND') { + throw new Error( + `attempted to rewrite bazel-out path [${from}] to [${toResolve}] but couldn't find the rewrite target` + ); + } + + throw error; + } +} + /** * sass-loader creates about a 40% overhead on the overall optimizer runtime, and * so this constant is used to indicate to assignBundlesToWorkers() that there is @@ -57,17 +71,44 @@ export class PopulateBundleCachePlugin { let path = getModulePath(module); let parsedPath = parseFilePath(path); - if (parsedPath.dirs.includes('bazel-out')) { - const index = parsedPath.dirs.indexOf('bazel-out'); - path = Path.join( - workerConfig.repoRoot, - 'bazel-out', - ...parsedPath.dirs.slice(index + 1), - parsedPath.filename ?? '' + const bazelOut = parsedPath.matchDirs( + 'bazel-out', + /-fastbuild$/, + 'bin', + 'packages', + /.*/, + 'target' + ); + + // if the module is referenced from one of our packages and resolved to the `bazel-out` dir + // we should rewrite our reference to point to the source file so that we can track the + // modified time of that file rather than the built output which is rebuilt all the time + // without actually changing + if (bazelOut) { + const packageDir = parsedPath.dirs[bazelOut.endIndex - 1]; + const subDirs = parsedPath.dirs.slice(bazelOut.endIndex + 1); + path = tryToResolveRewrittenPath( + path, + Path.join( + workerConfig.repoRoot, + 'packages', + packageDir, + 'src', + ...subDirs, + parsedPath.filename + ? Path.basename(parsedPath.filename, Path.extname(parsedPath.filename)) + : '' + ) ); parsedPath = parseFilePath(path); } + if (parsedPath.matchDirs('bazel-out')) { + throw new Error( + `a bazel-out dir is being referenced by module [${path}] and not getting rewritten to its source location` + ); + } + if (!parsedPath.dirs.includes('node_modules')) { referencedFiles.add(path); diff --git a/packages/kbn-rule-data-utils/src/technical_field_names.ts b/packages/kbn-rule-data-utils/src/technical_field_names.ts index 31779c9f08e81..6c45403fc0a13 100644 --- a/packages/kbn-rule-data-utils/src/technical_field_names.ts +++ b/packages/kbn-rule-data-utils/src/technical_field_names.ts @@ -19,6 +19,7 @@ const RULE_NAME = 'rule.name' as const; const RULE_CATEGORY = 'rule.category' as const; const TAGS = 'tags' as const; const PRODUCER = `${ALERT_NAMESPACE}.producer` as const; +const OWNER = `${ALERT_NAMESPACE}.owner` as const; const ALERT_ID = `${ALERT_NAMESPACE}.id` as const; const ALERT_UUID = `${ALERT_NAMESPACE}.uuid` as const; const ALERT_START = `${ALERT_NAMESPACE}.start` as const; @@ -40,6 +41,7 @@ const fields = { RULE_CATEGORY, TAGS, PRODUCER, + OWNER, ALERT_ID, ALERT_UUID, ALERT_START, @@ -62,6 +64,7 @@ export { RULE_CATEGORY, TAGS, PRODUCER, + OWNER, ALERT_ID, ALERT_UUID, ALERT_START, diff --git a/x-pack/plugins/lists/server/services/utils/decode_version.ts b/packages/kbn-securitysolution-es-utils/src/decode_version/index.ts similarity index 85% rename from x-pack/plugins/lists/server/services/utils/decode_version.ts rename to packages/kbn-securitysolution-es-utils/src/decode_version/index.ts index 8ed934204ed98..d58c7add67a27 100644 --- a/x-pack/plugins/lists/server/services/utils/decode_version.ts +++ b/packages/kbn-securitysolution-es-utils/src/decode_version/index.ts @@ -1,8 +1,9 @@ /* * 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. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ // Similar to the src/core/server/saved_objects/version/decode_version.ts diff --git a/x-pack/plugins/lists/server/services/utils/encode_hit_version.ts b/packages/kbn-securitysolution-es-utils/src/encode_hit_version/index.ts similarity index 84% rename from x-pack/plugins/lists/server/services/utils/encode_hit_version.ts rename to packages/kbn-securitysolution-es-utils/src/encode_hit_version/index.ts index 4c55d858d283b..29b5a18f7c303 100644 --- a/x-pack/plugins/lists/server/services/utils/encode_hit_version.ts +++ b/packages/kbn-securitysolution-es-utils/src/encode_hit_version/index.ts @@ -1,8 +1,9 @@ /* * 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. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ /** diff --git a/packages/kbn-securitysolution-es-utils/src/index.ts b/packages/kbn-securitysolution-es-utils/src/index.ts index cfa6820e9aac5..8dead7f899ba2 100644 --- a/packages/kbn-securitysolution-es-utils/src/index.ts +++ b/packages/kbn-securitysolution-es-utils/src/index.ts @@ -8,10 +8,12 @@ export * from './bad_request_error'; export * from './create_boostrap_index'; +export * from './decode_version'; export * from './delete_all_index'; export * from './delete_policy'; export * from './delete_template'; export * from './elasticsearch_client'; +export * from './encode_hit_version'; export * from './get_index_aliases'; export * from './get_index_count'; export * from './get_index_exists'; diff --git a/packages/kbn-spec-to-console/README.md b/packages/kbn-spec-to-console/README.md index 0328dec791320..a0e654713f61b 100644 --- a/packages/kbn-spec-to-console/README.md +++ b/packages/kbn-spec-to-console/README.md @@ -18,15 +18,10 @@ git pull --depth=1 origin master ### Usage -You need to run the command twice: once for the **OSS** specs and once for the **X-Pack** specs At the root of the Kibana repository, run the following commands: ```sh -# OSS yarn spec_to_console -g "/rest-api-spec/src/main/resources/rest-api-spec/api/*" -d "src/plugins/console/server/lib/spec_definitions/json/generated" - -# X-pack -yarn spec_to_console -g "/x-pack/plugin/src/test/resources/rest-api-spec/api/*" -d "x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated" ``` ### Information used in Console that is not available in the REST spec diff --git a/packages/kbn-spec-to-console/lib/convert/params.js b/packages/kbn-spec-to-console/lib/convert/params.js index e5365b4d7311e..1aa89be11c76d 100644 --- a/packages/kbn-spec-to-console/lib/convert/params.js +++ b/packages/kbn-spec-to-console/lib/convert/params.js @@ -37,6 +37,7 @@ module.exports = (params) => { case 'string': case 'number': case 'number|string': + case 'boolean|long': result[param] = defaultValue || ''; break; case 'list': diff --git a/packages/kbn-storybook/lib/default_config.ts b/packages/kbn-storybook/lib/default_config.ts index e194c9789daab..989f707b06fed 100644 --- a/packages/kbn-storybook/lib/default_config.ts +++ b/packages/kbn-storybook/lib/default_config.ts @@ -6,8 +6,11 @@ * Side Public License, v 1. */ +import * as path from 'path'; import { StorybookConfig } from '@storybook/core/types'; +import { REPO_ROOT } from './constants'; +const toPath = (_path: string) => path.join(REPO_ROOT, _path); export const defaultConfig: StorybookConfig = { addons: ['@kbn/storybook/preset', '@storybook/addon-a11y', '@storybook/addon-essentials'], stories: ['../**/*.stories.tsx'], @@ -22,6 +25,21 @@ export const defaultConfig: StorybookConfig = { config.node = { fs: 'empty' }; - return config; + // Remove when @storybook has moved to @emotion v11 + // https://github.com/storybookjs/storybook/issues/13145 + const emotion11CompatibleConfig = { + ...config, + resolve: { + ...config.resolve, + alias: { + ...config.resolve?.alias, + '@emotion/core': toPath('node_modules/@emotion/react'), + '@emotion/styled': toPath('node_modules/@emotion/styled'), + 'emotion-theming': toPath('node_modules/@emotion/react'), + }, + }, + }; + + return emotion11CompatibleConfig; }, }; diff --git a/packages/kbn-storybook/lib/theme_switcher.tsx b/packages/kbn-storybook/lib/theme_switcher.tsx index da62bc7010c4b..24ddec1fdf51c 100644 --- a/packages/kbn-storybook/lib/theme_switcher.tsx +++ b/packages/kbn-storybook/lib/theme_switcher.tsx @@ -54,6 +54,7 @@ export function ThemeSwitcher() { closeOnClick tooltip={({ onHide }) => } > + {/* @ts-ignore Remove when @storybook has moved to @emotion v11 */} diff --git a/packages/kbn-telemetry-tools/BUILD.bazel b/packages/kbn-telemetry-tools/BUILD.bazel index d394b0c93d45f..ef1316cec75a3 100644 --- a/packages/kbn-telemetry-tools/BUILD.bazel +++ b/packages/kbn-telemetry-tools/BUILD.bazel @@ -47,7 +47,8 @@ TYPES_DEPS = [ "@npm//@types/node", "@npm//@types/normalize-path", "@npm//@types/testing-library__jest-dom", - "@npm//resize-observer-polyfill" + "@npm//resize-observer-polyfill", + "@npm//@emotion/react", ] DEPS = SRC_DEPS + TYPES_DEPS diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js index c84fe3f7a55b0..abc5cfa8efaa8 100644 --- a/packages/kbn-test/jest-preset.js +++ b/packages/kbn-test/jest-preset.js @@ -66,6 +66,7 @@ module.exports = { snapshotSerializers: [ '/src/plugins/kibana_react/public/util/test_helpers/react_mount_serializer.ts', '/node_modules/enzyme-to-json/serializer', + '/node_modules/@emotion/jest/serializer', ], // The test environment that will be used for testing diff --git a/packages/kbn-test/src/kbn_client/kbn_client_requester.test.ts b/packages/kbn-test/src/kbn_client/kbn_client_requester.test.ts new file mode 100644 index 0000000000000..bb2f923ad1f01 --- /dev/null +++ b/packages/kbn-test/src/kbn_client/kbn_client_requester.test.ts @@ -0,0 +1,35 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pathWithSpace } from './kbn_client_requester'; + +describe('pathWithSpace()', () => { + it('adds a space to the path', () => { + expect(pathWithSpace('hello')`/foo/bar`).toMatchInlineSnapshot(`"/s/hello/foo/bar"`); + }); + + it('ignores the space when it is empty', () => { + expect(pathWithSpace(undefined)`/foo/bar`).toMatchInlineSnapshot(`"/foo/bar"`); + expect(pathWithSpace('')`/foo/bar`).toMatchInlineSnapshot(`"/foo/bar"`); + }); + + it('ignores the space when it is the default space', () => { + expect(pathWithSpace('default')`/foo/bar`).toMatchInlineSnapshot(`"/foo/bar"`); + }); + + it('uriencodes variables in the path', () => { + expect(pathWithSpace('space')`hello/${'funky/username🏴‍☠️'}`).toMatchInlineSnapshot( + `"/s/space/hello/funky%2Fusername%F0%9F%8F%B4%E2%80%8D%E2%98%A0%EF%B8%8F"` + ); + }); + + it('ensures the path always starts with a slash', () => { + expect(pathWithSpace('foo')`hello/world`).toMatchInlineSnapshot(`"/s/foo/hello/world"`); + expect(pathWithSpace()`hello/world`).toMatchInlineSnapshot(`"/hello/world"`); + }); +}); diff --git a/packages/kbn-test/src/kbn_client/kbn_client_requester.ts b/packages/kbn-test/src/kbn_client/kbn_client_requester.ts index a194b593b3863..c2e4247df1ab0 100644 --- a/packages/kbn-test/src/kbn_client/kbn_client_requester.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_requester.ts @@ -23,6 +23,19 @@ const isIgnorableError = (error: any, ignorableErrors: number[] = []) => { return isAxiosResponseError(error) && ignorableErrors.includes(error.response.status); }; +/** + * Creates a template literal tag which will uriencode the variables in a template literal + * as well as prefix the path with a specific space if one is defined + */ +export const pathWithSpace = (space?: string) => { + const prefix = !space || space === 'default' ? '' : uriencode`/s/${space}`; + + return (strings: TemplateStringsArray, ...args: Array) => { + const path = uriencode(strings, ...args); + return path.startsWith('/') || path === '' ? `${prefix}${path}` : `${prefix}/${path}`; + }; +}; + export const uriencode = ( strings: TemplateStringsArray, ...values: Array diff --git a/packages/kbn-test/src/kbn_client/kbn_client_ui_settings.ts b/packages/kbn-test/src/kbn_client/kbn_client_ui_settings.ts index 78155098ef038..7ea685667d48b 100644 --- a/packages/kbn-test/src/kbn_client/kbn_client_ui_settings.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_ui_settings.ts @@ -8,7 +8,7 @@ import { ToolingLog } from '@kbn/dev-utils'; -import { KbnClientRequester, uriencode } from './kbn_client_requester'; +import { KbnClientRequester, pathWithSpace } from './kbn_client_requester'; export type UiSettingValues = Record; interface UiSettingsApiResponse { @@ -27,8 +27,8 @@ export class KbnClientUiSettings { private readonly defaults?: UiSettingValues ) {} - async get(setting: string) { - const all = await this.getAll(); + async get(setting: string, { space }: { space?: string } = {}) { + const all = await this.getAll({ space }); const value = all[setting]?.userValue; this.log.verbose('uiSettings.value: %j', value); @@ -45,9 +45,9 @@ export class KbnClientUiSettings { /** * Unset a uiSetting */ - async unset(setting: string) { + async unset(setting: string, { space }: { space?: string } = {}) { const { data } = await this.requester.request({ - path: uriencode`/api/kibana/settings/${setting}`, + path: pathWithSpace(space)`/api/kibana/settings/${setting}`, method: 'DELETE', }); return data; @@ -57,7 +57,10 @@ export class KbnClientUiSettings { * Replace all uiSettings with the `doc` values, `doc` is merged * with some defaults */ - async replace(doc: UiSettingValues, { retries = 5 }: { retries?: number } = {}) { + async replace( + doc: UiSettingValues, + { retries = 5, space }: { retries?: number; space?: string } = {} + ) { this.log.debug('replacing kibana config doc: %j', doc); const changes: Record = { @@ -73,7 +76,7 @@ export class KbnClientUiSettings { await this.requester.request({ method: 'POST', - path: '/api/kibana/settings', + path: pathWithSpace(space)`/api/kibana/settings`, body: { changes }, retries, }); @@ -82,11 +85,11 @@ export class KbnClientUiSettings { /** * Add fields to the config doc (like setting timezone and defaultIndex) */ - async update(updates: UiSettingValues) { + async update(updates: UiSettingValues, { space }: { space?: string } = {}) { this.log.debug('applying update to kibana config: %j', updates); await this.requester.request({ - path: '/api/kibana/settings', + path: pathWithSpace(space)`/api/kibana/settings`, method: 'POST', body: { changes: updates, @@ -95,9 +98,9 @@ export class KbnClientUiSettings { }); } - private async getAll() { + private async getAll({ space }: { space?: string } = {}) { const { data } = await this.requester.request({ - path: '/api/kibana/settings', + path: pathWithSpace(space)`/api/kibana/settings`, method: 'GET', }); diff --git a/packages/kbn-ui-shared-deps/BUILD.bazel b/packages/kbn-ui-shared-deps/BUILD.bazel index 9096905a2586b..f92049292f373 100644 --- a/packages/kbn-ui-shared-deps/BUILD.bazel +++ b/packages/kbn-ui-shared-deps/BUILD.bazel @@ -40,6 +40,7 @@ SRC_DEPS = [ "@npm//@elastic/charts", "@npm//@elastic/eui", "@npm//@elastic/numeral", + "@npm//@emotion/react", "@npm//abortcontroller-polyfill", "@npm//angular", "@npm//babel-loader", diff --git a/packages/kbn-ui-shared-deps/src/entry.js b/packages/kbn-ui-shared-deps/src/entry.js index 0e91c45ae6392..20e26ca6a2864 100644 --- a/packages/kbn-ui-shared-deps/src/entry.js +++ b/packages/kbn-ui-shared-deps/src/entry.js @@ -18,6 +18,7 @@ export const KbnI18n = require('@kbn/i18n'); export const KbnI18nAngular = require('@kbn/i18n/angular'); export const KbnI18nReact = require('@kbn/i18n/react'); export const Angular = require('angular'); +export const EmotionReact = require('@emotion/react'); export const Moment = require('moment'); export const MomentTimezone = require('moment-timezone/moment-timezone'); export const KbnMonaco = require('@kbn/monaco'); diff --git a/packages/kbn-ui-shared-deps/src/index.js b/packages/kbn-ui-shared-deps/src/index.js index 36c2e6b02879e..291c7c471d27c 100644 --- a/packages/kbn-ui-shared-deps/src/index.js +++ b/packages/kbn-ui-shared-deps/src/index.js @@ -57,6 +57,7 @@ exports.externals = { '@kbn/i18n': '__kbnSharedDeps__.KbnI18n', '@kbn/i18n/angular': '__kbnSharedDeps__.KbnI18nAngular', '@kbn/i18n/react': '__kbnSharedDeps__.KbnI18nReact', + '@emotion/react': '__kbnSharedDeps__.EmotionReact', jquery: '__kbnSharedDeps__.Jquery', moment: '__kbnSharedDeps__.Moment', 'moment-timezone': '__kbnSharedDeps__.MomentTimezone', diff --git a/packages/kbn-utils/src/path/index.ts b/packages/kbn-utils/src/path/index.ts index 2d6d0e9e919eb..9835179a61e9d 100644 --- a/packages/kbn-utils/src/path/index.ts +++ b/packages/kbn-utils/src/path/index.ts @@ -18,6 +18,7 @@ const CONFIG_PATHS = [ process.env.KIBANA_PATH_CONF && join(process.env.KIBANA_PATH_CONF, 'kibana.yml'), process.env.CONFIG_PATH, // deprecated join(REPO_ROOT, 'config/kibana.yml'), + '/etc/kibana/kibana.yml', ].filter(isString); const CONFIG_DIRECTORIES = [ diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index ad83965efde33..be949350f7229 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -68,12 +68,14 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) { delete extraCliOptions.env; if (opts.dev) { - if (!has('elasticsearch.username')) { - set('elasticsearch.username', 'kibana_system'); - } + if (!has('elasticsearch.serviceAccountToken')) { + if (!has('elasticsearch.username')) { + set('elasticsearch.username', 'kibana_system'); + } - if (!has('elasticsearch.password')) { - set('elasticsearch.password', 'changeme'); + if (!has('elasticsearch.password')) { + set('elasticsearch.password', 'changeme'); + } } if (opts.ssl) { diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 82353a96dc33c..6e33e39b148c4 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -746,9 +746,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` onResize={[Function]} >
-
+
@@ -1021,9 +1019,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` onResize={[Function]} >
-
+
@@ -1315,9 +1311,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` onResize={[Function]} >
-
+
@@ -1570,9 +1564,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` onResize={[Function]} >
-
+
@@ -1786,9 +1778,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` onResize={[Function]} >
-
+
diff --git a/src/core/public/chrome/ui/header/header_help_menu.tsx b/src/core/public/chrome/ui/header/header_help_menu.tsx index c6a09c1177a5e..cbf89bba2ca44 100644 --- a/src/core/public/chrome/ui/header/header_help_menu.tsx +++ b/src/core/public/chrome/ui/header/header_help_menu.tsx @@ -211,7 +211,7 @@ export class HeaderHelpMenu extends Component { return ( - + { - + { - + { @@ -330,7 +330,7 @@ export class HeaderHelpMenu extends Component { {customLinks} {content && ( <> - {customLinks && } + {customLinks && } )} @@ -383,7 +383,7 @@ const createCustomLink = ( ) => { return ( - + {text} {addSpacer && } diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 2187d5a0a33be..e8453d009e720 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -232,26 +232,26 @@ export class DocLinksService { guide: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/index.html`, aggregations: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-configuring-aggregation.html`, anomalyDetection: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-overview.html`, - anomalyDetectionJobs: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-jobs.html`, + anomalyDetectionJobs: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-finding-anomalies.html`, anomalyDetectionConfiguringCategories: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-configuring-categories.html`, - anomalyDetectionBucketSpan: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/create-jobs.html#bucket-span`, - anomalyDetectionCardinality: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/create-jobs.html#cardinality`, - anomalyDetectionCreateJobs: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/create-jobs.html`, - anomalyDetectionDetectors: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/create-jobs.html#detectors`, - anomalyDetectionInfluencers: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-influencers.html`, + anomalyDetectionBucketSpan: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-finding-anomalies.html#ml-ad-bucket-span`, + anomalyDetectionCardinality: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-finding-anomalies.html#ml-ad-cardinality`, + anomalyDetectionCreateJobs: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-finding-anomalies.html#ml-ad-create-job`, + anomalyDetectionDetectors: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-finding-anomalies.html#ml-ad-detectors`, + anomalyDetectionInfluencers: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-finding-anomalies.html#ml-ad-influencers`, anomalyDetectionJobResource: `${ELASTICSEARCH_DOCS}ml-put-job.html#ml-put-job-path-parms`, anomalyDetectionJobResourceAnalysisConfig: `${ELASTICSEARCH_DOCS}ml-put-job.html#put-analysisconfig`, - anomalyDetectionJobTips: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/create-jobs.html#job-tips`, - anomalyDetectionModelMemoryLimits: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/create-jobs.html#model-memory-limits`, - calendars: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-calendars.html`, - classificationEvaluation: `${ELASTICSEARCH_DOCS}evaluate-dfanalytics.html`, - customRules: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-rules.html`, + anomalyDetectionJobTips: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-finding-anomalies.html#ml-ad-job-tips`, + anomalyDetectionModelMemoryLimits: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-finding-anomalies.html#ml-ad-model-memory-limits`, + calendars: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-finding-anomalies.html#ml-ad-calendars`, + classificationEvaluation: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-classification.html#ml-dfanalytics-classification-evaluation`, + customRules: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-ad-finding-anomalies.html#ml-ad-rules`, customUrls: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-configuring-url.html`, dataFrameAnalytics: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics.html`, featureImportance: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-feature-importance.html`, outlierDetectionRoc: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-finding-outliers.html#ml-dfanalytics-roc`, - regressionEvaluation: `${ELASTICSEARCH_DOCS}evaluate-dfanalytics.html`, - classificationAucRoc: `${ELASTICSEARCH_DOCS}evaluate-dfanalytics.html`, + regressionEvaluation: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-regression.html#ml-dfanalytics-regression-evaluation`, + classificationAucRoc: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-classification.html#ml-dfanalytics-class-aucroc`, }, transforms: { guide: `${ELASTICSEARCH_DOCS}transforms.html`, diff --git a/src/core/server/core_app/bundle_routes/file_hash.test.ts b/src/core/server/core_app/bundle_routes/file_hash.test.ts index 918f435156344..ef24ebe063057 100644 --- a/src/core/server/core_app/bundle_routes/file_hash.test.ts +++ b/src/core/server/core_app/bundle_routes/file_hash.test.ts @@ -19,7 +19,8 @@ const mockedCache = (): jest.Mocked => ({ set: jest.fn(), }); -describe('getFileHash', () => { +// FLAKY: https://github.com/elastic/kibana/issues/105174 +describe.skip('getFileHash', () => { const sampleFilePath = resolve(__dirname, 'foo.js'); const fd = 42; const stats: Stats = { ino: 42, size: 9000 } as any; diff --git a/src/core/server/elasticsearch/client/client_config.test.ts b/src/core/server/elasticsearch/client/client_config.test.ts index faca79b3aa6fa..7e16339b40235 100644 --- a/src/core/server/elasticsearch/client/client_config.test.ts +++ b/src/core/server/elasticsearch/client/client_config.test.ts @@ -204,11 +204,27 @@ describe('parseClientOptions', () => { ); }); + it('adds an authorization header if `serviceAccountToken` is set', () => { + expect( + parseClientOptions( + createConfig({ + serviceAccountToken: 'ABC123', + }), + false + ) + ).toEqual( + expect.objectContaining({ + headers: expect.objectContaining({ + authorization: `Bearer ABC123`, + }), + }) + ); + }); + it('does not add auth to the nodes', () => { const options = parseClientOptions( createConfig({ - username: 'user', - password: 'pass', + serviceAccountToken: 'ABC123', hosts: ['http://node-A:9200'], }), true @@ -252,6 +268,34 @@ describe('parseClientOptions', () => { ] `); }); + + it('does not add the authorization header even if `serviceAccountToken` is set', () => { + expect( + parseClientOptions( + createConfig({ + serviceAccountToken: 'ABC123', + }), + true + ).headers + ).not.toHaveProperty('authorization'); + }); + + it('does not add auth to the nodes even if `serviceAccountToken` is set', () => { + const options = parseClientOptions( + createConfig({ + serviceAccountToken: 'ABC123', + hosts: ['http://node-A:9200'], + }), + true + ); + expect(options.nodes).toMatchInlineSnapshot(` + Array [ + Object { + "url": "http://node-a:9200/", + }, + ] + `); + }); }); }); diff --git a/src/core/server/elasticsearch/client/client_config.ts b/src/core/server/elasticsearch/client/client_config.ts index 3044b277db902..bbbb1ac247b3b 100644 --- a/src/core/server/elasticsearch/client/client_config.ts +++ b/src/core/server/elasticsearch/client/client_config.ts @@ -29,6 +29,7 @@ export type ElasticsearchClientConfig = Pick< | 'hosts' | 'username' | 'password' + | 'serviceAccountToken' > & { pingTimeout?: ElasticsearchConfig['pingTimeout'] | ClientOptions['pingTimeout']; requestTimeout?: ElasticsearchConfig['requestTimeout'] | ClientOptions['requestTimeout']; @@ -74,11 +75,16 @@ export function parseClientOptions( }; } - if (config.username && config.password && !scoped) { - clientOptions.auth = { - username: config.username, - password: config.password, - }; + if (!scoped) { + if (config.username && config.password) { + clientOptions.auth = { + username: config.username, + password: config.password, + }; + } else if (config.serviceAccountToken) { + // TODO: change once ES client has native support for service account tokens: https://github.com/elastic/elasticsearch-js/issues/1477 + clientOptions.headers!.authorization = `Bearer ${config.serviceAccountToken}`; + } } clientOptions.nodes = config.hosts.map((host) => convertHost(host)); diff --git a/src/core/server/elasticsearch/elasticsearch_config.test.ts b/src/core/server/elasticsearch/elasticsearch_config.test.ts index f8ef1a7a20a83..6e05baac88e34 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.test.ts @@ -41,6 +41,7 @@ test('set correct defaults', () => { "authorization", ], "requestTimeout": "PT30S", + "serviceAccountToken": undefined, "shardTimeout": "PT30S", "sniffInterval": false, "sniffOnConnectionFault": false, @@ -377,3 +378,22 @@ test('#username throws if equal to "elastic", only while running from source', ( ); expect(() => config.schema.validate(obj, { dist: true })).not.toThrow(); }); + +test('serviceAccountToken throws if username is also set', () => { + const obj = { + username: 'elastic', + serviceAccountToken: 'abc123', + }; + + expect(() => config.schema.validate(obj)).toThrowErrorMatchingInlineSnapshot( + `"[serviceAccountToken]: serviceAccountToken cannot be specified when \\"username\\" is also set."` + ); +}); + +test('serviceAccountToken does not throw if username is not set', () => { + const obj = { + serviceAccountToken: 'abc123', + }; + + expect(() => config.schema.validate(obj)).not.toThrow(); +}); diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index b2b25cda3ac2a..e756d9da867b3 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -53,6 +53,18 @@ export const configSchema = schema.object({ ) ), password: schema.maybe(schema.string()), + serviceAccountToken: schema.maybe( + schema.conditional( + schema.siblingRef('username'), + schema.never(), + schema.string(), + schema.string({ + validate: () => { + return `serviceAccountToken cannot be specified when "username" is also set.`; + }, + }) + ) + ), requestHeadersWhitelist: schema.oneOf( [ schema.string({ @@ -272,6 +284,7 @@ export class ElasticsearchConfig { /** * If Elasticsearch is protected with basic authentication, this setting provides * the username that the Kibana server uses to perform its administrative functions. + * Cannot be used in conjunction with serviceAccountToken. */ public readonly username?: string; @@ -281,6 +294,14 @@ export class ElasticsearchConfig { */ public readonly password?: string; + /** + * If Elasticsearch security features are enabled, this setting provides the service account + * token that the Kibana server users to perform its administrative functions. + * + * This is an alternative to specifying a username and password. + */ + public readonly serviceAccountToken?: string; + /** * Set of settings configure SSL connection between Kibana and Elasticsearch that * are required when `xpack.ssl.verification_mode` in Elasticsearch is set to @@ -314,6 +335,7 @@ export class ElasticsearchConfig { this.healthCheckDelay = rawConfig.healthCheck.delay; this.username = rawConfig.username; this.password = rawConfig.password; + this.serviceAccountToken = rawConfig.serviceAccountToken; this.customHeaders = rawConfig.customHeaders; const { alwaysPresentCertificate, verificationMode } = rawConfig.ssl; diff --git a/src/core/server/elasticsearch/legacy/cluster_client.test.ts b/src/core/server/elasticsearch/legacy/cluster_client.test.ts index 2ce19570677c5..52bc4bd45660e 100644 --- a/src/core/server/elasticsearch/legacy/cluster_client.test.ts +++ b/src/core/server/elasticsearch/legacy/cluster_client.test.ts @@ -101,6 +101,30 @@ describe('#callAsInternalUser', () => { expect(mockEsClientInstance.ping).toHaveBeenLastCalledWith(mockParams); }); + test('sets the authorization header when a service account token is configured', async () => { + clusterClient = new LegacyClusterClient( + { apiVersion: 'es-version', serviceAccountToken: 'ABC123' } as any, + logger.get(), + 'custom-type' + ); + + const mockResponse = { data: 'ping' }; + const mockParams = { param: 'ping' }; + mockEsClientInstance.ping.mockImplementation(function mockCall(this: any) { + return Promise.resolve({ + context: this, + response: mockResponse, + }); + }); + + await clusterClient.callAsInternalUser('ping', mockParams); + + expect(mockEsClientInstance.ping).toHaveBeenCalledWith({ + headers: { authorization: 'Bearer ABC123' }, + param: 'ping', + }); + }); + test('correctly deals with nested endpoint', async () => { const mockResponse = { data: 'authenticate' }; const mockParams = { param: 'authenticate' }; @@ -355,6 +379,31 @@ describe('#asScoped', () => { ); }); + test('does not set the authorization header when a service account token is configured', async () => { + clusterClient = new LegacyClusterClient( + { + apiVersion: 'es-version', + requestHeadersWhitelist: ['zero'], + serviceAccountToken: 'ABC123', + } as any, + logger.get(), + 'custom-type' + ); + + clusterClient.asScoped( + httpServerMock.createRawRequest({ headers: { zero: '0', one: '1', two: '2', three: '3' } }) + ); + + const expectedHeaders = { zero: '0' }; + + expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); + expect(MockScopedClusterClient).toHaveBeenCalledWith( + expect.any(Function), + expect.any(Function), + expectedHeaders + ); + }); + test('both scoped and internal API caller fail if cluster client is closed', async () => { clusterClient.asScoped( httpServerMock.createRawRequest({ headers: { zero: '0', one: '1', two: '2', three: '3' } }) diff --git a/src/core/server/elasticsearch/legacy/cluster_client.ts b/src/core/server/elasticsearch/legacy/cluster_client.ts index bdb2ca4d01b3c..6a6765b67da9f 100644 --- a/src/core/server/elasticsearch/legacy/cluster_client.ts +++ b/src/core/server/elasticsearch/legacy/cluster_client.ts @@ -147,6 +147,13 @@ export class LegacyClusterClient implements ILegacyClusterClient { ) => { this.assertIsNotClosed(); + if (this.config.serviceAccountToken) { + clientParams.headers = { + ...clientParams.headers, + authorization: `Bearer ${this.config.serviceAccountToken}`, + }; + } + return await (callAPI.bind(null, this.client) as LegacyAPICaller)( endpoint, clientParams, diff --git a/src/core/server/elasticsearch/legacy/elasticsearch_client_config.test.ts b/src/core/server/elasticsearch/legacy/elasticsearch_client_config.test.ts index 6239ad270d5b5..a343c0d5d2ad1 100644 --- a/src/core/server/elasticsearch/legacy/elasticsearch_client_config.test.ts +++ b/src/core/server/elasticsearch/legacy/elasticsearch_client_config.test.ts @@ -333,6 +333,128 @@ describe('#auth', () => { }); }); +describe('#serviceAccountToken', () => { + it('is set when #auth is true, and a token is provided', () => { + expect( + parseElasticsearchClientConfig( + { + apiVersion: 'v7.0.0', + customHeaders: { xsrf: 'something' }, + sniffOnStart: true, + sniffOnConnectionFault: true, + hosts: ['https://es.local'], + requestHeadersWhitelist: [], + serviceAccountToken: 'ABC123', + }, + logger.get(), + 'custom-type', + { auth: true } + ) + ).toMatchInlineSnapshot(` + Object { + "apiVersion": "v7.0.0", + "hosts": Array [ + Object { + "headers": Object { + "x-elastic-product-origin": "kibana", + "xsrf": "something", + }, + "host": "es.local", + "path": "/", + "port": "443", + "protocol": "https:", + "query": null, + }, + ], + "keepAlive": true, + "log": [Function], + "serviceAccountToken": "ABC123", + "sniffOnConnectionFault": true, + "sniffOnStart": true, + } + `); + }); + + it('is not set when #auth is true, and a token is not provided', () => { + expect( + parseElasticsearchClientConfig( + { + apiVersion: 'v7.0.0', + customHeaders: { xsrf: 'something' }, + sniffOnStart: true, + sniffOnConnectionFault: true, + hosts: ['https://es.local'], + requestHeadersWhitelist: [], + }, + logger.get(), + 'custom-type', + { auth: true } + ) + ).toMatchInlineSnapshot(` + Object { + "apiVersion": "v7.0.0", + "hosts": Array [ + Object { + "headers": Object { + "x-elastic-product-origin": "kibana", + "xsrf": "something", + }, + "host": "es.local", + "path": "/", + "port": "443", + "protocol": "https:", + "query": null, + }, + ], + "keepAlive": true, + "log": [Function], + "sniffOnConnectionFault": true, + "sniffOnStart": true, + } + `); + }); + + it('is not set when #auth is false, and a token is provided', () => { + expect( + parseElasticsearchClientConfig( + { + apiVersion: 'v7.0.0', + customHeaders: { xsrf: 'something' }, + sniffOnStart: true, + sniffOnConnectionFault: true, + hosts: ['https://es.local'], + requestHeadersWhitelist: [], + serviceAccountToken: 'ABC123', + }, + logger.get(), + 'custom-type', + { auth: false } + ) + ).toMatchInlineSnapshot(` + Object { + "apiVersion": "v7.0.0", + "hosts": Array [ + Object { + "headers": Object { + "x-elastic-product-origin": "kibana", + "xsrf": "something", + }, + "host": "es.local", + "path": "/", + "port": "443", + "protocol": "https:", + "query": null, + }, + ], + "keepAlive": true, + "log": [Function], + "sniffOnConnectionFault": true, + "sniffOnStart": true, + } + `); + }); +}); + describe('#customHeaders', () => { test('override the default headers', () => { const headerKey = Object.keys(DEFAULT_HEADERS)[0]; diff --git a/src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts b/src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts index d68e7635c57cb..3d81caefad457 100644 --- a/src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts +++ b/src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts @@ -35,6 +35,7 @@ export type LegacyElasticsearchClientConfig = Pick & { pingTimeout?: ElasticsearchConfig['pingTimeout'] | ConfigOptions['pingTimeout']; requestTimeout?: ElasticsearchConfig['requestTimeout'] | ConfigOptions['requestTimeout']; @@ -61,6 +62,7 @@ interface LegacyElasticsearchClientConfigOverrides { /** @internal */ type ExtendedConfigOptions = ConfigOptions & Partial<{ + serviceAccountToken?: string; ssl: Partial<{ rejectUnauthorized: boolean; checkServerIdentity: typeof checkServerIdentity; @@ -106,9 +108,14 @@ export function parseElasticsearchClientConfig( esClientConfig.sniffInterval = getDurationAsMs(config.sniffInterval); } - const needsAuth = auth !== false && config.username && config.password; + const needsAuth = + auth !== false && ((config.username && config.password) || config.serviceAccountToken); if (needsAuth) { - esClientConfig.httpAuth = `${config.username}:${config.password}`; + if (config.username) { + esClientConfig.httpAuth = `${config.username}:${config.password}`; + } else if (config.serviceAccountToken) { + esClientConfig.serviceAccountToken = config.serviceAccountToken; + } } if (Array.isArray(config.hosts)) { diff --git a/src/core/server/saved_objects/migrations/core/elastic_index.ts b/src/core/server/saved_objects/migrations/core/elastic_index.ts index 09c2935422b79..8bda77563be8c 100644 --- a/src/core/server/saved_objects/migrations/core/elastic_index.ts +++ b/src/core/server/saved_objects/migrations/core/elastic_index.ts @@ -40,6 +40,7 @@ export const REMOVED_TYPES: string[] = [ 'fleet-agent-events', // Was removed in 7.12 'ml-telemetry', + 'server', // https://github.com/elastic/kibana/issues/95617 'tsvb-validation-telemetry', ].sort(); diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts index 0481e6118acb0..c8ff79351aadb 100644 --- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts +++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts @@ -137,14 +137,15 @@ describe('migrateRawDocsSafely', () => { const transform = jest.fn((doc: any) => [ set(_.cloneDeep(doc), 'attributes.name', 'HOI!'), ]); - const task = migrateRawDocsSafely( - new SavedObjectsSerializer(new SavedObjectTypeRegistry()), - transform, - [ + const task = migrateRawDocsSafely({ + serializer: new SavedObjectsSerializer(new SavedObjectTypeRegistry()), + knownTypes: new Set(['a', 'c']), + migrateDoc: transform, + rawDocs: [ { _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }, { _id: 'c:d', _source: { type: 'c', c: { name: 'DDD' } } }, - ] - ); + ], + }); const result = (await task()) as Either.Right; expect(result._tag).toEqual('Right'); expect(result.right.processedDocs).toEqual([ @@ -181,14 +182,15 @@ describe('migrateRawDocsSafely', () => { const transform = jest.fn((doc: any) => [ set(_.cloneDeep(doc), 'attributes.name', 'TADA'), ]); - const task = migrateRawDocsSafely( - new SavedObjectsSerializer(new SavedObjectTypeRegistry()), - transform, - [ + const task = migrateRawDocsSafely({ + serializer: new SavedObjectsSerializer(new SavedObjectTypeRegistry()), + knownTypes: new Set(['a', 'c']), + migrateDoc: transform, + rawDocs: [ { _id: 'foo:b', _source: { type: 'a', a: { name: 'AAA' } } }, { _id: 'c:d', _source: { type: 'c', c: { name: 'DDD' } } }, - ] - ); + ], + }); const result = (await task()) as Either.Left; expect(transform).toHaveBeenCalledTimes(1); expect(result._tag).toEqual('Left'); @@ -202,11 +204,12 @@ describe('migrateRawDocsSafely', () => { set(_.cloneDeep(doc), 'attributes.name', 'HOI!'), { id: 'bar', type: 'foo', attributes: { name: 'baz' } }, ]); - const task = migrateRawDocsSafely( - new SavedObjectsSerializer(new SavedObjectTypeRegistry()), - transform, - [{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }] - ); + const task = migrateRawDocsSafely({ + serializer: new SavedObjectsSerializer(new SavedObjectTypeRegistry()), + knownTypes: new Set(['a', 'c']), + migrateDoc: transform, + rawDocs: [{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }], + }); const result = (await task()) as Either.Right; expect(result._tag).toEqual('Right'); expect(result.right.processedDocs).toEqual([ @@ -235,11 +238,12 @@ describe('migrateRawDocsSafely', () => { const transform = jest.fn((doc: any) => { throw new TransformSavedObjectDocumentError(new Error('error during transform'), '8.0.0'); }); - const task = migrateRawDocsSafely( - new SavedObjectsSerializer(new SavedObjectTypeRegistry()), - transform, - [{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }] // this is the raw doc - ); + const task = migrateRawDocsSafely({ + serializer: new SavedObjectsSerializer(new SavedObjectTypeRegistry()), + knownTypes: new Set(['a', 'c']), + migrateDoc: transform, + rawDocs: [{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }], // this is the raw doc + }); const result = (await task()) as Either.Left; expect(transform).toHaveBeenCalledTimes(1); expect(result._tag).toEqual('Left'); @@ -252,4 +256,43 @@ describe('migrateRawDocsSafely', () => { } `); }); + + test('skips documents of unknown types', async () => { + const transform = jest.fn((doc: any) => [ + set(_.cloneDeep(doc), 'attributes.name', 'HOI!'), + ]); + const task = migrateRawDocsSafely({ + serializer: new SavedObjectsSerializer(new SavedObjectTypeRegistry()), + knownTypes: new Set(['a']), + migrateDoc: transform, + rawDocs: [ + { _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }, + { _id: 'c:d', _source: { type: 'c', c: { name: 'DDD' } } }, + ], + }); + + const result = (await task()) as Either.Right; + expect(result._tag).toEqual('Right'); + expect(result.right.processedDocs).toEqual([ + { + _id: 'a:b', + _source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] }, + }, + { + _id: 'c:d', + // name field is not migrated on unknown type + _source: { type: 'c', c: { name: 'DDD' } }, + }, + ]); + + const obj1 = { + id: 'b', + type: 'a', + attributes: { name: 'AAA' }, + migrationVersion: {}, + references: [], + }; + expect(transform).toHaveBeenCalledTimes(1); + expect(transform).toHaveBeenNthCalledWith(1, obj1); + }); }); diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts index 461ae1df6bc3d..65ea21a6778d5 100644 --- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts +++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts @@ -81,6 +81,13 @@ export async function migrateRawDocs( return processedDocs; } +interface MigrateRawDocsSafelyDeps { + serializer: SavedObjectsSerializer; + knownTypes: ReadonlySet; + migrateDoc: MigrateAndConvertFn; + rawDocs: SavedObjectsRawDoc[]; +} + /** * Applies the specified migration function to every saved object document provided * and converts the saved object to a raw document. @@ -88,11 +95,15 @@ export async function migrateRawDocs( * for which the transformation function failed. * @returns {TaskEither.TaskEither} */ -export function migrateRawDocsSafely( - serializer: SavedObjectsSerializer, - migrateDoc: MigrateAndConvertFn, - rawDocs: SavedObjectsRawDoc[] -): TaskEither.TaskEither { +export function migrateRawDocsSafely({ + serializer, + knownTypes, + migrateDoc, + rawDocs, +}: MigrateRawDocsSafelyDeps): TaskEither.TaskEither< + DocumentsTransformFailed, + DocumentsTransformSuccess +> { return async () => { const migrateDocNonBlocking = transformNonBlocking(migrateDoc); const processedDocs: SavedObjectsRawDoc[] = []; @@ -100,7 +111,10 @@ export function migrateRawDocsSafely( const corruptSavedObjectIds: string[] = []; const options = { namespaceTreatment: 'lax' as const }; for (const raw of rawDocs) { - if (serializer.isRawSavedObject(raw, options)) { + // Do not transform documents of unknown types + if (raw?._source?.type && !knownTypes.has(raw._source.type)) { + processedDocs.push(raw); + } else if (serializer.isRawSavedObject(raw, options)) { try { const savedObject = convertToRawAddMigrationVersion(raw, options, serializer); processedDocs.push( diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts index 2d0282e6d2632..572b2934e49b8 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts @@ -184,11 +184,12 @@ export class KibanaMigrator { logger: this.log, preMigrationScript: indexMap[index].script, transformRawDocs: (rawDocs: SavedObjectsRawDoc[]) => - migrateRawDocsSafely( - this.serializer, - this.documentMigrator.migrateAndConvert, - rawDocs - ), + migrateRawDocsSafely({ + serializer: this.serializer, + knownTypes: new Set(this.typeRegistry.getAllTypes().map((t) => t.name)), + migrateDoc: this.documentMigrator.migrateAndConvert, + rawDocs, + }), migrationVersionPerType: this.documentMigrator.migrationVersion, indexPrefix: index, migrationsConfig: this.soMigrationsConfig, diff --git a/src/core/server/saved_objects/migrationsv2/actions/check_for_unknown_docs.test.ts b/src/core/server/saved_objects/migrationsv2/actions/check_for_unknown_docs.test.ts index 62a619ef447fa..a52cb2a922968 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/check_for_unknown_docs.test.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/check_for_unknown_docs.test.ts @@ -97,9 +97,12 @@ describe('checkForUnknownDocs', () => { const result = await task(); expect(Either.isRight(result)).toBe(true); + expect((result as Either.Right).right).toEqual({ + unknownDocs: [], + }); }); - it('resolves with `Either.left` when unknown docs are found', async () => { + it('resolves with `Either.right` when unknown docs are found', async () => { const client = elasticsearchClientMock.createInternalClient( elasticsearchClientMock.createSuccessTransportRequestPromise({ hits: { @@ -120,9 +123,8 @@ describe('checkForUnknownDocs', () => { const result = await task(); - expect(Either.isLeft(result)).toBe(true); - expect((result as Either.Left).left).toEqual({ - type: 'unknown_docs_found', + expect(Either.isRight(result)).toBe(true); + expect((result as Either.Right).right).toEqual({ unknownDocs: [ { id: '12', type: 'foo' }, { id: '14', type: 'bar' }, @@ -148,9 +150,8 @@ describe('checkForUnknownDocs', () => { const result = await task(); - expect(Either.isLeft(result)).toBe(true); - expect((result as Either.Left).left).toEqual({ - type: 'unknown_docs_found', + expect(Either.isRight(result)).toBe(true); + expect((result as Either.Right).right).toEqual({ unknownDocs: [{ id: '12', type: 'unknown' }], }); }); diff --git a/src/core/server/saved_objects/migrationsv2/actions/check_for_unknown_docs.ts b/src/core/server/saved_objects/migrationsv2/actions/check_for_unknown_docs.ts index 7cc1c26a2ea8b..e3d72fbdf866f 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/check_for_unknown_docs.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/check_for_unknown_docs.ts @@ -32,7 +32,6 @@ export interface CheckForUnknownDocsFoundDoc { /** @internal */ export interface UnknownDocsFound { - type: 'unknown_docs_found'; unknownDocs: CheckForUnknownDocsFoundDoc[]; } @@ -42,8 +41,8 @@ export const checkForUnknownDocs = ({ unusedTypesQuery, knownTypes, }: CheckForUnknownDocsParams): TaskEither.TaskEither< - RetryableEsClientError | UnknownDocsFound, - {} + RetryableEsClientError, + UnknownDocsFound > => () => { const query = createUnknownDocQuery(unusedTypesQuery, knownTypes); @@ -56,14 +55,9 @@ export const checkForUnknownDocs = ({ }) .then((response) => { const { hits } = response.body.hits; - if (hits.length) { - return Either.left({ - type: 'unknown_docs_found' as const, - unknownDocs: hits.map((hit) => ({ id: hit._id, type: hit._source?.type ?? 'unknown' })), - }); - } else { - return Either.right({}); - } + return Either.right({ + unknownDocs: hits.map((hit) => ({ id: hit._id, type: hit._source?.type ?? 'unknown' })), + }); }) .catch(catchRetryableEsClientErrors); }; diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.ts b/src/core/server/saved_objects/migrationsv2/actions/index.ts index 8e4584970f138..6bfcddfe1f6de 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.ts @@ -80,7 +80,6 @@ export type { } from './update_and_pickup_mappings'; export { updateAndPickupMappings } from './update_and_pickup_mappings'; -import type { UnknownDocsFound } from './check_for_unknown_docs'; export type { CheckForUnknownDocsParams, UnknownDocsFound, @@ -131,7 +130,6 @@ export interface ActionErrorTypeMap { alias_not_found_exception: AliasNotFound; remove_index_not_a_concrete_index: RemoveIndexNotAConcreteIndex; documents_transform_failed: DocumentsTransformFailed; - unknown_docs_found: UnknownDocsFound; } /** diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7_13_0_unknown_types.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7_13_0_unknown_types.test.ts index c5e302adbe903..a30b3d291e7ec 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7_13_0_unknown_types.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7_13_0_unknown_types.test.ts @@ -7,42 +7,31 @@ */ import Path from 'path'; -import Fs from 'fs'; -import Util from 'util'; +import fs from 'fs/promises'; import { estypes } from '@elastic/elasticsearch'; import * as kbnTestServer from '../../../../test_helpers/kbn_server'; import { Root } from '../../../root'; +import JSON5 from 'json5'; +import { ElasticsearchClient } from '../../../elasticsearch'; const logFilePath = Path.join(__dirname, '7_13_unknown_types_test.log'); -const asyncUnlink = Util.promisify(Fs.unlink); - async function removeLogFile() { // ignore errors if it doesn't exist - await asyncUnlink(logFilePath).catch(() => void 0); + await fs.unlink(logFilePath).catch(() => void 0); } describe('migration v2', () => { let esServer: kbnTestServer.TestElasticsearchUtils; let root: Root; + let startES: () => Promise; beforeAll(async () => { await removeLogFile(); }); - afterAll(async () => { - if (root) { - await root.shutdown(); - } - if (esServer) { - await esServer.stop(); - } - - await new Promise((resolve) => setTimeout(resolve, 10000)); - }); - - it('migrates the documents to the highest version', async () => { - const { startES } = kbnTestServer.createTestServers({ + beforeEach(() => { + ({ startES } = kbnTestServer.createTestServers({ adjustTimeout: (t: number) => jest.setTimeout(t), settings: { es: { @@ -53,50 +42,155 @@ describe('migration v2', () => { dataArchive: Path.join(__dirname, 'archives', '7.13.0_with_unknown_so.zip'), }, }, - }); + })); + }); + + afterEach(async () => { + if (root) { + await root.shutdown(); + } + if (esServer) { + await esServer.stop(); + } + + await new Promise((resolve) => setTimeout(resolve, 10000)); + }); + it('logs a warning and completes the migration with unknown docs retained', async () => { root = createRoot(); + esServer = await startES(); + await root.setup(); + await root.start(); + + const logFileContent = await fs.readFile(logFilePath, 'utf-8'); + const records = logFileContent + .split('\n') + .filter(Boolean) + .map((str) => JSON5.parse(str)); + + const unknownDocsWarningLog = records.find((rec) => + rec.message.startsWith(`[.kibana] CHECK_UNKNOWN_DOCUMENTS`) + ); + + expect( + unknownDocsWarningLog.message.startsWith( + '[.kibana] CHECK_UNKNOWN_DOCUMENTS Upgrades will fail for 8.0+ because documents were found for unknown saved ' + + 'object types. To ensure that upgrades will succeed in the future, either re-enable plugins or delete ' + + 'these documents from the ".kibana_8.0.0_001" index after the current upgrade completes.' + ) + ).toBeTruthy(); + + const unknownDocs = [ + { type: 'space', id: 'space:default' }, + { type: 'space', id: 'space:first' }, + { type: 'space', id: 'space:second' }, + { type: 'space', id: 'space:third' }, + { type: 'space', id: 'space:forth' }, + { type: 'space', id: 'space:fifth' }, + { type: 'space', id: 'space:sixth' }, + { type: 'foo', id: 'P2SQfHkBs3dBRGh--No5' }, + { type: 'foo', id: 'QGSZfHkBs3dBRGh-ANoD' }, + { type: 'foo', id: 'QWSZfHkBs3dBRGh-hNob' }, + ]; + + unknownDocs.forEach(({ id, type }) => { + expect(unknownDocsWarningLog.message).toEqual( + expect.stringContaining(`- "${id}" (type: "${type}")`) + ); + }); + + const client: ElasticsearchClient = esServer.es.getClient(); + const { body: response } = await client.indices.getSettings({ index: '.kibana_8.0.0_001' }); + const settings = response['.kibana_8.0.0_001'] + .settings as estypes.IndicesIndexStatePrefixedSettings; + expect(settings.index).not.toBeUndefined(); + expect(settings.index!.blocks?.write).not.toEqual('true'); + + // Ensure that documents for unknown types were preserved in target index in an unmigrated state + const spaceDocs = await fetchDocs(client, '.kibana_8.0.0_001', 'space'); + expect(spaceDocs.map((s) => s.id)).toEqual( + expect.arrayContaining([ + 'space:default', + 'space:first', + 'space:second', + 'space:third', + 'space:forth', + 'space:fifth', + 'space:sixth', + ]) + ); + spaceDocs.forEach((d) => { + expect(d.migrationVersion.space).toEqual('6.6.0'); + expect(d.coreMigrationVersion).toEqual('7.13.0'); + }); + const fooDocs = await fetchDocs(client, '.kibana_8.0.0_001', 'foo'); + expect(fooDocs.map((f) => f.id)).toEqual( + expect.arrayContaining([ + 'P2SQfHkBs3dBRGh--No5', + 'QGSZfHkBs3dBRGh-ANoD', + 'QWSZfHkBs3dBRGh-hNob', + ]) + ); + fooDocs.forEach((d) => { + expect(d.migrationVersion.foo).toEqual('7.13.0'); + expect(d.coreMigrationVersion).toEqual('7.13.0'); + }); + }); + it('migrates outdated documents when types are re-enabled', async () => { + // Start kibana with foo and space types disabled + root = createRoot(); esServer = await startES(); await root.setup(); + await root.start(); - try { - await root.start(); - } catch (err) { - const errorMessage = err.message; - - expect( - errorMessage.startsWith( - 'Unable to complete saved object migrations for the [.kibana] index: Migration failed because documents ' + - 'were found for unknown saved object types. To proceed with the migration, please delete these documents from the ' + - '".kibana_7.13.0_001" index.' - ) - ).toBeTruthy(); - - const unknownDocs = [ - { type: 'space', id: 'space:default' }, - { type: 'space', id: 'space:first' }, - { type: 'space', id: 'space:second' }, - { type: 'space', id: 'space:third' }, - { type: 'space', id: 'space:forth' }, - { type: 'space', id: 'space:fifth' }, - { type: 'space', id: 'space:sixth' }, - { type: 'foo', id: 'P2SQfHkBs3dBRGh--No5' }, - { type: 'foo', id: 'QGSZfHkBs3dBRGh-ANoD' }, - { type: 'foo', id: 'QWSZfHkBs3dBRGh-hNob' }, - ]; - - unknownDocs.forEach(({ id, type }) => { - expect(errorMessage).toEqual(expect.stringContaining(`- "${id}" (type: "${type}")`)); - }); - - const client = esServer.es.getClient(); - const { body: response } = await client.indices.getSettings({ index: '.kibana_7.13.0_001' }); - const settings = response['.kibana_7.13.0_001'] - .settings as estypes.IndicesIndexStatePrefixedSettings; - expect(settings.index).not.toBeUndefined(); - expect(settings.index!.blocks?.write).not.toEqual('true'); - } + // Shutdown and start Kibana again with space type registered to ensure space docs get migrated + await root.shutdown(); + root = createRoot(); + const coreSetup = await root.setup(); + coreSetup.savedObjects.registerType({ + name: 'space', + hidden: false, + mappings: { properties: {} }, + namespaceType: 'agnostic', + migrations: { + '6.6.0': (d) => d, + '8.0.0': (d) => d, + }, + }); + await root.start(); + + const client: ElasticsearchClient = esServer.es.getClient(); + const spacesDocsMigrated = await fetchDocs(client, '.kibana_8.0.0_001', 'space'); + expect(spacesDocsMigrated.map((s) => s.id)).toEqual( + expect.arrayContaining([ + 'space:default', + 'space:first', + 'space:second', + 'space:third', + 'space:forth', + 'space:fifth', + 'space:sixth', + ]) + ); + spacesDocsMigrated.forEach((d) => { + expect(d.migrationVersion.space).toEqual('8.0.0'); // should be migrated + expect(d.coreMigrationVersion).toEqual('8.0.0'); + }); + + // Make sure unmigrated foo docs are also still there in an unmigrated state + const fooDocsUnmigrated = await fetchDocs(client, '.kibana_8.0.0_001', 'foo'); + expect(fooDocsUnmigrated.map((f) => f.id)).toEqual( + expect.arrayContaining([ + 'P2SQfHkBs3dBRGh--No5', + 'QGSZfHkBs3dBRGh-ANoD', + 'QWSZfHkBs3dBRGh-hNob', + ]) + ); + fooDocsUnmigrated.forEach((d) => { + expect(d.migrationVersion.foo).toEqual('7.13.0'); // should still not be migrated + expect(d.coreMigrationVersion).toEqual('7.13.0'); + }); }); }); @@ -131,3 +225,26 @@ function createRoot() { } ); } + +async function fetchDocs(esClient: ElasticsearchClient, index: string, type: string) { + const { body } = await esClient.search({ + index, + size: 10000, + body: { + query: { + bool: { + should: [ + { + term: { type }, + }, + ], + }, + }, + }, + }); + + return body.hits.hits.map((h) => ({ + ...h._source, + id: h._id, + })); +} diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/type_registrations.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/type_registrations.test.ts index 47c492622fa3f..e8871586cdfb7 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/type_registrations.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/type_registrations.test.ts @@ -79,6 +79,7 @@ const previouslyRegisteredTypes = [ 'search-telemetry', 'security-rule', 'security-solution-signals-migration', + 'server', 'siem-detection-engine-rule-actions', 'siem-detection-engine-rule-status', 'siem-ui-timeline', diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts index 8443f837a7f1d..cd42d4077695e 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts +++ b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts @@ -52,6 +52,8 @@ const logStateTransition = ( switch (level) { case 'error': return logger.error(logMessagePrefix + message); + case 'warning': + return logger.warn(logMessagePrefix + message); case 'info': return logger.info(logMessagePrefix + message); default: diff --git a/src/core/server/saved_objects/migrationsv2/model/extract_errors.test.ts b/src/core/server/saved_objects/migrationsv2/model/extract_errors.test.ts index a028c40ca6597..c2daadcd342ac 100644 --- a/src/core/server/saved_objects/migrationsv2/model/extract_errors.test.ts +++ b/src/core/server/saved_objects/migrationsv2/model/extract_errors.test.ts @@ -25,7 +25,7 @@ describe('extractUnknownDocFailureReason', () => { '.kibana_15' ) ).toMatchInlineSnapshot(` - "Migration failed because documents were found for unknown saved object types. To proceed with the migration, please delete these documents from the \\".kibana_15\\" index. + "Upgrades will fail for 8.0+ because documents were found for unknown saved object types. To ensure that upgrades will succeed in the future, either re-enable plugins or delete these documents from the \\".kibana_15\\" index after the current upgrade completes. The documents with unknown types are: - \\"unknownType:12\\" (type: \\"unknownType\\") - \\"anotherUnknownType:42\\" (type: \\"anotherUnknownType\\") diff --git a/src/core/server/saved_objects/migrationsv2/model/extract_errors.ts b/src/core/server/saved_objects/migrationsv2/model/extract_errors.ts index cc6fe7bad3ca7..082e6344afffc 100644 --- a/src/core/server/saved_objects/migrationsv2/model/extract_errors.ts +++ b/src/core/server/saved_objects/migrationsv2/model/extract_errors.ts @@ -38,15 +38,16 @@ export function extractTransformFailuresReason( export function extractUnknownDocFailureReason( unknownDocs: CheckForUnknownDocsFoundDoc[], - sourceIndex: string + targetIndex: string ): string { return ( - `Migration failed because documents were found for unknown saved object types. ` + - `To proceed with the migration, please delete these documents from the "${sourceIndex}" index.\n` + + `Upgrades will fail for 8.0+ because documents were found for unknown saved object types. ` + + `To ensure that upgrades will succeed in the future, either re-enable plugins or delete these documents from the ` + + `"${targetIndex}" index after the current upgrade completes.\n` + `The documents with unknown types are:\n` + unknownDocs.map((doc) => `- "${doc.id}" (type: "${doc.type}")\n`).join('') + `You can delete them using the following command:\n` + - `curl -X POST "{elasticsearch}/${sourceIndex}/_bulk?pretty" -H 'Content-Type: application/json' -d'\n` + + `curl -X POST "{elasticsearch}/${targetIndex}/_bulk?pretty" -H 'Content-Type: application/json' -d'\n` + unknownDocs.map((doc) => `{ "delete" : { "_id" : "${doc.id}" } }\n`).join('') + `'` ); diff --git a/src/core/server/saved_objects/migrationsv2/model/model.test.ts b/src/core/server/saved_objects/migrationsv2/model/model.test.ts index 136709d1b874f..3058f586efb0c 100644 --- a/src/core/server/saved_objects/migrationsv2/model/model.test.ts +++ b/src/core/server/saved_objects/migrationsv2/model/model.test.ts @@ -715,7 +715,7 @@ describe('migrations v2 model', () => { }, } as const; - test('CHECK_UNKNOWN_DOCUMENTS -> SET_SOURCE_WRITE_BLOCK if action succeeds', () => { + test('CHECK_UNKNOWN_DOCUMENTS -> SET_SOURCE_WRITE_BLOCK if action succeeds and no unknown docs are found', () => { const checkUnknownDocumentsSourceState: CheckUnknownDocumentsState = { ...baseState, controlState: 'CHECK_UNKNOWN_DOCUMENTS', @@ -723,7 +723,7 @@ describe('migrations v2 model', () => { sourceIndexMappings: mappingsWithUnknownType, }; - const res: ResponseType<'CHECK_UNKNOWN_DOCUMENTS'> = Either.right({}); + const res: ResponseType<'CHECK_UNKNOWN_DOCUMENTS'> = Either.right({ unknownDocs: [] }); const newState = model(checkUnknownDocumentsSourceState, res); expect(newState.controlState).toEqual('SET_SOURCE_WRITE_BLOCK'); @@ -758,9 +758,12 @@ describe('migrations v2 model', () => { }, } `); + + // No log message gets appended + expect(newState.logs).toEqual([]); }); - test('CHECK_UNKNOWN_DOCUMENTS -> FATAL if action fails and unknown docs were found', () => { + test('CHECK_UNKNOWN_DOCUMENTS -> SET_SOURCE_WRITE_BLOCK and adds log if action succeeds and unknown docs were found', () => { const checkUnknownDocumentsSourceState: CheckUnknownDocumentsState = { ...baseState, controlState: 'CHECK_UNKNOWN_DOCUMENTS', @@ -768,20 +771,51 @@ describe('migrations v2 model', () => { sourceIndexMappings: mappingsWithUnknownType, }; - const res: ResponseType<'CHECK_UNKNOWN_DOCUMENTS'> = Either.left({ - type: 'unknown_docs_found', + const res: ResponseType<'CHECK_UNKNOWN_DOCUMENTS'> = Either.right({ unknownDocs: [ { id: 'dashboard:12', type: 'dashboard' }, { id: 'foo:17', type: 'foo' }, ], }); const newState = model(checkUnknownDocumentsSourceState, res); - expect(newState.controlState).toEqual('FATAL'); + expect(newState.controlState).toEqual('SET_SOURCE_WRITE_BLOCK'); expect(newState).toMatchObject({ - controlState: 'FATAL', - reason: expect.stringContaining( - 'Migration failed because documents were found for unknown saved object types' + controlState: 'SET_SOURCE_WRITE_BLOCK', + sourceIndex: Option.some('.kibana_3'), + targetIndex: '.kibana_7.11.0_001', + }); + + // This snapshot asserts that we disable the unknown saved object + // type. Because it's mappings are disabled, we also don't copy the + // `_meta.migrationMappingPropertyHashes` for the disabled type. + expect(newState.targetIndexMappings).toMatchInlineSnapshot(` + Object { + "_meta": Object { + "migrationMappingPropertyHashes": Object { + "new_saved_object_type": "4a11183eee21e6fbad864f7a30b39ad0", + }, + }, + "properties": Object { + "disabled_saved_object_type": Object { + "dynamic": false, + "properties": Object {}, + }, + "new_saved_object_type": Object { + "properties": Object { + "value": Object { + "type": "text", + }, + }, + }, + }, + } + `); + + expect(newState.logs[0]).toMatchObject({ + level: 'warning', + message: expect.stringContaining( + 'Upgrades will fail for 8.0+ because documents were found for unknown saved object types' ), }); }); diff --git a/src/core/server/saved_objects/migrationsv2/model/model.ts b/src/core/server/saved_objects/migrationsv2/model/model.ts index b28e4e3024380..a78457fa891f7 100644 --- a/src/core/server/saved_objects/migrationsv2/model/model.ts +++ b/src/core/server/saved_objects/migrationsv2/model/model.ts @@ -10,7 +10,7 @@ import * as Either from 'fp-ts/lib/Either'; import * as Option from 'fp-ts/lib/Option'; import { AliasAction, isLeftTypeof } from '../actions'; -import { AllActionStates, State } from '../types'; +import { AllActionStates, MigrationLog, State } from '../types'; import type { ResponseType } from '../next'; import { disableUnknownTypeMappingFields } from '../../migrations/core/migration_context'; import { @@ -318,6 +318,7 @@ export const model = (currentState: State, resW: ResponseType): } } else if (stateP.controlState === 'CHECK_UNKNOWN_DOCUMENTS') { const res = resW as ExcludeRetryableEsError>; + if (Either.isRight(res)) { const source = stateP.sourceIndex; const target = stateP.versionIndex; @@ -336,17 +337,24 @@ export const model = (currentState: State, resW: ResponseType): { add: { index: target, alias: stateP.versionAlias } }, { remove_index: { index: stateP.tempIndex } }, ]), + + logs: [ + ...stateP.logs, + ...(res.right.unknownDocs.length > 0 + ? ([ + { + level: 'warning', + message: `CHECK_UNKNOWN_DOCUMENTS ${extractUnknownDocFailureReason( + res.right.unknownDocs, + target + )}`, + }, + ] as MigrationLog[]) + : []), + ], }; } else { - if (isLeftTypeof(res.left, 'unknown_docs_found')) { - return { - ...stateP, - controlState: 'FATAL', - reason: extractUnknownDocFailureReason(res.left.unknownDocs, stateP.sourceIndex.value), - }; - } else { - return throwBadResponse(stateP, res.left); - } + return throwBadResponse(stateP, res); } } else if (stateP.controlState === 'SET_SOURCE_WRITE_BLOCK') { const res = resW as ExcludeRetryableEsError>; diff --git a/src/core/server/saved_objects/migrationsv2/types.ts b/src/core/server/saved_objects/migrationsv2/types.ts index ed361a710ac99..576e3a4412184 100644 --- a/src/core/server/saved_objects/migrationsv2/types.ts +++ b/src/core/server/saved_objects/migrationsv2/types.ts @@ -19,7 +19,7 @@ import { DocumentsTransformSuccess, } from '../migrations/core/migrate_raw_docs'; -export type MigrationLogLevel = 'error' | 'info'; +export type MigrationLogLevel = 'error' | 'info' | 'warning'; export interface MigrationLog { level: MigrationLogLevel; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index ed55c6e3d09cb..65ea082c9d8a8 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -345,6 +345,7 @@ export const config: { hosts: Type; username: Type; password: Type; + serviceAccountToken: Type; requestHeadersWhitelist: Type; customHeaders: Type>; shardTimeout: Type; @@ -948,7 +949,7 @@ export type ElasticsearchClient = Omit & { +export type ElasticsearchClientConfig = Pick & { pingTimeout?: ElasticsearchConfig['pingTimeout'] | ClientOptions['pingTimeout']; requestTimeout?: ElasticsearchConfig['requestTimeout'] | ClientOptions['requestTimeout']; ssl?: Partial; @@ -968,6 +969,7 @@ export class ElasticsearchConfig { readonly pingTimeout: Duration; readonly requestHeadersWhitelist: string[]; readonly requestTimeout: Duration; + readonly serviceAccountToken?: string; readonly shardTimeout: Duration; readonly sniffInterval: false | Duration; readonly sniffOnConnectionFault: boolean; @@ -1675,7 +1677,7 @@ export class LegacyClusterClient implements ILegacyClusterClient { } // @public @deprecated (undocumented) -export type LegacyElasticsearchClientConfig = Pick & Pick & { +export type LegacyElasticsearchClientConfig = Pick & Pick & { pingTimeout?: ElasticsearchConfig['pingTimeout'] | ConfigOptions['pingTimeout']; requestTimeout?: ElasticsearchConfig['requestTimeout'] | ConfigOptions['requestTimeout']; sniffInterval?: ElasticsearchConfig['sniffInterval'] | ConfigOptions['sniffInterval']; diff --git a/src/dev/build/tasks/install_chromium.js b/src/dev/build/tasks/install_chromium.js index 37abcbad4466e..95e0df8984f9d 100644 --- a/src/dev/build/tasks/install_chromium.js +++ b/src/dev/build/tasks/install_chromium.js @@ -19,7 +19,6 @@ export const InstallChromium = { log.info(`Installing Chromium for ${platform.getName()}-${platform.getArchitecture()}`); const { binaryPath$ } = installBrowser( - // TODO: https://github.com/elastic/kibana/issues/72496 log, build.resolvePathForPlatform(platform, 'x-pack/plugins/reporting/chromium'), platform.getName(), diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index 39a7665f1ce5e..c7a129418765b 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -51,6 +51,7 @@ kibana_vars=( elasticsearch.pingTimeout elasticsearch.requestHeadersWhitelist elasticsearch.requestTimeout + elasticsearch.serviceAccountToken elasticsearch.shardTimeout elasticsearch.sniffInterval elasticsearch.sniffOnConnectionFault diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index ba18c085b649d..57ae640da3c84 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -77,7 +77,11 @@ export const IGNORE_FILE_GLOBS = [ * * @type {Array} */ -export const KEBAB_CASE_DIRECTORY_GLOBS = ['packages/*', 'x-pack']; +export const KEBAB_CASE_DIRECTORY_GLOBS = [ + 'packages/*', + 'x-pack', + 'packages/kbn-optimizer/src/__fixtures__/mock_repo/packages/kbn-ui-shared-deps', +]; /** * These patterns are matched against directories and indicate diff --git a/src/plugins/console/server/lib/spec_definitions/js/ingest.ts b/src/plugins/console/server/lib/spec_definitions/js/ingest.ts index 2ac8d2cb87ccc..5ffae9d7be23e 100644 --- a/src/plugins/console/server/lib/spec_definitions/js/ingest.ts +++ b/src/plugins/console/server/lib/spec_definitions/js/ingest.ts @@ -179,6 +179,29 @@ const dropProcessorDefinition = { }, }; +// Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/enrich-processor.html +const enrichProcessorDefinition = { + enrich: { + __template: { + policy_name: '', + field: '', + target_field: '', + }, + policy_name: '', + field: '', + target_field: '', + ignore_missing: { + __one_of: [false, true], + }, + override: { + __one_of: [true, false], + }, + max_matches: 1, + shape_relation: 'INTERSECTS', + ...commonPipelineParams, + }, +}; + // Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/fail-processor.html const failProcessorDefinition = { fail: { @@ -279,13 +302,26 @@ const inferenceProcessorDefinition = { inference: { __template: { model_id: '', - field_map: {}, inference_config: {}, + field_mappings: {}, }, - model_id: '', - field_map: {}, - inference_config: {}, target_field: '', + model_id: '', + field_mappings: { + __template: {}, + }, + inference_config: { + regression: { + __template: {}, + results_field: '', + }, + classification: { + __template: {}, + results_field: '', + num_top_classes: 2, + top_classes_results_field: '', + }, + }, ...commonPipelineParams, }, }; @@ -530,6 +566,7 @@ const processorDefinition = { dissectProcessorDefinition, dotExpanderProcessorDefinition, dropProcessorDefinition, + enrichProcessorDefinition, failProcessorDefinition, foreachProcessorDefinition, geoipProcessorDefinition, diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/async_search.delete.json b/src/plugins/console/server/lib/spec_definitions/json/generated/async_search.delete.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/async_search.delete.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/async_search.delete.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/async_search.get.json b/src/plugins/console/server/lib/spec_definitions/json/generated/async_search.get.json similarity index 88% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/async_search.get.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/async_search.get.json index 09f4520d580e3..3fb1f3da6fdf7 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/async_search.get.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/async_search.get.json @@ -1,7 +1,7 @@ { "async_search.get": { "url_params": { - "wait_for_completion": "", + "wait_for_completion_timeout": "", "keep_alive": "", "typed_keys": "__flag__" }, diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/async_search.status.json b/src/plugins/console/server/lib/spec_definitions/json/generated/async_search.status.json new file mode 100644 index 0000000000000..f2aef917ea23d --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/async_search.status.json @@ -0,0 +1,11 @@ +{ + "async_search.status": { + "methods": [ + "GET" + ], + "patterns": [ + "_async_search/status/{id}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/async-search.html" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/async_search.submit.json b/src/plugins/console/server/lib/spec_definitions/json/generated/async_search.submit.json similarity index 92% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/async_search.submit.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/async_search.submit.json index 83fb7c0fe75ad..635436bdb167f 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/async_search.submit.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/async_search.submit.json @@ -1,8 +1,8 @@ { "async_search.submit": { "url_params": { - "wait_for_completion": "", - "clean_on_completion": "__flag__", + "wait_for_completion_timeout": "", + "keep_on_completion": "__flag__", "keep_alive": "", "batched_reduce_size": "", "request_cache": "__flag__", @@ -23,6 +23,7 @@ "expand_wildcards": [ "open", "closed", + "hidden", "none", "all" ], @@ -51,7 +52,7 @@ "suggest_text": "", "timeout": "", "track_scores": "__flag__", - "track_total_hits": "__flag__", + "track_total_hits": "", "allow_partial_search_results": "__flag__", "typed_keys": "__flag__", "version": "__flag__", diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/autoscaling.delete_autoscaling_policy.json b/src/plugins/console/server/lib/spec_definitions/json/generated/autoscaling.delete_autoscaling_policy.json new file mode 100644 index 0000000000000..2d65c9744987c --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/autoscaling.delete_autoscaling_policy.json @@ -0,0 +1,11 @@ +{ + "autoscaling.delete_autoscaling_policy": { + "methods": [ + "DELETE" + ], + "patterns": [ + "_autoscaling/policy/{name}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/autoscaling-delete-autoscaling-policy.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/autoscaling.get_autoscaling_capacity.json b/src/plugins/console/server/lib/spec_definitions/json/generated/autoscaling.get_autoscaling_capacity.json new file mode 100644 index 0000000000000..24aea8cfba86b --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/autoscaling.get_autoscaling_capacity.json @@ -0,0 +1,11 @@ +{ + "autoscaling.get_autoscaling_capacity": { + "methods": [ + "GET" + ], + "patterns": [ + "_autoscaling/capacity" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/autoscaling-get-autoscaling-capacity.html" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/autoscaling.get_autoscaling_decision.json b/src/plugins/console/server/lib/spec_definitions/json/generated/autoscaling.get_autoscaling_decision.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/autoscaling.get_autoscaling_decision.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/autoscaling.get_autoscaling_decision.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/autoscaling.get_autoscaling_policy.json b/src/plugins/console/server/lib/spec_definitions/json/generated/autoscaling.get_autoscaling_policy.json new file mode 100644 index 0000000000000..4d5ab1d00266b --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/autoscaling.get_autoscaling_policy.json @@ -0,0 +1,11 @@ +{ + "autoscaling.get_autoscaling_policy": { + "methods": [ + "GET" + ], + "patterns": [ + "_autoscaling/policy/{name}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/autoscaling-get-autoscaling-policy.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/autoscaling.put_autoscaling_policy.json b/src/plugins/console/server/lib/spec_definitions/json/generated/autoscaling.put_autoscaling_policy.json new file mode 100644 index 0000000000000..3ed40851c94d9 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/autoscaling.put_autoscaling_policy.json @@ -0,0 +1,11 @@ +{ + "autoscaling.put_autoscaling_policy": { + "methods": [ + "PUT" + ], + "patterns": [ + "_autoscaling/policy/{name}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/autoscaling-put-autoscaling-policy.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/bulk.json b/src/plugins/console/server/lib/spec_definitions/json/generated/bulk.json index 2d3bd260372ba..144a3ad359c40 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/bulk.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/bulk.json @@ -13,7 +13,8 @@ "_source": [], "_source_excludes": [], "_source_includes": [], - "pipeline": "" + "pipeline": "", + "require_alias": "__flag__" }, "methods": [ "POST", diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.indices.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.indices.json index 410350df13721..a0d405f60904d 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.indices.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.indices.json @@ -15,7 +15,6 @@ "p", "pb" ], - "local": "__flag__", "master_timeout": "", "h": [], "health": [ diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/cat.ml_data_frame_analytics.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.ml_data_frame_analytics.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/cat.ml_data_frame_analytics.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.ml_data_frame_analytics.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/cat.ml_datafeeds.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.ml_datafeeds.json similarity index 93% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/cat.ml_datafeeds.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.ml_datafeeds.json index 04f4e45782e1f..e58735a037153 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/cat.ml_datafeeds.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.ml_datafeeds.json @@ -1,6 +1,7 @@ { "cat.ml_datafeeds": { "url_params": { + "allow_no_match": "__flag__", "allow_no_datafeeds": "__flag__", "format": "", "h": [], diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/cat.ml_jobs.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.ml_jobs.json similarity index 95% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/cat.ml_jobs.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.ml_jobs.json index 2f7e03e564b5d..a7a068fa3691e 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/cat.ml_jobs.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.ml_jobs.json @@ -1,6 +1,7 @@ { "cat.ml_jobs": { "url_params": { + "allow_no_match": "__flag__", "allow_no_jobs": "__flag__", "bytes": [ "b", diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/cat.ml_trained_models.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.ml_trained_models.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/cat.ml_trained_models.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/cat.ml_trained_models.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.nodes.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.nodes.json index 2e89a66ef1f35..b695bb1df9625 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.nodes.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.nodes.json @@ -16,7 +16,6 @@ ], "format": "", "full_id": "__flag__", - "local": "__flag__", "master_timeout": "", "h": [], "help": "__flag__", @@ -30,7 +29,8 @@ "micros", "nanos" ], - "v": "__flag__" + "v": "__flag__", + "include_unloaded_segments": "__flag__" }, "methods": [ "GET" diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.plugins.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.plugins.json index 958992be81216..752313bf6f975 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.plugins.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.plugins.json @@ -6,6 +6,7 @@ "master_timeout": "", "h": [], "help": "__flag__", + "include_bootstrap": "__flag__", "s": [], "v": "__flag__" }, diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.shards.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.shards.json index 2b66562e3d6df..64a235eae67d5 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.shards.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.shards.json @@ -15,7 +15,6 @@ "p", "pb" ], - "local": "__flag__", "master_timeout": "", "h": [], "help": "__flag__", diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.tasks.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.tasks.json index 31c987a5893cf..f981d189a17b2 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.tasks.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.tasks.json @@ -2,10 +2,10 @@ "cat.tasks": { "url_params": { "format": "", - "node_id": [], + "nodes": [], "actions": [], "detailed": "__flag__", - "parent_task": "", + "parent_task_id": "", "h": [], "help": "__flag__", "s": [], diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.thread_pool.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.thread_pool.json index 5017a0390a2e4..d237f8d54ab2a 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.thread_pool.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.thread_pool.json @@ -2,13 +2,14 @@ "cat.thread_pool": { "url_params": { "format": "", - "size": [ - "", - "k", + "time": [ + "d", + "h", "m", - "g", - "t", - "p" + "s", + "ms", + "micros", + "nanos" ], "local": "__flag__", "master_timeout": "", diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cat.transforms.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.transforms.json new file mode 100644 index 0000000000000..048d7af411635 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cat.transforms.json @@ -0,0 +1,31 @@ +{ + "cat.transforms": { + "url_params": { + "from": 0, + "size": 0, + "allow_no_match": "__flag__", + "format": "", + "h": [], + "help": "__flag__", + "s": [], + "time": [ + "d", + "h", + "m", + "s", + "ms", + "micros", + "nanos" + ], + "v": "__flag__" + }, + "methods": [ + "GET" + ], + "patterns": [ + "_cat/transforms", + "_cat/transforms/{transform_id}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/cat-transforms.html" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ccr.delete_auto_follow_pattern.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ccr.delete_auto_follow_pattern.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ccr.delete_auto_follow_pattern.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ccr.delete_auto_follow_pattern.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ccr.follow.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ccr.follow.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ccr.follow.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ccr.follow.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ccr.follow_info.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ccr.follow_info.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ccr.follow_info.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ccr.follow_info.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ccr.follow_stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ccr.follow_stats.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ccr.follow_stats.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ccr.follow_stats.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ccr.forget_follower.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ccr.forget_follower.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ccr.forget_follower.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ccr.forget_follower.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ccr.get_auto_follow_pattern.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ccr.get_auto_follow_pattern.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ccr.get_auto_follow_pattern.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ccr.get_auto_follow_pattern.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ccr.pause_auto_follow_pattern.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ccr.pause_auto_follow_pattern.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ccr.pause_auto_follow_pattern.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ccr.pause_auto_follow_pattern.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ccr.pause_follow.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ccr.pause_follow.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ccr.pause_follow.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ccr.pause_follow.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ccr.put_auto_follow_pattern.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ccr.put_auto_follow_pattern.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ccr.put_auto_follow_pattern.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ccr.put_auto_follow_pattern.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ccr.resume_auto_follow_pattern.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ccr.resume_auto_follow_pattern.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ccr.resume_auto_follow_pattern.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ccr.resume_auto_follow_pattern.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ccr.resume_follow.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ccr.resume_follow.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ccr.resume_follow.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ccr.resume_follow.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ccr.stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ccr.stats.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ccr.stats.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ccr.stats.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ccr.unfollow.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ccr.unfollow.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ccr.unfollow.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ccr.unfollow.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/clear_scroll.json b/src/plugins/console/server/lib/spec_definitions/json/generated/clear_scroll.json index 7e6e6692f931b..5273574357ddd 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/clear_scroll.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/clear_scroll.json @@ -6,6 +6,6 @@ "patterns": [ "_search/scroll" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/search-request-body.html#_clear_scroll_api" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/clear-scroll-api.html" } } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/close_point_in_time.json b/src/plugins/console/server/lib/spec_definitions/json/generated/close_point_in_time.json new file mode 100644 index 0000000000000..e1997bc2e20ea --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/close_point_in_time.json @@ -0,0 +1,11 @@ +{ + "close_point_in_time": { + "methods": [ + "DELETE" + ], + "patterns": [ + "_pit" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/point-in-time-api.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.delete_component_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.delete_component_template.json index e935b8999e6d3..24255f7231892 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.delete_component_template.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.delete_component_template.json @@ -10,6 +10,6 @@ "patterns": [ "_component_template/{name}" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-component-templates.html" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-component-template.html" } } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.delete_voting_config_exclusions.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.delete_voting_config_exclusions.json new file mode 100644 index 0000000000000..6b4b48113f2a8 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.delete_voting_config_exclusions.json @@ -0,0 +1,14 @@ +{ + "cluster.delete_voting_config_exclusions": { + "url_params": { + "wait_for_removal": "__flag__" + }, + "methods": [ + "DELETE" + ], + "patterns": [ + "_cluster/voting_config_exclusions" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/voting-config-exclusions.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.exists_component_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.exists_component_template.json new file mode 100644 index 0000000000000..24dcbeb006e6f --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.exists_component_template.json @@ -0,0 +1,15 @@ +{ + "cluster.exists_component_template": { + "url_params": { + "master_timeout": "", + "local": "__flag__" + }, + "methods": [ + "HEAD" + ], + "patterns": [ + "_component_template/{name}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-component-template.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_component_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_component_template.json index 784053d69ad5a..cbfed6741f8a4 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_component_template.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_component_template.json @@ -12,5 +12,6 @@ "_component_template", "_component_template/{name}" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/getting-component-templates.html" } + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/getting-component-templates.html" + } } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_settings.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_settings.json index 249f582c33682..6c97254b5201c 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_settings.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_settings.json @@ -12,6 +12,6 @@ "patterns": [ "_cluster/settings" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/cluster-update-settings.html" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/cluster-get-settings.html" } } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.post_voting_config_exclusions.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.post_voting_config_exclusions.json new file mode 100644 index 0000000000000..74cc74a8c05a4 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.post_voting_config_exclusions.json @@ -0,0 +1,16 @@ +{ + "cluster.post_voting_config_exclusions": { + "url_params": { + "node_ids": "", + "node_names": "", + "timeout": "" + }, + "methods": [ + "POST" + ], + "patterns": [ + "_cluster/voting_config_exclusions" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/voting-config-exclusions.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.put_component_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.put_component_template.json index 8a0bfc278cbf3..999ff0c149fe8 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.put_component_template.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.put_component_template.json @@ -5,10 +5,12 @@ "master_timeout": "" }, "methods": [ - "PUT" + "PUT", + "POST" ], "patterns": [ "_component_template/{name}" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-component-template.html" } + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-component-template.html" + } } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/dangling_indices.delete_dangling_index.json b/src/plugins/console/server/lib/spec_definitions/json/generated/dangling_indices.delete_dangling_index.json new file mode 100644 index 0000000000000..03d7c914baab5 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/dangling_indices.delete_dangling_index.json @@ -0,0 +1,16 @@ +{ + "dangling_indices.delete_dangling_index": { + "url_params": { + "accept_data_loss": "__flag__", + "timeout": "", + "master_timeout": "" + }, + "methods": [ + "DELETE" + ], + "patterns": [ + "_dangling/{index_uuid}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/modules-gateway-dangling-indices.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/dangling_indices.import_dangling_index.json b/src/plugins/console/server/lib/spec_definitions/json/generated/dangling_indices.import_dangling_index.json new file mode 100644 index 0000000000000..c55cdd2901699 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/dangling_indices.import_dangling_index.json @@ -0,0 +1,16 @@ +{ + "dangling_indices.import_dangling_index": { + "url_params": { + "accept_data_loss": "__flag__", + "timeout": "", + "master_timeout": "" + }, + "methods": [ + "POST" + ], + "patterns": [ + "_dangling/{index_uuid}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/modules-gateway-dangling-indices.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/dangling_indices.list_dangling_indices.json b/src/plugins/console/server/lib/spec_definitions/json/generated/dangling_indices.list_dangling_indices.json new file mode 100644 index 0000000000000..63dd560947876 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/dangling_indices.list_dangling_indices.json @@ -0,0 +1,11 @@ +{ + "dangling_indices.list_dangling_indices": { + "methods": [ + "GET" + ], + "patterns": [ + "_dangling" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/modules-gateway-dangling-indices.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/data_frame_transform_deprecated.delete_transform.json b/src/plugins/console/server/lib/spec_definitions/json/generated/data_frame_transform_deprecated.delete_transform.json new file mode 100644 index 0000000000000..6dfd81ded7bc5 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/data_frame_transform_deprecated.delete_transform.json @@ -0,0 +1,10 @@ +{ + "data_frame_transform_deprecated.delete_transform": { + "url_params": { + "force": "__flag__" + }, + "methods": [], + "patterns": [], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/delete-transform.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/data_frame_transform_deprecated.get_transform.json b/src/plugins/console/server/lib/spec_definitions/json/generated/data_frame_transform_deprecated.get_transform.json new file mode 100644 index 0000000000000..be3716ba3da6e --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/data_frame_transform_deprecated.get_transform.json @@ -0,0 +1,13 @@ +{ + "data_frame_transform_deprecated.get_transform": { + "url_params": { + "from": 0, + "size": 0, + "allow_no_match": "__flag__", + "exclude_generated": "__flag__" + }, + "methods": [], + "patterns": [], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/get-transform.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/data_frame_transform_deprecated.get_transform_stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/data_frame_transform_deprecated.get_transform_stats.json new file mode 100644 index 0000000000000..440eed6a1f2a4 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/data_frame_transform_deprecated.get_transform_stats.json @@ -0,0 +1,12 @@ +{ + "data_frame_transform_deprecated.get_transform_stats": { + "url_params": { + "from": "", + "size": "", + "allow_no_match": "__flag__" + }, + "methods": [], + "patterns": [], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/get-transform-stats.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/data_frame_transform_deprecated.preview_transform.json b/src/plugins/console/server/lib/spec_definitions/json/generated/data_frame_transform_deprecated.preview_transform.json new file mode 100644 index 0000000000000..f095847ed0d9a --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/data_frame_transform_deprecated.preview_transform.json @@ -0,0 +1,7 @@ +{ + "data_frame_transform_deprecated.preview_transform": { + "methods": [], + "patterns": [], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/preview-transform.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/data_frame_transform_deprecated.put_transform.json b/src/plugins/console/server/lib/spec_definitions/json/generated/data_frame_transform_deprecated.put_transform.json new file mode 100644 index 0000000000000..e7555ce4bad29 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/data_frame_transform_deprecated.put_transform.json @@ -0,0 +1,10 @@ +{ + "data_frame_transform_deprecated.put_transform": { + "url_params": { + "defer_validation": "__flag__" + }, + "methods": [], + "patterns": [], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/put-transform.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/data_frame_transform_deprecated.start_transform.json b/src/plugins/console/server/lib/spec_definitions/json/generated/data_frame_transform_deprecated.start_transform.json new file mode 100644 index 0000000000000..d50346ff328c6 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/data_frame_transform_deprecated.start_transform.json @@ -0,0 +1,10 @@ +{ + "data_frame_transform_deprecated.start_transform": { + "url_params": { + "timeout": "" + }, + "methods": [], + "patterns": [], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/start-transform.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/data_frame_transform_deprecated.stop_transform.json b/src/plugins/console/server/lib/spec_definitions/json/generated/data_frame_transform_deprecated.stop_transform.json new file mode 100644 index 0000000000000..2618446916c76 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/data_frame_transform_deprecated.stop_transform.json @@ -0,0 +1,12 @@ +{ + "data_frame_transform_deprecated.stop_transform": { + "url_params": { + "wait_for_completion": "__flag__", + "timeout": "", + "allow_no_match": "__flag__" + }, + "methods": [], + "patterns": [], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/stop-transform.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/data_frame_transform_deprecated.update_transform.json b/src/plugins/console/server/lib/spec_definitions/json/generated/data_frame_transform_deprecated.update_transform.json new file mode 100644 index 0000000000000..60f71474e865a --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/data_frame_transform_deprecated.update_transform.json @@ -0,0 +1,10 @@ +{ + "data_frame_transform_deprecated.update_transform": { + "url_params": { + "defer_validation": "__flag__" + }, + "methods": [], + "patterns": [], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/update-transform.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/delete.json b/src/plugins/console/server/lib/spec_definitions/json/generated/delete.json index 0852d8d184831..a71a659618eb1 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/delete.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/delete.json @@ -15,8 +15,7 @@ "version_type": [ "internal", "external", - "external_gte", - "force" + "external_gte" ] }, "methods": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/delete_by_query.json b/src/plugins/console/server/lib/spec_definitions/json/generated/delete_by_query.json index e01ea8b2dec6d..4e2b3a180fbba 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/delete_by_query.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/delete_by_query.json @@ -32,7 +32,6 @@ "dfs_query_then_fetch" ], "search_timeout": "", - "size": "", "max_docs": "all documents", "sort": [], "_source": [], diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/enrich.delete_policy.json b/src/plugins/console/server/lib/spec_definitions/json/generated/enrich.delete_policy.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/enrich.delete_policy.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/enrich.delete_policy.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/enrich.execute_policy.json b/src/plugins/console/server/lib/spec_definitions/json/generated/enrich.execute_policy.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/enrich.execute_policy.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/enrich.execute_policy.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/enrich.get_policy.json b/src/plugins/console/server/lib/spec_definitions/json/generated/enrich.get_policy.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/enrich.get_policy.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/enrich.get_policy.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/enrich.put_policy.json b/src/plugins/console/server/lib/spec_definitions/json/generated/enrich.put_policy.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/enrich.put_policy.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/enrich.put_policy.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/enrich.stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/enrich.stats.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/enrich.stats.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/enrich.stats.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/eql.delete.json b/src/plugins/console/server/lib/spec_definitions/json/generated/eql.delete.json new file mode 100644 index 0000000000000..4bc4846f628a0 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/eql.delete.json @@ -0,0 +1,11 @@ +{ + "eql.delete": { + "methods": [ + "DELETE" + ], + "patterns": [ + "_eql/search/{id}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/eql-search-api.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/eql.get.json b/src/plugins/console/server/lib/spec_definitions/json/generated/eql.get.json new file mode 100644 index 0000000000000..4aa740c333beb --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/eql.get.json @@ -0,0 +1,15 @@ +{ + "eql.get": { + "url_params": { + "wait_for_completion_timeout": "", + "keep_alive": "" + }, + "methods": [ + "GET" + ], + "patterns": [ + "_eql/search/{id}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/eql-search-api.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/eql.get_status.json b/src/plugins/console/server/lib/spec_definitions/json/generated/eql.get_status.json new file mode 100644 index 0000000000000..68fe8aff70046 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/eql.get_status.json @@ -0,0 +1,11 @@ +{ + "eql.get_status": { + "methods": [ + "GET" + ], + "patterns": [ + "_eql/search/status/{id}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/get-async-eql-status-api.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/eql.search.json b/src/plugins/console/server/lib/spec_definitions/json/generated/eql.search.json new file mode 100644 index 0000000000000..67e7a1729c295 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/eql.search.json @@ -0,0 +1,17 @@ +{ + "eql.search": { + "url_params": { + "wait_for_completion_timeout": "", + "keep_on_completion": "__flag__", + "keep_alive": "" + }, + "methods": [ + "GET", + "POST" + ], + "patterns": [ + "{indices}/_eql/search" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/eql-search-api.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/exists.json b/src/plugins/console/server/lib/spec_definitions/json/generated/exists.json index a6799b9e361cd..7d66dad050e5d 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/exists.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/exists.json @@ -13,8 +13,7 @@ "version_type": [ "internal", "external", - "external_gte", - "force" + "external_gte" ] }, "methods": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/exists_source.json b/src/plugins/console/server/lib/spec_definitions/json/generated/exists_source.json index 9ffc4b55f3037..6811940cb0f1e 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/exists_source.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/exists_source.json @@ -12,8 +12,7 @@ "version_type": [ "internal", "external", - "external_gte", - "force" + "external_gte" ] }, "methods": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/features.get_features.json b/src/plugins/console/server/lib/spec_definitions/json/generated/features.get_features.json new file mode 100644 index 0000000000000..abbf74b880f1a --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/features.get_features.json @@ -0,0 +1,14 @@ +{ + "features.get_features": { + "url_params": { + "master_timeout": "" + }, + "methods": [ + "GET" + ], + "patterns": [ + "_features" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/get-features-api.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/features.reset_features.json b/src/plugins/console/server/lib/spec_definitions/json/generated/features.reset_features.json new file mode 100644 index 0000000000000..98280afdab1a0 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/features.reset_features.json @@ -0,0 +1,11 @@ +{ + "features.reset_features": { + "methods": [ + "POST" + ], + "patterns": [ + "_features/_reset" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/reset-features-api.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/fleet.global_checkpoints.json b/src/plugins/console/server/lib/spec_definitions/json/generated/fleet.global_checkpoints.json new file mode 100644 index 0000000000000..29f37c9099de2 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/fleet.global_checkpoints.json @@ -0,0 +1,17 @@ +{ + "fleet.global_checkpoints": { + "url_params": { + "wait_for_advance": "__flag__", + "wait_for_index": "__flag__", + "checkpoints": [], + "timeout": "" + }, + "methods": [ + "GET" + ], + "patterns": [ + "{indices}/_fleet/global_checkpoints" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/get-global-checkpoints.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/get.json b/src/plugins/console/server/lib/spec_definitions/json/generated/get.json index 5a72761c7c32e..396396f7550ca 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/get.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/get.json @@ -13,8 +13,7 @@ "version_type": [ "internal", "external", - "external_gte", - "force" + "external_gte" ] }, "methods": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/get_script_context.json b/src/plugins/console/server/lib/spec_definitions/json/generated/get_script_context.json index 528c261df7707..f3808d568e989 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/get_script_context.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/get_script_context.json @@ -5,6 +5,7 @@ ], "patterns": [ "_script_context" - ] + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/painless/master/painless-contexts.html" } } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/get_script_languages.json b/src/plugins/console/server/lib/spec_definitions/json/generated/get_script_languages.json index 10ea433ca68c5..a872885615d05 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/get_script_languages.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/get_script_languages.json @@ -5,6 +5,7 @@ ], "patterns": [ "_script_language" - ] + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/modules-scripting.html" } } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/get_source.json b/src/plugins/console/server/lib/spec_definitions/json/generated/get_source.json index 8201960363a78..de1b5b0f3de21 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/get_source.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/get_source.json @@ -12,8 +12,7 @@ "version_type": [ "internal", "external", - "external_gte", - "force" + "external_gte" ] }, "methods": [ diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/graph.explore.json b/src/plugins/console/server/lib/spec_definitions/json/generated/graph.explore.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/graph.explore.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/graph.explore.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ilm.delete_lifecycle.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ilm.delete_lifecycle.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ilm.delete_lifecycle.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ilm.delete_lifecycle.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ilm.explain_lifecycle.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ilm.explain_lifecycle.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ilm.explain_lifecycle.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ilm.explain_lifecycle.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ilm.get_lifecycle.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ilm.get_lifecycle.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ilm.get_lifecycle.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ilm.get_lifecycle.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ilm.get_status.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ilm.get_status.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ilm.get_status.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ilm.get_status.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/ilm.migrate_to_data_tiers.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ilm.migrate_to_data_tiers.json new file mode 100644 index 0000000000000..acafe1183a893 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ilm.migrate_to_data_tiers.json @@ -0,0 +1,14 @@ +{ + "ilm.migrate_to_data_tiers": { + "url_params": { + "dry_run": "__flag__" + }, + "methods": [ + "POST" + ], + "patterns": [ + "_ilm/migrate_to_data_tiers" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-migrate-to-data-tiers.html" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ilm.move_to_step.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ilm.move_to_step.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ilm.move_to_step.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ilm.move_to_step.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ilm.put_lifecycle.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ilm.put_lifecycle.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ilm.put_lifecycle.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ilm.put_lifecycle.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ilm.remove_policy.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ilm.remove_policy.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ilm.remove_policy.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ilm.remove_policy.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ilm.retry.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ilm.retry.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ilm.retry.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ilm.retry.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ilm.set_policy.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ilm.set_policy.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ilm.set_policy.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ilm.set_policy.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ilm.start.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ilm.start.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ilm.start.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ilm.start.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ilm.stop.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ilm.stop.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ilm.stop.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ilm.stop.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/index.json b/src/plugins/console/server/lib/spec_definitions/json/generated/index.json index 25977806776a7..c6b290a58812b 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/index.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/index.json @@ -21,7 +21,8 @@ ], "if_seq_no": "", "if_primary_term": "", - "pipeline": "" + "pipeline": "", + "require_alias": "__flag__" }, "methods": [ "PUT", diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.add_block.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.add_block.json new file mode 100644 index 0000000000000..ff2623b100bda --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.add_block.json @@ -0,0 +1,24 @@ +{ + "indices.add_block": { + "url_params": { + "timeout": "", + "master_timeout": "", + "ignore_unavailable": "__flag__", + "allow_no_indices": "__flag__", + "expand_wildcards": [ + "open", + "closed", + "hidden", + "none", + "all" + ] + }, + "methods": [ + "PUT" + ], + "patterns": [ + "{indices}/_block/{block}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/index-modules-blocks.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.create.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.create.json index 1970f88b30958..8227e38d3c6d9 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.create.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.create.json @@ -1,7 +1,6 @@ { "indices.create": { "url_params": { - "include_type_name": "__flag__", "wait_for_active_shards": "", "timeout": "", "master_timeout": "" diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.create_data_stream.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.create_data_stream.json new file mode 100644 index 0000000000000..832d5b8de1f47 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.create_data_stream.json @@ -0,0 +1,11 @@ +{ + "indices.create_data_stream": { + "methods": [ + "PUT" + ], + "patterns": [ + "_data_stream/{name}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/data-streams.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.data_streams_stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.data_streams_stats.json new file mode 100644 index 0000000000000..6a88c351ff700 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.data_streams_stats.json @@ -0,0 +1,12 @@ +{ + "indices.data_streams_stats": { + "methods": [ + "GET" + ], + "patterns": [ + "_data_stream/_stats", + "_data_stream/{name}/_stats" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/data-streams.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_data_stream.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_data_stream.json new file mode 100644 index 0000000000000..9b91e3deb3a08 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_data_stream.json @@ -0,0 +1,20 @@ +{ + "indices.delete_data_stream": { + "url_params": { + "expand_wildcards": [ + "open", + "closed", + "hidden", + "none", + "all" + ] + }, + "methods": [ + "DELETE" + ], + "patterns": [ + "_data_stream/{name}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/data-streams.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_index_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_index_template.json new file mode 100644 index 0000000000000..ef3f836207f17 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_index_template.json @@ -0,0 +1,15 @@ +{ + "indices.delete_index_template": { + "url_params": { + "timeout": "", + "master_timeout": "" + }, + "methods": [ + "DELETE" + ], + "patterns": [ + "_index_template/{name}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-templates.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.disk_usage.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.disk_usage.json new file mode 100644 index 0000000000000..fe7d378b2b168 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.disk_usage.json @@ -0,0 +1,24 @@ +{ + "indices.disk_usage": { + "url_params": { + "run_expensive_tasks": "__flag__", + "flush": "__flag__", + "ignore_unavailable": "__flag__", + "allow_no_indices": "__flag__", + "expand_wildcards": [ + "open", + "closed", + "hidden", + "none", + "all" + ] + }, + "methods": [ + "POST" + ], + "patterns": [ + "{indices}/_disk_usage" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-disk-usage.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_index_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_index_template.json new file mode 100644 index 0000000000000..97fa8cf55576f --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.exists_index_template.json @@ -0,0 +1,16 @@ +{ + "indices.exists_index_template": { + "url_params": { + "flat_settings": "__flag__", + "master_timeout": "", + "local": "__flag__" + }, + "methods": [ + "HEAD" + ], + "patterns": [ + "_index_template/{name}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-templates.html" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/indices.freeze.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.freeze.json similarity index 75% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/indices.freeze.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.freeze.json index 06e8c606f59f7..77c765b90bcdc 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/indices.freeze.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.freeze.json @@ -8,17 +8,14 @@ "expand_wildcards": [ "open", "closed", + "hidden", "none", "all" ], "wait_for_active_shards": "" }, - "methods": [ - "POST" - ], - "patterns": [ - "{indices}/_freeze" - ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/frozen.html" + "methods": [], + "patterns": [], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/freeze-index-api.html" } } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get.json index 6df796ed6c4cf..99a654d023ba2 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get.json @@ -1,7 +1,6 @@ { "indices.get": { "url_params": { - "include_type_name": "__flag__", "local": "__flag__", "ignore_unavailable": "__flag__", "allow_no_indices": "__flag__", diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_data_stream.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_data_stream.json new file mode 100644 index 0000000000000..45199a60f337d --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_data_stream.json @@ -0,0 +1,21 @@ +{ + "indices.get_data_stream": { + "url_params": { + "expand_wildcards": [ + "open", + "closed", + "hidden", + "none", + "all" + ] + }, + "methods": [ + "GET" + ], + "patterns": [ + "_data_stream", + "_data_stream/{name}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/data-streams.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_field_mapping.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_field_mapping.json index c95e2efc73fab..c50919af0a2f3 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_field_mapping.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_field_mapping.json @@ -1,7 +1,6 @@ { "indices.get_field_mapping": { "url_params": { - "include_type_name": "__flag__", "include_defaults": "__flag__", "ignore_unavailable": "__flag__", "allow_no_indices": "__flag__", diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_index_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_index_template.json new file mode 100644 index 0000000000000..142b75f22c2a6 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_index_template.json @@ -0,0 +1,17 @@ +{ + "indices.get_index_template": { + "url_params": { + "flat_settings": "__flag__", + "master_timeout": "", + "local": "__flag__" + }, + "methods": [ + "GET" + ], + "patterns": [ + "_index_template", + "_index_template/{name}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-templates.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_mapping.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_mapping.json index 555137d0e2ee0..f484f0dca0669 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_mapping.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_mapping.json @@ -1,7 +1,6 @@ { "indices.get_mapping": { "url_params": { - "include_type_name": "__flag__", "ignore_unavailable": "__flag__", "allow_no_indices": "__flag__", "expand_wildcards": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_template.json index d5f52ec76b374..f5902929c25cc 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_template.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_template.json @@ -1,7 +1,6 @@ { "indices.get_template": { "url_params": { - "include_type_name": "__flag__", "flat_settings": "__flag__", "master_timeout": "", "local": "__flag__" diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.migrate_to_data_stream.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.migrate_to_data_stream.json new file mode 100644 index 0000000000000..45cd033801e55 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.migrate_to_data_stream.json @@ -0,0 +1,11 @@ +{ + "indices.migrate_to_data_stream": { + "methods": [ + "POST" + ], + "patterns": [ + "_data_stream/_migrate/{name}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/data-streams.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.promote_data_stream.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.promote_data_stream.json new file mode 100644 index 0000000000000..a92b9a7a4a40d --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.promote_data_stream.json @@ -0,0 +1,11 @@ +{ + "indices.promote_data_stream": { + "methods": [ + "POST" + ], + "patterns": [ + "_data_stream/_promote/{name}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/data-streams.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_index_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_index_template.json new file mode 100644 index 0000000000000..0ce27c1d9d21e --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_index_template.json @@ -0,0 +1,17 @@ +{ + "indices.put_index_template": { + "url_params": { + "create": "__flag__", + "cause": "", + "master_timeout": "" + }, + "methods": [ + "PUT", + "POST" + ], + "patterns": [ + "_index_template/{name}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-templates.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_mapping.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_mapping.json index e36783c815e3f..5b8ce2b8b4350 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_mapping.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_mapping.json @@ -1,7 +1,6 @@ { "indices.put_mapping": { "url_params": { - "include_type_name": "__flag__", "timeout": "", "master_timeout": "", "ignore_unavailable": "__flag__", @@ -12,7 +11,8 @@ "hidden", "none", "all" - ] + ], + "write_index_only": "__flag__" }, "methods": [ "PUT", diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_template.json index 8fb31277da44e..537428f3c53c9 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_template.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.put_template.json @@ -1,11 +1,9 @@ { "indices.put_template": { "url_params": { - "include_type_name": "__flag__", "order": "", "create": "__flag__", - "master_timeout": "", - "flat_settings": "__flag__" + "master_timeout": "" }, "methods": [ "PUT", diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/indices.reload_search_analyzers.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.reload_search_analyzers.json similarity index 96% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/indices.reload_search_analyzers.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.reload_search_analyzers.json index 13ae3a0fd4d18..d4d1d8c52b349 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/indices.reload_search_analyzers.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.reload_search_analyzers.json @@ -6,6 +6,7 @@ "expand_wildcards": [ "open", "closed", + "hidden", "none", "all" ] diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.resolve_index.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.resolve_index.json new file mode 100644 index 0000000000000..574d07f1e4324 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.resolve_index.json @@ -0,0 +1,20 @@ +{ + "indices.resolve_index": { + "url_params": { + "expand_wildcards": [ + "open", + "closed", + "hidden", + "none", + "all" + ] + }, + "methods": [ + "GET" + ], + "patterns": [ + "_resolve/index/{name}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-resolve-index-api.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.rollover.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.rollover.json index 7fa76a687eb77..19e0f1f909ab8 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.rollover.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.rollover.json @@ -1,7 +1,6 @@ { "indices.rollover": { "url_params": { - "include_type_name": "__flag__", "timeout": "", "dry_run": "__flag__", "master_timeout": "", diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.shrink.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.shrink.json index 6fbdea0f1244b..31acc86a2fa56 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.shrink.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.shrink.json @@ -1,7 +1,6 @@ { "indices.shrink": { "url_params": { - "copy_settings": "__flag__", "timeout": "", "master_timeout": "", "wait_for_active_shards": "" diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.simulate_index_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.simulate_index_template.json new file mode 100644 index 0000000000000..e122bd844e9bc --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.simulate_index_template.json @@ -0,0 +1,16 @@ +{ + "indices.simulate_index_template": { + "url_params": { + "create": "__flag__", + "cause": "", + "master_timeout": "" + }, + "methods": [ + "POST" + ], + "patterns": [ + "_index_template/_simulate_index/{name}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-templates.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.simulate_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.simulate_template.json new file mode 100644 index 0000000000000..9e174799e6c07 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.simulate_template.json @@ -0,0 +1,17 @@ +{ + "indices.simulate_template": { + "url_params": { + "create": "__flag__", + "cause": "", + "master_timeout": "" + }, + "methods": [ + "POST" + ], + "patterns": [ + "_index_template/_simulate", + "_index_template/_simulate/{name}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-templates.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.split.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.split.json index 68f2e338cd201..1bfbaa078b796 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.split.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.split.json @@ -1,7 +1,6 @@ { "indices.split": { "url_params": { - "copy_settings": "__flag__", "timeout": "", "master_timeout": "", "wait_for_active_shards": "" diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.stats.json index 1fa32265c91ee..9cdcbdf672d8c 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.stats.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.stats.json @@ -34,6 +34,7 @@ "url_components": { "metrics": [ "_all", + "bulk", "completion", "docs", "fielddata", @@ -47,7 +48,6 @@ "search", "segments", "store", - "suggest", "warmer" ], "indices": null diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/indices.unfreeze.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.unfreeze.json similarity index 75% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/indices.unfreeze.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/indices.unfreeze.json index 186a671347240..3e87971cb66f5 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/indices.unfreeze.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.unfreeze.json @@ -8,17 +8,14 @@ "expand_wildcards": [ "open", "closed", + "hidden", "none", "all" ], "wait_for_active_shards": "" }, - "methods": [ - "POST" - ], - "patterns": [ - "{indices}/_unfreeze" - ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/frozen.html" + "methods": [], + "patterns": [], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/unfreeze-index-api.html" } } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.geo_ip_stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.geo_ip_stats.json new file mode 100644 index 0000000000000..2993eca9f0af5 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.geo_ip_stats.json @@ -0,0 +1,11 @@ +{ + "ingest.geo_ip_stats": { + "methods": [ + "GET" + ], + "patterns": [ + "_ingest/geoip/stats" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/geoip-stats-api.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.get_pipeline.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.get_pipeline.json index cffcbb1261f90..df90b976444c1 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.get_pipeline.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ingest.get_pipeline.json @@ -1,6 +1,7 @@ { "ingest.get_pipeline": { "url_params": { + "summary": "__flag__", "master_timeout": "" }, "methods": [ diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/license.delete.json b/src/plugins/console/server/lib/spec_definitions/json/generated/license.delete.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/license.delete.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/license.delete.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/license.get.json b/src/plugins/console/server/lib/spec_definitions/json/generated/license.get.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/license.get.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/license.get.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/license.get_basic_status.json b/src/plugins/console/server/lib/spec_definitions/json/generated/license.get_basic_status.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/license.get_basic_status.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/license.get_basic_status.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/license.get_trial_status.json b/src/plugins/console/server/lib/spec_definitions/json/generated/license.get_trial_status.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/license.get_trial_status.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/license.get_trial_status.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/license.post.json b/src/plugins/console/server/lib/spec_definitions/json/generated/license.post.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/license.post.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/license.post.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/license.post_start_basic.json b/src/plugins/console/server/lib/spec_definitions/json/generated/license.post_start_basic.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/license.post_start_basic.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/license.post_start_basic.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/license.post_start_trial.json b/src/plugins/console/server/lib/spec_definitions/json/generated/license.post_start_trial.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/license.post_start_trial.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/license.post_start_trial.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/logstash.delete_pipeline.json b/src/plugins/console/server/lib/spec_definitions/json/generated/logstash.delete_pipeline.json new file mode 100644 index 0000000000000..9e2c32e287253 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/logstash.delete_pipeline.json @@ -0,0 +1,11 @@ +{ + "logstash.delete_pipeline": { + "methods": [ + "DELETE" + ], + "patterns": [ + "_logstash/pipeline/{id}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/logstash-api-delete-pipeline.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/logstash.get_pipeline.json b/src/plugins/console/server/lib/spec_definitions/json/generated/logstash.get_pipeline.json new file mode 100644 index 0000000000000..a6b3a69b53a60 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/logstash.get_pipeline.json @@ -0,0 +1,11 @@ +{ + "logstash.get_pipeline": { + "methods": [ + "GET" + ], + "patterns": [ + "_logstash/pipeline/{id}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/logstash-api-get-pipeline.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/logstash.put_pipeline.json b/src/plugins/console/server/lib/spec_definitions/json/generated/logstash.put_pipeline.json new file mode 100644 index 0000000000000..9640ced182898 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/logstash.put_pipeline.json @@ -0,0 +1,11 @@ +{ + "logstash.put_pipeline": { + "methods": [ + "PUT" + ], + "patterns": [ + "_logstash/pipeline/{id}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/logstash-api-put-pipeline.html" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/migration.deprecations.json b/src/plugins/console/server/lib/spec_definitions/json/generated/migration.deprecations.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/migration.deprecations.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/migration.deprecations.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/migration.get_assistance.json b/src/plugins/console/server/lib/spec_definitions/json/generated/migration.get_assistance.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/migration.get_assistance.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/migration.get_assistance.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/migration.upgrade.json b/src/plugins/console/server/lib/spec_definitions/json/generated/migration.upgrade.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/migration.upgrade.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/migration.upgrade.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.close_job.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.close_job.json similarity index 90% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.close_job.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.close_job.json index b0f2c6489b30e..4fd0b7ccf98a7 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.close_job.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.close_job.json @@ -1,6 +1,7 @@ { "ml.close_job": { "url_params": { + "allow_no_match": "__flag__", "allow_no_jobs": "__flag__", "force": "__flag__", "timeout": "" diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_calendar.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_calendar.json similarity index 51% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_calendar.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_calendar.json index 97e5898072a08..7ccf7ef673070 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_calendar.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_calendar.json @@ -5,6 +5,7 @@ ], "patterns": [ "_ml/calendars/{calendar_id}" - ] + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-delete-calendar.html" } } diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_calendar_event.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_calendar_event.json similarity index 54% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_calendar_event.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_calendar_event.json index 85ea5add0b60b..7ee8ef376a4db 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_calendar_event.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_calendar_event.json @@ -5,6 +5,7 @@ ], "patterns": [ "_ml/calendars/{calendar_id}/events/{event_id}" - ] + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-delete-calendar-event.html" } } diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_calendar_job.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_calendar_job.json similarity index 54% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_calendar_job.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_calendar_job.json index 6665f52eeb90d..b97667900fe35 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_calendar_job.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_calendar_job.json @@ -5,6 +5,7 @@ ], "patterns": [ "_ml/calendars/{calendar_id}/jobs/{job_id}" - ] + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-delete-calendar-job.html" } } diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_data_frame_analytics.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_data_frame_analytics.json similarity index 85% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_data_frame_analytics.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_data_frame_analytics.json index 2e4593f339212..a46bfdf8318db 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_data_frame_analytics.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_data_frame_analytics.json @@ -1,7 +1,8 @@ { "ml.delete_data_frame_analytics": { "url_params": { - "force": "__flag__" + "force": "__flag__", + "timeout": "" }, "methods": [ "DELETE" diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_datafeed.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_datafeed.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_datafeed.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_datafeed.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_expired_data.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_expired_data.json new file mode 100644 index 0000000000000..1515821cb1628 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_expired_data.json @@ -0,0 +1,16 @@ +{ + "ml.delete_expired_data": { + "url_params": { + "requests_per_second": "", + "timeout": "" + }, + "methods": [ + "DELETE" + ], + "patterns": [ + "_ml/_delete_expired_data/{job_id}", + "_ml/_delete_expired_data" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-delete-expired-data.html" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_filter.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_filter.json similarity index 51% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_filter.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_filter.json index 8210a2acd71d0..515986d44d77c 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_filter.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_filter.json @@ -5,6 +5,7 @@ ], "patterns": [ "_ml/filters/{filter_id}" - ] + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-delete-filter.html" } } diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_forecast.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_forecast.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_forecast.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_forecast.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_job.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_job.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_job.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_job.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_model_snapshot.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_model_snapshot.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_model_snapshot.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_model_snapshot.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_trained_model.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_trained_model.json similarity index 65% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_trained_model.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_trained_model.json index 343fa904c4216..75592c5964ed0 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_trained_model.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_trained_model.json @@ -4,8 +4,8 @@ "DELETE" ], "patterns": [ - "_ml/inference/{model_id}" + "_ml/trained_models/{model_id}" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/delete-inference.html" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/delete-trained-models.html" } } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_trained_model_alias.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_trained_model_alias.json new file mode 100644 index 0000000000000..cd27f21b1d6e4 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.delete_trained_model_alias.json @@ -0,0 +1,11 @@ +{ + "ml.delete_trained_model_alias": { + "methods": [ + "DELETE" + ], + "patterns": [ + "_ml/trained_models/{model_id}/model_aliases/{model_alias}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/delete-trained-models-aliases.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/ml.estimate_model_memory.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.estimate_model_memory.json new file mode 100644 index 0000000000000..94b149e542d43 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.estimate_model_memory.json @@ -0,0 +1,11 @@ +{ + "ml.estimate_model_memory": { + "methods": [ + "POST" + ], + "patterns": [ + "_ml/anomaly_detectors/_estimate_model_memory" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-apis.html" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.evaluate_data_frame.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.evaluate_data_frame.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.evaluate_data_frame.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.evaluate_data_frame.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.explain_data_frame_analytics.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.explain_data_frame_analytics.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.explain_data_frame_analytics.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.explain_data_frame_analytics.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.find_file_structure.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.find_file_structure.json similarity index 88% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.find_file_structure.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.find_file_structure.json index 6e7163ae2b740..9ede28306e313 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.find_file_structure.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.find_file_structure.json @@ -25,8 +25,8 @@ "POST" ], "patterns": [ - "_ml/find_file_structure" + "_text_structure/find_structure" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-find-file-structure.html" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/find-structure.html" } } diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.flush_job.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.flush_job.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.flush_job.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.flush_job.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.forecast.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.forecast.json similarity index 52% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.forecast.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.forecast.json index 3a8849aad3e4d..d2b53c11cb985 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.forecast.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.forecast.json @@ -2,13 +2,15 @@ "ml.forecast": { "url_params": { "duration": "", - "expires_in": "" + "expires_in": "", + "max_model_memory": "" }, "methods": [ "POST" ], "patterns": [ "_ml/anomaly_detectors/{job_id}/_forecast" - ] + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-forecast.html" } } diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_buckets.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_buckets.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_buckets.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_buckets.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_calendar_events.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_calendar_events.json similarity index 67% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_calendar_events.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_calendar_events.json index 8999af6320dfa..f8886067dd80e 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_calendar_events.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_calendar_events.json @@ -12,6 +12,7 @@ ], "patterns": [ "_ml/calendars/{calendar_id}/events" - ] + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-calendar-event.html" } } diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_calendars.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_calendars.json similarity index 65% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_calendars.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_calendars.json index 87f582b5c364d..575f2573d1ee9 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_calendars.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_calendars.json @@ -11,6 +11,7 @@ "patterns": [ "_ml/calendars", "_ml/calendars/{calendar_id}" - ] + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-calendar.html" } } diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_categories.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_categories.json similarity index 88% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_categories.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_categories.json index 64edb196bb366..1083760d7594f 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_categories.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_categories.json @@ -2,7 +2,8 @@ "ml.get_categories": { "url_params": { "from": 0, - "size": 0 + "size": 0, + "partition_field_value": "" }, "methods": [ "GET", diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_data_frame_analytics.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_data_frame_analytics.json similarity index 86% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_data_frame_analytics.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_data_frame_analytics.json index ecccec9c7e059..cccaa516110aa 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_data_frame_analytics.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_data_frame_analytics.json @@ -3,7 +3,8 @@ "url_params": { "allow_no_match": "__flag__", "from": 0, - "size": 0 + "size": 0, + "exclude_generated": "__flag__" }, "methods": [ "GET" diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_data_frame_analytics_stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_data_frame_analytics_stats.json similarity index 89% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_data_frame_analytics_stats.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_data_frame_analytics_stats.json index 3ae103f79f798..20fbb2981a28c 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_data_frame_analytics_stats.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_data_frame_analytics_stats.json @@ -3,7 +3,8 @@ "url_params": { "allow_no_match": "__flag__", "from": 0, - "size": 0 + "size": 0, + "verbose": "__flag__" }, "methods": [ "GET" diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_datafeed_stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_datafeed_stats.json similarity index 90% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_datafeed_stats.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_datafeed_stats.json index 2971b8a7f6c63..75ece20e87443 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_datafeed_stats.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_datafeed_stats.json @@ -1,6 +1,7 @@ { "ml.get_datafeed_stats": { "url_params": { + "allow_no_match": "__flag__", "allow_no_datafeeds": "__flag__" }, "methods": [ diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_datafeeds.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_datafeeds.json similarity index 71% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_datafeeds.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_datafeeds.json index deeb81d692739..ec557966a7271 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_datafeeds.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_datafeeds.json @@ -1,7 +1,9 @@ { "ml.get_datafeeds": { "url_params": { - "allow_no_datafeeds": "__flag__" + "allow_no_match": "__flag__", + "allow_no_datafeeds": "__flag__", + "exclude_generated": "__flag__" }, "methods": [ "GET" diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_filters.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_filters.json similarity index 63% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_filters.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_filters.json index 9b06e618a0b0c..df5747e143412 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_filters.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_filters.json @@ -10,6 +10,7 @@ "patterns": [ "_ml/filters", "_ml/filters/{filter_id}" - ] + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-filter.html" } } diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_influencers.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_influencers.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_influencers.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_influencers.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_job_stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_job_stats.json similarity index 90% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_job_stats.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_job_stats.json index 6173b3ebdc6d0..48dd9e52cd990 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_job_stats.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_job_stats.json @@ -1,6 +1,7 @@ { "ml.get_job_stats": { "url_params": { + "allow_no_match": "__flag__", "allow_no_jobs": "__flag__" }, "methods": [ diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_jobs.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_jobs.json similarity index 72% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_jobs.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_jobs.json index 2486684424670..b46c86559ea09 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_jobs.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_jobs.json @@ -1,7 +1,9 @@ { "ml.get_jobs": { "url_params": { - "allow_no_jobs": "__flag__" + "allow_no_match": "__flag__", + "allow_no_jobs": "__flag__", + "exclude_generated": "__flag__" }, "methods": [ "GET" diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_model_snapshots.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_model_snapshots.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_model_snapshots.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_model_snapshots.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_overall_buckets.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_overall_buckets.json similarity index 93% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_overall_buckets.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_overall_buckets.json index 3a88c9d8ab9c9..e374f6c5ef63f 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_overall_buckets.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_overall_buckets.json @@ -7,6 +7,7 @@ "exclude_interim": "__flag__", "start": "", "end": "", + "allow_no_match": "__flag__", "allow_no_jobs": "__flag__" }, "methods": [ diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_records.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_records.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_records.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_records.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_trained_models.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_trained_models.json similarity index 64% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_trained_models.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_trained_models.json index 76598ee015c6d..19f169fa98e86 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_trained_models.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_trained_models.json @@ -2,19 +2,21 @@ "ml.get_trained_models": { "url_params": { "allow_no_match": "__flag__", + "include": "", "include_model_definition": "__flag__", "decompress_definition": "__flag__", "from": 0, "size": 0, - "tags": [] + "tags": [], + "exclude_generated": "__flag__" }, "methods": [ "GET" ], "patterns": [ - "_ml/inference/{model_id}", - "_ml/inference" + "_ml/trained_models/{model_id}", + "_ml/trained_models" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/get-inference.html" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/get-trained-models.html" } } diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_trained_models_stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_trained_models_stats.json similarity index 66% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_trained_models_stats.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_trained_models_stats.json index ab05e203b3980..44d06c78aef70 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.get_trained_models_stats.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.get_trained_models_stats.json @@ -9,9 +9,9 @@ "GET" ], "patterns": [ - "_ml/inference/{model_id}/_stats", - "_ml/inference/_stats" + "_ml/trained_models/{model_id}/_stats", + "_ml/trained_models/_stats" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/get-inference-stats.html" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/get-trained-models-stats.html" } } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/ml.infer_trained_model_deployment.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.infer_trained_model_deployment.json new file mode 100644 index 0000000000000..644942801d2f6 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.infer_trained_model_deployment.json @@ -0,0 +1,14 @@ +{ + "ml.infer_trained_model_deployment": { + "url_params": { + "timeout": "" + }, + "methods": [ + "POST" + ], + "patterns": [ + "_ml/trained_models/{model_id}/deployment/_infer" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-infer-trained-model-deployment.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/ml.info.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.info.json new file mode 100644 index 0000000000000..e09491dacf1fd --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.info.json @@ -0,0 +1,11 @@ +{ + "ml.info": { + "methods": [ + "GET" + ], + "patterns": [ + "_ml/info" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/get-ml-info.html" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.open_job.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.open_job.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.open_job.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.open_job.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.post_calendar_events.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.post_calendar_events.json similarity index 52% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.post_calendar_events.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.post_calendar_events.json index 89ce2df63315b..9d0947c4704a1 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.post_calendar_events.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.post_calendar_events.json @@ -5,6 +5,7 @@ ], "patterns": [ "_ml/calendars/{calendar_id}/events" - ] + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-post-calendar-event.html" } } diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.post_data.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.post_data.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.post_data.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.post_data.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/ml.preview_data_frame_analytics.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.preview_data_frame_analytics.json new file mode 100644 index 0000000000000..bc3e0a4fd54b5 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.preview_data_frame_analytics.json @@ -0,0 +1,13 @@ +{ + "ml.preview_data_frame_analytics": { + "methods": [ + "GET", + "POST" + ], + "patterns": [ + "_ml/data_frame/analytics/_preview", + "_ml/data_frame/analytics/{id}/_preview" + ], + "documentation": "http://www.elastic.co/guide/en/elasticsearch/reference/current/preview-dfanalytics.html" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.preview_datafeed.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.preview_datafeed.json similarity index 65% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.preview_datafeed.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.preview_datafeed.json index 6eb537804134b..29dbabd67d60b 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.preview_datafeed.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.preview_datafeed.json @@ -1,10 +1,12 @@ { "ml.preview_datafeed": { "methods": [ - "GET" + "GET", + "POST" ], "patterns": [ - "_ml/datafeeds/{datafeed_id}/_preview" + "_ml/datafeeds/{datafeed_id}/_preview", + "_ml/datafeeds/_preview" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-preview-datafeed.html" } diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.put_calendar.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.put_calendar.json similarity index 51% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.put_calendar.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.put_calendar.json index 7452d1b4b9707..c60a9b60f4ca0 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.put_calendar.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.put_calendar.json @@ -5,6 +5,7 @@ ], "patterns": [ "_ml/calendars/{calendar_id}" - ] + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-put-calendar.html" } } diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.put_calendar_job.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.put_calendar_job.json similarity index 53% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.put_calendar_job.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.put_calendar_job.json index 08dc3d77b5f25..5a3ac8e0b3243 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.put_calendar_job.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.put_calendar_job.json @@ -5,6 +5,7 @@ ], "patterns": [ "_ml/calendars/{calendar_id}/jobs/{job_id}" - ] + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-put-calendar-job.html" } } diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.put_data_frame_analytics.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.put_data_frame_analytics.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.put_data_frame_analytics.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.put_data_frame_analytics.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.put_datafeed.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.put_datafeed.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.put_datafeed.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.put_datafeed.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.put_job.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.put_filter.json similarity index 58% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.put_job.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.put_filter.json index 7a48994bd1a6c..ad9caa9417da9 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.put_job.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.put_filter.json @@ -1,11 +1,11 @@ { - "ml.put_job": { + "ml.put_filter": { "methods": [ "PUT" ], "patterns": [ - "_ml/anomaly_detectors/{job_id}" + "_ml/filters/{filter_id}" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-put-job.html" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-put-filter.html" } } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/ml.put_job.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.put_job.json new file mode 100644 index 0000000000000..a55ee4ab21a63 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.put_job.json @@ -0,0 +1,23 @@ +{ + "ml.put_job": { + "url_params": { + "ignore_unavailable": "__flag__", + "allow_no_indices": "__flag__", + "ignore_throttled": "__flag__", + "expand_wildcards": [ + "open", + "closed", + "hidden", + "none", + "all" + ] + }, + "methods": [ + "PUT" + ], + "patterns": [ + "_ml/anomaly_detectors/{job_id}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-put-job.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/ml.put_trained_model.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.put_trained_model.json new file mode 100644 index 0000000000000..9b95f6891d474 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.put_trained_model.json @@ -0,0 +1,11 @@ +{ + "ml.put_trained_model": { + "methods": [ + "PUT" + ], + "patterns": [ + "_ml/trained_models/{model_id}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/put-trained-models.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/ml.put_trained_model_alias.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.put_trained_model_alias.json new file mode 100644 index 0000000000000..a83d54220b31a --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.put_trained_model_alias.json @@ -0,0 +1,14 @@ +{ + "ml.put_trained_model_alias": { + "url_params": { + "reassign": "__flag__" + }, + "methods": [ + "PUT" + ], + "patterns": [ + "_ml/trained_models/{model_id}/model_aliases/{model_alias}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/put-trained-models-aliases.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/ml.reset_job.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.reset_job.json new file mode 100644 index 0000000000000..79441afbdebf7 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.reset_job.json @@ -0,0 +1,14 @@ +{ + "ml.reset_job": { + "url_params": { + "wait_for_completion": "__flag__" + }, + "methods": [ + "POST" + ], + "patterns": [ + "_ml/anomaly_detectors/{job_id}/_reset" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-reset-job.html" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.revert_model_snapshot.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.revert_model_snapshot.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.revert_model_snapshot.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.revert_model_snapshot.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.set_upgrade_mode.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.set_upgrade_mode.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.set_upgrade_mode.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.set_upgrade_mode.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.start_data_frame_analytics.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.start_data_frame_analytics.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.start_data_frame_analytics.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.start_data_frame_analytics.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.start_datafeed.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.start_datafeed.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.start_datafeed.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.start_datafeed.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/ml.start_trained_model_deployment.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.start_trained_model_deployment.json new file mode 100644 index 0000000000000..49e246c90d8e1 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.start_trained_model_deployment.json @@ -0,0 +1,14 @@ +{ + "ml.start_trained_model_deployment": { + "url_params": { + "timeout": "" + }, + "methods": [ + "POST" + ], + "patterns": [ + "_ml/trained_models/{model_id}/deployment/_start" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-start-trained-model-deployment.html" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.stop_data_frame_analytics.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.stop_data_frame_analytics.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.stop_data_frame_analytics.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.stop_data_frame_analytics.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.stop_datafeed.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.stop_datafeed.json similarity index 90% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.stop_datafeed.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.stop_datafeed.json index d6769ed58148f..b52f1e22bddf8 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.stop_datafeed.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.stop_datafeed.json @@ -1,6 +1,7 @@ { "ml.stop_datafeed": { "url_params": { + "allow_no_match": "__flag__", "allow_no_datafeeds": "__flag__", "force": "__flag__", "timeout": "" diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/ml.stop_trained_model_deployment.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.stop_trained_model_deployment.json new file mode 100644 index 0000000000000..94b34f597fc3b --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.stop_trained_model_deployment.json @@ -0,0 +1,11 @@ +{ + "ml.stop_trained_model_deployment": { + "methods": [ + "POST" + ], + "patterns": [ + "_ml/trained_models/{model_id}/deployment/_stop" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/stop-trained-model-deployment.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/ml.update_data_frame_analytics.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.update_data_frame_analytics.json new file mode 100644 index 0000000000000..e3134770742cb --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.update_data_frame_analytics.json @@ -0,0 +1,11 @@ +{ + "ml.update_data_frame_analytics": { + "methods": [ + "POST" + ], + "patterns": [ + "_ml/data_frame/analytics/{id}/_update" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/update-dfanalytics.html" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.update_datafeed.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.update_datafeed.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.update_datafeed.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.update_datafeed.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.update_filter.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.update_filter.json similarity index 52% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.update_filter.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.update_filter.json index 3f48013c2be1c..16b6aedb404d3 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.update_filter.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.update_filter.json @@ -5,6 +5,7 @@ ], "patterns": [ "_ml/filters/{filter_id}/_update" - ] + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-update-filter.html" } } diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.update_job.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.update_job.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.update_job.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.update_job.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.update_model_snapshot.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.update_model_snapshot.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.update_model_snapshot.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.update_model_snapshot.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.upgrade.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.upgrade.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.upgrade.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.upgrade.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/ml.upgrade_job_snapshot.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.upgrade_job_snapshot.json new file mode 100644 index 0000000000000..6818eab2bb084 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.upgrade_job_snapshot.json @@ -0,0 +1,15 @@ +{ + "ml.upgrade_job_snapshot": { + "url_params": { + "timeout": "", + "wait_for_completion": "__flag__" + }, + "methods": [ + "POST" + ], + "patterns": [ + "_ml/anomaly_detectors/{job_id}/model_snapshots/{snapshot_id}/_upgrade" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-upgrade-job-model-snapshot.html" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.validate.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.validate.json similarity index 54% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.validate.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.validate.json index 298c7cf6dce26..da07f1a2fa203 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.validate.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.validate.json @@ -5,6 +5,7 @@ ], "patterns": [ "_ml/anomaly_detectors/_validate" - ] + ], + "documentation": "https://www.elastic.co/guide/en/machine-learning/current/ml-jobs.html" } } diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.validate_detector.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.validate_detector.json similarity index 58% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.validate_detector.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ml.validate_detector.json index 5e6d7daeea772..73527bd28c5d8 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.validate_detector.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/ml.validate_detector.json @@ -5,6 +5,7 @@ ], "patterns": [ "_ml/anomaly_detectors/_validate/detector" - ] + ], + "documentation": "https://www.elastic.co/guide/en/machine-learning/current/ml-jobs.html" } } diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/monitoring.bulk.json b/src/plugins/console/server/lib/spec_definitions/json/generated/monitoring.bulk.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/monitoring.bulk.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/monitoring.bulk.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/msearch.json b/src/plugins/console/server/lib/spec_definitions/json/generated/msearch.json index ecb71e9ba23c0..d460bf2b1add5 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/msearch.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/msearch.json @@ -3,9 +3,7 @@ "url_params": { "search_type": [ "query_then_fetch", - "query_and_fetch", - "dfs_query_then_fetch", - "dfs_query_and_fetch" + "dfs_query_then_fetch" ], "max_concurrent_searches": "", "typed_keys": "__flag__", diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/msearch_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/msearch_template.json index 0b0ca087b1819..cc7cfbd6e82b6 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/msearch_template.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/msearch_template.json @@ -3,9 +3,7 @@ "url_params": { "search_type": [ "query_then_fetch", - "query_and_fetch", - "dfs_query_then_fetch", - "dfs_query_and_fetch" + "dfs_query_then_fetch" ], "typed_keys": "__flag__", "max_concurrent_searches": "", diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/mtermvectors.json b/src/plugins/console/server/lib/spec_definitions/json/generated/mtermvectors.json index 72a134eca4d2e..20c3daca849c7 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/mtermvectors.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/mtermvectors.json @@ -15,8 +15,7 @@ "version_type": [ "internal", "external", - "external_gte", - "force" + "external_gte" ] }, "methods": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/nodes.stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/nodes.stats.json index e194a6d42c031..cfbcf69b39b7a 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/nodes.stats.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/nodes.stats.json @@ -12,7 +12,8 @@ ], "types": [], "timeout": "", - "include_segment_file_sizes": "__flag__" + "include_segment_file_sizes": "__flag__", + "include_unloaded_segments": "__flag__" }, "methods": [ "GET" @@ -33,6 +34,7 @@ "discovery", "fs", "http", + "indexing_pressure", "indices", "jvm", "os", @@ -42,6 +44,7 @@ ], "index_metric": [ "_all", + "bulk", "completion", "docs", "fielddata", @@ -55,7 +58,6 @@ "search", "segments", "store", - "suggest", "warmer" ] }, diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/open_point_in_time.json b/src/plugins/console/server/lib/spec_definitions/json/generated/open_point_in_time.json new file mode 100644 index 0000000000000..9ce78acbc1a2a --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/open_point_in_time.json @@ -0,0 +1,25 @@ +{ + "open_point_in_time": { + "url_params": { + "preference": "random", + "routing": "", + "ignore_unavailable": "__flag__", + "expand_wildcards": [ + "open", + "closed", + "hidden", + "none", + "all" + ], + "keep_alive": "" + }, + "methods": [ + "POST" + ], + "patterns": [ + "_pit", + "{indices}/_pit" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/point-in-time-api.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/render_search_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/render_search_template.json index e206140f9d16a..84377591e270b 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/render_search_template.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/render_search_template.json @@ -8,6 +8,6 @@ "_render/template", "_render/template/{id}" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/search-template.html#_validating_templates" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/render-search-template-api.html" } } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/rollup.delete_job.json b/src/plugins/console/server/lib/spec_definitions/json/generated/rollup.delete_job.json new file mode 100644 index 0000000000000..4f7b37006c3b6 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/rollup.delete_job.json @@ -0,0 +1,11 @@ +{ + "rollup.delete_job": { + "methods": [ + "DELETE" + ], + "patterns": [ + "_rollup/job/{id}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/rollup-delete-job.html" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/rollup.get_jobs.json b/src/plugins/console/server/lib/spec_definitions/json/generated/rollup.get_jobs.json similarity index 53% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/rollup.get_jobs.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/rollup.get_jobs.json index 49524b394167c..70ce98063d57c 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/rollup.get_jobs.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/rollup.get_jobs.json @@ -6,6 +6,7 @@ "patterns": [ "_rollup/job/{id}", "_rollup/job/" - ] + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/rollup-get-job.html" } } diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/rollup.get_rollup_caps.json b/src/plugins/console/server/lib/spec_definitions/json/generated/rollup.get_rollup_caps.json similarity index 53% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/rollup.get_rollup_caps.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/rollup.get_rollup_caps.json index 8a0ef67a0ad4e..9d39f5a4e2ba8 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/rollup.get_rollup_caps.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/rollup.get_rollup_caps.json @@ -6,6 +6,7 @@ "patterns": [ "_rollup/data/{id}", "_rollup/data/" - ] + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/rollup-get-rollup-caps.html" } } diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/rollup.get_rollup_index_caps.json b/src/plugins/console/server/lib/spec_definitions/json/generated/rollup.get_rollup_index_caps.json similarity index 50% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/rollup.get_rollup_index_caps.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/rollup.get_rollup_index_caps.json index 325115553c294..f7ae6d14f675c 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/rollup.get_rollup_index_caps.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/rollup.get_rollup_index_caps.json @@ -5,6 +5,7 @@ ], "patterns": [ "{indices}/_rollup/data" - ] + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/rollup-get-rollup-index-caps.html" } } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/rollup.put_job.json b/src/plugins/console/server/lib/spec_definitions/json/generated/rollup.put_job.json new file mode 100644 index 0000000000000..7970008f8c8f4 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/rollup.put_job.json @@ -0,0 +1,11 @@ +{ + "rollup.put_job": { + "methods": [ + "PUT" + ], + "patterns": [ + "_rollup/job/{id}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/rollup-put-job.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/rollup.rollup.json b/src/plugins/console/server/lib/spec_definitions/json/generated/rollup.rollup.json new file mode 100644 index 0000000000000..c3cc629abac80 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/rollup.rollup.json @@ -0,0 +1,11 @@ +{ + "rollup.rollup": { + "methods": [ + "POST" + ], + "patterns": [ + "{indices}/_rollup/{rollup_index}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/xpack-rollup.html" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/rollup.rollup_search.json b/src/plugins/console/server/lib/spec_definitions/json/generated/rollup.rollup_search.json similarity index 68% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/rollup.rollup_search.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/rollup.rollup_search.json index a1771126a71b4..431e5e532a323 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/rollup.rollup_search.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/rollup.rollup_search.json @@ -10,6 +10,7 @@ ], "patterns": [ "{indices}/_rollup_search" - ] + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/rollup-search.html" } } diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/rollup.start_job.json b/src/plugins/console/server/lib/spec_definitions/json/generated/rollup.start_job.json similarity index 50% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/rollup.start_job.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/rollup.start_job.json index 9714275072b07..39ced395166c7 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/rollup.start_job.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/rollup.start_job.json @@ -5,6 +5,7 @@ ], "patterns": [ "_rollup/job/{id}/_start" - ] + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/rollup-start-job.html" } } diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/rollup.stop_job.json b/src/plugins/console/server/lib/spec_definitions/json/generated/rollup.stop_job.json similarity index 64% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/rollup.stop_job.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/rollup.stop_job.json index 70931030b4402..a1290cd388a09 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/rollup.stop_job.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/rollup.stop_job.json @@ -9,6 +9,7 @@ ], "patterns": [ "_rollup/job/{id}/_stop" - ] + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/rollup-stop-job.html" } } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/search.json b/src/plugins/console/server/lib/spec_definitions/json/generated/search.json index 78b969d3ed8f2..f5e3a0041c7a6 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/search.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/search.json @@ -49,7 +49,7 @@ "suggest_text": "", "timeout": "", "track_scores": "__flag__", - "track_total_hits": "__flag__", + "track_total_hits": "", "allow_partial_search_results": "__flag__", "typed_keys": "__flag__", "version": "__flag__", @@ -58,7 +58,8 @@ "batched_reduce_size": "", "max_concurrent_shard_requests": "", "pre_filter_shard_size": "", - "rest_total_hits_as_int": "__flag__" + "rest_total_hits_as_int": "__flag__", + "min_compatible_shard_node": "" }, "methods": [ "GET", diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/search_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/search_template.json index 748326522e5c2..6d7b3aa34c107 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/search_template.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/search_template.json @@ -16,9 +16,7 @@ "scroll": "", "search_type": [ "query_then_fetch", - "query_and_fetch", - "dfs_query_then_fetch", - "dfs_query_and_fetch" + "dfs_query_then_fetch" ], "explain": "__flag__", "profile": "__flag__", diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/searchable_snapshots.cache_stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/searchable_snapshots.cache_stats.json new file mode 100644 index 0000000000000..5a53319c164f8 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/searchable_snapshots.cache_stats.json @@ -0,0 +1,15 @@ +{ + "searchable_snapshots.cache_stats": { + "url_params": { + "master_timeout": "" + }, + "methods": [ + "GET" + ], + "patterns": [ + "_searchable_snapshots/cache/stats", + "_searchable_snapshots/{nodes}/cache/stats" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/searchable-snapshots-api-cache-stats.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/searchable_snapshots.clear_cache.json b/src/plugins/console/server/lib/spec_definitions/json/generated/searchable_snapshots.clear_cache.json new file mode 100644 index 0000000000000..428991f758cdb --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/searchable_snapshots.clear_cache.json @@ -0,0 +1,23 @@ +{ + "searchable_snapshots.clear_cache": { + "url_params": { + "ignore_unavailable": "__flag__", + "allow_no_indices": "__flag__", + "expand_wildcards": [ + "open", + "closed", + "none", + "all" + ], + "index": [] + }, + "methods": [ + "POST" + ], + "patterns": [ + "_searchable_snapshots/cache/clear", + "{indices}/_searchable_snapshots/cache/clear" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/searchable-snapshots-apis.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/searchable_snapshots.mount.json b/src/plugins/console/server/lib/spec_definitions/json/generated/searchable_snapshots.mount.json new file mode 100644 index 0000000000000..6556c466120da --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/searchable_snapshots.mount.json @@ -0,0 +1,16 @@ +{ + "searchable_snapshots.mount": { + "url_params": { + "master_timeout": "", + "wait_for_completion": "__flag__", + "storage": "" + }, + "methods": [ + "POST" + ], + "patterns": [ + "_snapshot/{repository}/{snapshot}/_mount" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/searchable-snapshots-api-mount-snapshot.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/searchable_snapshots.stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/searchable_snapshots.stats.json new file mode 100644 index 0000000000000..ccf7522634a0e --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/searchable_snapshots.stats.json @@ -0,0 +1,19 @@ +{ + "searchable_snapshots.stats": { + "url_params": { + "level": [ + "cluster", + "indices", + "shards" + ] + }, + "methods": [ + "GET" + ], + "patterns": [ + "_searchable_snapshots/stats", + "{indices}/_searchable_snapshots/stats" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/searchable-snapshots-api-stats.html" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.authenticate.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.authenticate.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.authenticate.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/security.authenticate.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.change_password.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.change_password.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.change_password.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/security.change_password.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/security.clear_api_key_cache.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.clear_api_key_cache.json new file mode 100644 index 0000000000000..0046dab4074e6 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/security.clear_api_key_cache.json @@ -0,0 +1,11 @@ +{ + "security.clear_api_key_cache": { + "methods": [ + "POST" + ], + "patterns": [ + "_security/api_key/{ids}/_clear_cache" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-clear-api-key-cache.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/security.clear_cached_privileges.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.clear_cached_privileges.json new file mode 100644 index 0000000000000..aaab692cefc35 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/security.clear_cached_privileges.json @@ -0,0 +1,11 @@ +{ + "security.clear_cached_privileges": { + "methods": [ + "POST" + ], + "patterns": [ + "_security/privilege/{application}/_clear_cache" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-clear-privilege-cache.html" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.clear_cached_realms.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.clear_cached_realms.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.clear_cached_realms.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/security.clear_cached_realms.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.clear_cached_roles.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.clear_cached_roles.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.clear_cached_roles.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/security.clear_cached_roles.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/security.clear_cached_service_tokens.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.clear_cached_service_tokens.json new file mode 100644 index 0000000000000..2fc747518fbc3 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/security.clear_cached_service_tokens.json @@ -0,0 +1,11 @@ +{ + "security.clear_cached_service_tokens": { + "methods": [ + "POST" + ], + "patterns": [ + "_security/service/{namespace}/{service}/credential/token/{name}/_clear_cache" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-clear-service-token-caches.html" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.create_api_key.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.create_api_key.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.create_api_key.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/security.create_api_key.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/security.create_service_token.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.create_service_token.json new file mode 100644 index 0000000000000..71c6ccedddc74 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/security.create_service_token.json @@ -0,0 +1,20 @@ +{ + "security.create_service_token": { + "url_params": { + "refresh": [ + "true", + "false", + "wait_for" + ] + }, + "methods": [ + "PUT", + "POST" + ], + "patterns": [ + "_security/service/{namespace}/{service}/credential/token/{name}", + "_security/service/{namespace}/{service}/credential/token" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-service-token.html" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.delete_privileges.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.delete_privileges.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.delete_privileges.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/security.delete_privileges.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.delete_role.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.delete_role.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.delete_role.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/security.delete_role.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.delete_role_mapping.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.delete_role_mapping.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.delete_role_mapping.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/security.delete_role_mapping.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/security.delete_service_token.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.delete_service_token.json new file mode 100644 index 0000000000000..bfdfeb79cb27e --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/security.delete_service_token.json @@ -0,0 +1,18 @@ +{ + "security.delete_service_token": { + "url_params": { + "refresh": [ + "true", + "false", + "wait_for" + ] + }, + "methods": [ + "DELETE" + ], + "patterns": [ + "_security/service/{namespace}/{service}/credential/token/{name}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-delete-service-token.html" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.delete_user.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.delete_user.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.delete_user.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/security.delete_user.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.disable_user.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.disable_user.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.disable_user.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/security.disable_user.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.enable_user.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.enable_user.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.enable_user.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/security.enable_user.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/security.enroll_kibana.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.enroll_kibana.json new file mode 100644 index 0000000000000..630adedd03ad7 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/security.enroll_kibana.json @@ -0,0 +1,11 @@ +{ + "security.enroll_kibana": { + "methods": [ + "GET" + ], + "patterns": [ + "_security/enroll/kibana" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/security-api-enroll-kibana.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/security.enroll_node.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.enroll_node.json new file mode 100644 index 0000000000000..d9cb7da17ed6e --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/security.enroll_node.json @@ -0,0 +1,11 @@ +{ + "security.enroll_node": { + "methods": [ + "GET" + ], + "patterns": [ + "_security/enroll/node" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/security-api-node-enrollment.html" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.get_api_key.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.get_api_key.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.get_api_key.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/security.get_api_key.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.get_builtin_privileges.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.get_builtin_privileges.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.get_builtin_privileges.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/security.get_builtin_privileges.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.get_privileges.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.get_privileges.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.get_privileges.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/security.get_privileges.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.get_role.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.get_role.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.get_role.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/security.get_role.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.get_role_mapping.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.get_role_mapping.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.get_role_mapping.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/security.get_role_mapping.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/security.get_service_accounts.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.get_service_accounts.json new file mode 100644 index 0000000000000..41afc1de8df5c --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/security.get_service_accounts.json @@ -0,0 +1,13 @@ +{ + "security.get_service_accounts": { + "methods": [ + "GET" + ], + "patterns": [ + "_security/service/{namespace}/{service}", + "_security/service/{namespace}", + "_security/service" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-service-accounts.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/security.get_service_credentials.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.get_service_credentials.json new file mode 100644 index 0000000000000..b98029aaae367 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/security.get_service_credentials.json @@ -0,0 +1,11 @@ +{ + "security.get_service_credentials": { + "methods": [ + "GET" + ], + "patterns": [ + "_security/service/{namespace}/{service}/credential" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-service-credentials.html" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.get_token.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.get_token.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.get_token.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/security.get_token.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.get_user.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.get_user.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.get_user.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/security.get_user.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.get_user_privileges.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.get_user_privileges.json similarity index 76% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.get_user_privileges.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/security.get_user_privileges.json index 63bf8d6969344..1b939d2d9a29e 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.get_user_privileges.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/security.get_user_privileges.json @@ -6,6 +6,6 @@ "patterns": [ "_security/user/_privileges" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-privileges.html" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-user-privileges.html" } } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/security.grant_api_key.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.grant_api_key.json new file mode 100644 index 0000000000000..4d1a56e4fb0bb --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/security.grant_api_key.json @@ -0,0 +1,18 @@ +{ + "security.grant_api_key": { + "url_params": { + "refresh": [ + "true", + "false", + "wait_for" + ] + }, + "methods": [ + "POST" + ], + "patterns": [ + "_security/api_key/grant" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-grant-api-key.html" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.has_privileges.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.has_privileges.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.has_privileges.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/security.has_privileges.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.invalidate_api_key.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.invalidate_api_key.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.invalidate_api_key.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/security.invalidate_api_key.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.invalidate_token.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.invalidate_token.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.invalidate_token.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/security.invalidate_token.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.put_privileges.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.put_privileges.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.put_privileges.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/security.put_privileges.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.put_role.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.put_role.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.put_role.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/security.put_role.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.put_role_mapping.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.put_role_mapping.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.put_role_mapping.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/security.put_role_mapping.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.put_user.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.put_user.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/security.put_user.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/security.put_user.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/security.saml_authenticate.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.saml_authenticate.json new file mode 100644 index 0000000000000..5ed30f2a4ef83 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/security.saml_authenticate.json @@ -0,0 +1,11 @@ +{ + "security.saml_authenticate": { + "methods": [ + "POST" + ], + "patterns": [ + "_security/saml/authenticate" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-saml-authenticate.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/security.saml_complete_logout.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.saml_complete_logout.json new file mode 100644 index 0000000000000..7ecc4bdbc8784 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/security.saml_complete_logout.json @@ -0,0 +1,11 @@ +{ + "security.saml_complete_logout": { + "methods": [ + "POST" + ], + "patterns": [ + "_security/saml/complete_logout" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-saml-complete-logout.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/security.saml_invalidate.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.saml_invalidate.json new file mode 100644 index 0000000000000..cea5236c474f4 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/security.saml_invalidate.json @@ -0,0 +1,11 @@ +{ + "security.saml_invalidate": { + "methods": [ + "POST" + ], + "patterns": [ + "_security/saml/invalidate" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-saml-invalidate.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/security.saml_logout.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.saml_logout.json new file mode 100644 index 0000000000000..4300c948199b3 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/security.saml_logout.json @@ -0,0 +1,11 @@ +{ + "security.saml_logout": { + "methods": [ + "POST" + ], + "patterns": [ + "_security/saml/logout" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-saml-logout.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/security.saml_prepare_authentication.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.saml_prepare_authentication.json new file mode 100644 index 0000000000000..fa4303ccfedae --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/security.saml_prepare_authentication.json @@ -0,0 +1,11 @@ +{ + "security.saml_prepare_authentication": { + "methods": [ + "POST" + ], + "patterns": [ + "_security/saml/prepare" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-saml-prepare-authentication.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/security.saml_service_provider_metadata.json b/src/plugins/console/server/lib/spec_definitions/json/generated/security.saml_service_provider_metadata.json new file mode 100644 index 0000000000000..a77953309c9ce --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/security.saml_service_provider_metadata.json @@ -0,0 +1,11 @@ +{ + "security.saml_service_provider_metadata": { + "methods": [ + "GET" + ], + "patterns": [ + "_security/saml/metadata/{realm_name}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-saml-sp-metadata.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/shutdown.delete_node.json b/src/plugins/console/server/lib/spec_definitions/json/generated/shutdown.delete_node.json new file mode 100644 index 0000000000000..18605b2afc549 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/shutdown.delete_node.json @@ -0,0 +1,11 @@ +{ + "shutdown.delete_node": { + "methods": [ + "DELETE" + ], + "patterns": [ + "_nodes/{nodes}/shutdown" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/shutdown.get_node.json b/src/plugins/console/server/lib/spec_definitions/json/generated/shutdown.get_node.json new file mode 100644 index 0000000000000..0b9c9139b57e3 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/shutdown.get_node.json @@ -0,0 +1,12 @@ +{ + "shutdown.get_node": { + "methods": [ + "GET" + ], + "patterns": [ + "_nodes/shutdown", + "_nodes/{nodes}/shutdown" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/shutdown.put_node.json b/src/plugins/console/server/lib/spec_definitions/json/generated/shutdown.put_node.json new file mode 100644 index 0000000000000..aee49831305f1 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/shutdown.put_node.json @@ -0,0 +1,11 @@ +{ + "shutdown.put_node": { + "methods": [ + "PUT" + ], + "patterns": [ + "_nodes/{nodes}/shutdown" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/slm.delete_lifecycle.json b/src/plugins/console/server/lib/spec_definitions/json/generated/slm.delete_lifecycle.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/slm.delete_lifecycle.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/slm.delete_lifecycle.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/slm.execute_lifecycle.json b/src/plugins/console/server/lib/spec_definitions/json/generated/slm.execute_lifecycle.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/slm.execute_lifecycle.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/slm.execute_lifecycle.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/slm.execute_retention.json b/src/plugins/console/server/lib/spec_definitions/json/generated/slm.execute_retention.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/slm.execute_retention.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/slm.execute_retention.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/slm.get_lifecycle.json b/src/plugins/console/server/lib/spec_definitions/json/generated/slm.get_lifecycle.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/slm.get_lifecycle.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/slm.get_lifecycle.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/slm.get_stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/slm.get_stats.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/slm.get_stats.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/slm.get_stats.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/slm.get_status.json b/src/plugins/console/server/lib/spec_definitions/json/generated/slm.get_status.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/slm.get_status.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/slm.get_status.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/slm.put_lifecycle.json b/src/plugins/console/server/lib/spec_definitions/json/generated/slm.put_lifecycle.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/slm.put_lifecycle.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/slm.put_lifecycle.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/slm.start.json b/src/plugins/console/server/lib/spec_definitions/json/generated/slm.start.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/slm.start.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/slm.start.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/slm.stop.json b/src/plugins/console/server/lib/spec_definitions/json/generated/slm.stop.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/slm.stop.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/slm.stop.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.cleanup_repository.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.cleanup_repository.json index eed3e597dc070..cd29d6c300dc6 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.cleanup_repository.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.cleanup_repository.json @@ -10,6 +10,6 @@ "patterns": [ "_snapshot/{repository}/_cleanup" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/modules-snapshots.html" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/clean-up-snapshot-repo-api.html" } } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.clone.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.clone.json new file mode 100644 index 0000000000000..ef8eedf78ac91 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.clone.json @@ -0,0 +1,14 @@ +{ + "snapshot.clone": { + "url_params": { + "master_timeout": "" + }, + "methods": [ + "PUT" + ], + "patterns": [ + "_snapshot/{repository}/{snapshot}/_clone/{target_snapshot}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/modules-snapshots.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.get.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.get.json index b37141728f6b7..8edb85f416b48 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.get.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.get.json @@ -3,7 +3,12 @@ "url_params": { "master_timeout": "", "ignore_unavailable": "__flag__", - "verbose": "__flag__" + "index_details": "__flag__", + "verbose": "__flag__", + "sort": ["start_time", "duration", "name", "index_count"], + "size": "", + "order": "", + "after": "" }, "methods": [ "GET" diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.repository_analyze.json b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.repository_analyze.json new file mode 100644 index 0000000000000..cf591a66ad2fd --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/snapshot.repository_analyze.json @@ -0,0 +1,24 @@ +{ + "snapshot.repository_analyze": { + "url_params": { + "blob_count": "", + "concurrency": "", + "read_node_count": "", + "early_read_node_count": "", + "seed": "", + "rare_action_probability": "", + "max_blob_size": "", + "max_total_data_size": "", + "timeout": "", + "detailed": "__flag__", + "rarely_abort_writes": "__flag__" + }, + "methods": [ + "POST" + ], + "patterns": [ + "_snapshot/{repository}/_analyze" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/modules-snapshots.html" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/sql.clear_cursor.json b/src/plugins/console/server/lib/spec_definitions/json/generated/sql.clear_cursor.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/sql.clear_cursor.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/sql.clear_cursor.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/sql.query.json b/src/plugins/console/server/lib/spec_definitions/json/generated/sql.query.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/sql.query.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/sql.query.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/sql.translate.json b/src/plugins/console/server/lib/spec_definitions/json/generated/sql.translate.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/sql.translate.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/sql.translate.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ssl.certificates.json b/src/plugins/console/server/lib/spec_definitions/json/generated/ssl.certificates.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ssl.certificates.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/ssl.certificates.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/tasks.cancel.json b/src/plugins/console/server/lib/spec_definitions/json/generated/tasks.cancel.json index 7a84c6acb53a7..2583876b93625 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/tasks.cancel.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/tasks.cancel.json @@ -3,7 +3,8 @@ "url_params": { "nodes": [], "actions": [], - "parent_task_id": "" + "parent_task_id": "", + "wait_for_completion": "__flag__" }, "methods": [ "POST" diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/terms_enum.json b/src/plugins/console/server/lib/spec_definitions/json/generated/terms_enum.json new file mode 100644 index 0000000000000..c1cb06b8a195a --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/terms_enum.json @@ -0,0 +1,12 @@ +{ + "terms_enum": { + "methods": [ + "GET", + "POST" + ], + "patterns": [ + "{indices}/_terms_enum" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/search-terms-enum.html" + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/termvectors.json b/src/plugins/console/server/lib/spec_definitions/json/generated/termvectors.json index d94cffc38b7af..53ea7a9d6cbf6 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/termvectors.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/termvectors.json @@ -14,8 +14,7 @@ "version_type": [ "internal", "external", - "external_gte", - "force" + "external_gte" ] }, "methods": [ diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/text_structure.find_structure.json b/src/plugins/console/server/lib/spec_definitions/json/generated/text_structure.find_structure.json new file mode 100644 index 0000000000000..8b5e576b495a3 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/text_structure.find_structure.json @@ -0,0 +1,32 @@ +{ + "text_structure.find_structure": { + "url_params": { + "lines_to_sample": 0, + "line_merge_size_limit": 0, + "timeout": "", + "charset": "", + "format": [ + "ndjson", + "xml", + "delimited", + "semi_structured_text" + ], + "has_header_row": "__flag__", + "column_names": [], + "delimiter": "", + "quote": "", + "should_trim_fields": "__flag__", + "grok_pattern": "", + "timestamp_field": "", + "timestamp_format": "", + "explain": "__flag__" + }, + "methods": [ + "POST" + ], + "patterns": [ + "_text_structure/find_structure" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/find-structure.html" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/transform.cat_transform.json b/src/plugins/console/server/lib/spec_definitions/json/generated/transform.cat_transform.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/transform.cat_transform.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/transform.cat_transform.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/transform.delete_transform.json b/src/plugins/console/server/lib/spec_definitions/json/generated/transform.delete_transform.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/transform.delete_transform.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/transform.delete_transform.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/transform.get_transform.json b/src/plugins/console/server/lib/spec_definitions/json/generated/transform.get_transform.json similarity index 81% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/transform.get_transform.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/transform.get_transform.json index 2875ff8b91937..3a50ff7fc3c1c 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/transform.get_transform.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/transform.get_transform.json @@ -3,7 +3,8 @@ "url_params": { "from": 0, "size": 0, - "allow_no_match": "__flag__" + "allow_no_match": "__flag__", + "exclude_generated": "__flag__" }, "methods": [ "GET" diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/transform.get_transform_stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/transform.get_transform_stats.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/transform.get_transform_stats.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/transform.get_transform_stats.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/transform.preview_transform.json b/src/plugins/console/server/lib/spec_definitions/json/generated/transform.preview_transform.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/transform.preview_transform.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/transform.preview_transform.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/transform.put_transform.json b/src/plugins/console/server/lib/spec_definitions/json/generated/transform.put_transform.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/transform.put_transform.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/transform.put_transform.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/transform.start_transform.json b/src/plugins/console/server/lib/spec_definitions/json/generated/transform.start_transform.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/transform.start_transform.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/transform.start_transform.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/transform.stop_transform.json b/src/plugins/console/server/lib/spec_definitions/json/generated/transform.stop_transform.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/transform.stop_transform.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/transform.stop_transform.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/transform.update_transform.json b/src/plugins/console/server/lib/spec_definitions/json/generated/transform.update_transform.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/transform.update_transform.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/transform.update_transform.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/update.json b/src/plugins/console/server/lib/spec_definitions/json/generated/update.json index 43945dfada35c..4dd33978a77b8 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/update.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/update.json @@ -15,7 +15,8 @@ "routing": "", "timeout": "", "if_seq_no": "", - "if_primary_term": "" + "if_primary_term": "", + "require_alias": "__flag__" }, "methods": [ "POST" diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/update_by_query.json b/src/plugins/console/server/lib/spec_definitions/json/generated/update_by_query.json index 596f8f8b83963..b4b88312e638e 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/update_by_query.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/update_by_query.json @@ -33,7 +33,6 @@ "dfs_query_then_fetch" ], "search_timeout": "", - "size": "", "max_docs": "all documents", "sort": [], "_source": [], diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/watcher.ack_watch.json b/src/plugins/console/server/lib/spec_definitions/json/generated/watcher.ack_watch.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/watcher.ack_watch.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/watcher.ack_watch.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/watcher.activate_watch.json b/src/plugins/console/server/lib/spec_definitions/json/generated/watcher.activate_watch.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/watcher.activate_watch.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/watcher.activate_watch.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/watcher.deactivate_watch.json b/src/plugins/console/server/lib/spec_definitions/json/generated/watcher.deactivate_watch.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/watcher.deactivate_watch.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/watcher.deactivate_watch.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/watcher.delete_watch.json b/src/plugins/console/server/lib/spec_definitions/json/generated/watcher.delete_watch.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/watcher.delete_watch.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/watcher.delete_watch.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/watcher.execute_watch.json b/src/plugins/console/server/lib/spec_definitions/json/generated/watcher.execute_watch.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/watcher.execute_watch.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/watcher.execute_watch.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/watcher.get_watch.json b/src/plugins/console/server/lib/spec_definitions/json/generated/watcher.get_watch.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/watcher.get_watch.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/watcher.get_watch.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/watcher.put_watch.json b/src/plugins/console/server/lib/spec_definitions/json/generated/watcher.put_watch.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/watcher.put_watch.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/watcher.put_watch.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/watcher.query_watches.json b/src/plugins/console/server/lib/spec_definitions/json/generated/watcher.query_watches.json new file mode 100644 index 0000000000000..ea6f03a5672c1 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/watcher.query_watches.json @@ -0,0 +1,12 @@ +{ + "watcher.query_watches": { + "methods": [ + "GET", + "POST" + ], + "patterns": [ + "_watcher/_query/watches" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api-query-watches.html" + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/watcher.start.json b/src/plugins/console/server/lib/spec_definitions/json/generated/watcher.start.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/watcher.start.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/watcher.start.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/watcher.stats.json b/src/plugins/console/server/lib/spec_definitions/json/generated/watcher.stats.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/watcher.stats.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/watcher.stats.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/watcher.stop.json b/src/plugins/console/server/lib/spec_definitions/json/generated/watcher.stop.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/watcher.stop.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/watcher.stop.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/xpack.info.json b/src/plugins/console/server/lib/spec_definitions/json/generated/xpack.info.json similarity index 78% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/xpack.info.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/xpack.info.json index 5b555dc129390..da92c12c5fd69 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/xpack.info.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/xpack.info.json @@ -1,7 +1,8 @@ { "xpack.info": { "url_params": { - "categories": [] + "categories": [], + "accept_enterprise": "__flag__" }, "methods": [ "GET" diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/xpack.ssl.certificates.json b/src/plugins/console/server/lib/spec_definitions/json/generated/xpack.ssl.certificates.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/xpack.ssl.certificates.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/xpack.ssl.certificates.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/xpack.usage.json b/src/plugins/console/server/lib/spec_definitions/json/generated/xpack.usage.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/xpack.usage.json rename to src/plugins/console/server/lib/spec_definitions/json/generated/xpack.usage.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/async_search.submit.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/async_search.submit.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/async_search.submit.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/async_search.submit.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ccr.follow.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/ccr.follow.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ccr.follow.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/ccr.follow.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ccr.forget_follower.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/ccr.forget_follower.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ccr.forget_follower.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/ccr.forget_follower.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ccr.put_auto_follow_pattern.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/ccr.put_auto_follow_pattern.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ccr.put_auto_follow_pattern.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/ccr.put_auto_follow_pattern.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ccr.resume_follow.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/ccr.resume_follow.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ccr.resume_follow.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/ccr.resume_follow.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/enrich.put_policy.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/enrich.put_policy.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/enrich.put_policy.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/enrich.put_policy.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ilm.move_to_step.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/ilm.move_to_step.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ilm.move_to_step.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/ilm.move_to_step.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ilm.put_lifecycle.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/ilm.put_lifecycle.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ilm.put_lifecycle.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/ilm.put_lifecycle.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.evaluate_data_frame.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/ml.evaluate_data_frame.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.evaluate_data_frame.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/ml.evaluate_data_frame.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.explain_data_frame_analytics.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/ml.explain_data_frame_analytics.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.explain_data_frame_analytics.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/ml.explain_data_frame_analytics.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.get_buckets.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/ml.get_buckets.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.get_buckets.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/ml.get_buckets.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.get_calendar_events.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/ml.get_calendar_events.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.get_calendar_events.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/ml.get_calendar_events.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.get_calendars.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/ml.get_calendars.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.get_calendars.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/ml.get_calendars.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.get_categories.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/ml.get_categories.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.get_categories.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/ml.get_categories.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.get_influencers.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/ml.get_influencers.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.get_influencers.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/ml.get_influencers.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.get_model_snapshots.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/ml.get_model_snapshots.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.get_model_snapshots.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/ml.get_model_snapshots.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.get_overall_buckets.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/ml.get_overall_buckets.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.get_overall_buckets.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/ml.get_overall_buckets.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.get_records.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/ml.get_records.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.get_records.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/ml.get_records.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.post_calendar_events.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/ml.post_calendar_events.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.post_calendar_events.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/ml.post_calendar_events.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.put_calendar.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/ml.put_calendar.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.put_calendar.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/ml.put_calendar.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.put_data_frame_analytics.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/ml.put_data_frame_analytics.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.put_data_frame_analytics.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/ml.put_data_frame_analytics.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.put_datafeed.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/ml.put_datafeed.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.put_datafeed.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/ml.put_datafeed.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.put_job.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/ml.put_job.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.put_job.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/ml.put_job.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.put_trained_model.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/ml.put_trained_model.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.put_trained_model.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/ml.put_trained_model.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.revert_model_snapshot.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/ml.revert_model_snapshot.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.revert_model_snapshot.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/ml.revert_model_snapshot.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.update_datafeed.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/ml.update_datafeed.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.update_datafeed.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/ml.update_datafeed.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.update_job.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/ml.update_job.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.update_job.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/ml.update_job.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.update_model_snapshot.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/ml.update_model_snapshot.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.update_model_snapshot.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/ml.update_model_snapshot.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/security.authenticate.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/security.authenticate.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/security.authenticate.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/security.authenticate.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/security.change_password.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/security.change_password.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/security.change_password.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/security.change_password.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/security.create_api_key.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/security.create_api_key.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/security.create_api_key.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/security.create_api_key.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/security.get_token.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/security.get_token.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/security.get_token.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/security.get_token.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/security.has_privileges.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/security.has_privileges.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/security.has_privileges.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/security.has_privileges.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/security.invalidate_api_key.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/security.invalidate_api_key.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/security.invalidate_api_key.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/security.invalidate_api_key.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/security.invalidate_token.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/security.invalidate_token.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/security.invalidate_token.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/security.invalidate_token.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/security.put_role.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/security.put_role.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/security.put_role.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/security.put_role.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/security.put_role_mapping.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/security.put_role_mapping.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/security.put_role_mapping.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/security.put_role_mapping.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/security.put_user.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/security.put_user.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/security.put_user.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/security.put_user.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/overrides/security.saml_authenticate.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/security.saml_authenticate.json new file mode 100644 index 0000000000000..a1d50daa70572 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/overrides/security.saml_authenticate.json @@ -0,0 +1,9 @@ +{ + "security.saml_authenticate": { + "data_autocomplete_rules": { + "content": "", + "ids": [], + "realm": "" + } + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/overrides/security.saml_complete_logout.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/security.saml_complete_logout.json new file mode 100644 index 0000000000000..b9d5b83e0424c --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/overrides/security.saml_complete_logout.json @@ -0,0 +1,10 @@ +{ + "security.saml_complete_logout": { + "data_autocomplete_rules": { + "realm": "", + "ids": [], + "query_string": "", + "content": "" + } + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/overrides/security.saml_invalidate.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/security.saml_invalidate.json new file mode 100644 index 0000000000000..f4a27d659c360 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/overrides/security.saml_invalidate.json @@ -0,0 +1,9 @@ +{ + "security.saml_invalidate": { + "data_autocomplete_rules": { + "query_string": "", + "acs": "", + "realm": "" + } + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/overrides/security.saml_logout.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/security.saml_logout.json new file mode 100644 index 0000000000000..40e5c6bc09299 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/overrides/security.saml_logout.json @@ -0,0 +1,8 @@ +{ + "security.saml_logout": { + "data_autocomplete_rules": { + "token": "", + "refresh_token": "" + } + } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/overrides/security.saml_prepare_authentication.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/security.saml_prepare_authentication.json new file mode 100644 index 0000000000000..4ebdd53c95166 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/overrides/security.saml_prepare_authentication.json @@ -0,0 +1,8 @@ +{ + "security.saml_prepare_authentication": { + "data_autocomplete_rules": { + "acs": "", + "realm": "" + } + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/slm.put_lifecycle.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/slm.put_lifecycle.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/slm.put_lifecycle.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/slm.put_lifecycle.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/slm.start.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/slm.start.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/slm.start.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/slm.start.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/slm.stop.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/slm.stop.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/slm.stop.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/slm.stop.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/sql.query.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/sql.query.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/sql.query.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/sql.query.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/overrides/terms_enum.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/terms_enum.json new file mode 100644 index 0000000000000..3a3681cefe072 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/overrides/terms_enum.json @@ -0,0 +1,12 @@ +{ + "terms_enum": { + "data_autocomplete_rules": { + "field": "", + "string": "", + "size": "", + "timeout": "", + "case_insensitive": { "__one_of": [true, false] }, + "index_filter": "" + } + } +} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/transform.get_transform_stats.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/transform.get_transform_stats.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/transform.get_transform_stats.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/transform.get_transform_stats.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/transform.preview_transform.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/transform.preview_transform.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/transform.preview_transform.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/transform.preview_transform.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/transform.put_transform.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/transform.put_transform.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/transform.put_transform.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/transform.put_transform.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/transform.stop_transform.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/transform.stop_transform.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/transform.stop_transform.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/transform.stop_transform.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/transform.update_transform.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/transform.update_transform.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/transform.update_transform.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/transform.update_transform.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.graph.explore.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.graph.explore.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.graph.explore.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.graph.explore.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.info.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.info.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.info.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.info.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.license.post.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.license.post.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.license.post.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.license.post.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.rollup.delete_job.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.rollup.delete_job.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.rollup.delete_job.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.rollup.delete_job.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.rollup.put_job.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.rollup.put_job.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.rollup.put_job.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.rollup.put_job.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.rollup.rollup_search.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.rollup.rollup_search.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.rollup.rollup_search.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.rollup.rollup_search.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.security.authenticate.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.security.authenticate.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.security.authenticate.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.security.authenticate.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.security.change_password.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.security.change_password.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.security.change_password.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.security.change_password.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.security.get_token.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.security.get_token.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.security.get_token.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.security.get_token.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.security.invalidate_token.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.security.invalidate_token.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.security.invalidate_token.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.security.invalidate_token.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.security.put_role.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.security.put_role.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.security.put_role.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.security.put_role.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.security.put_role_mapping.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.security.put_role_mapping.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.security.put_role_mapping.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.security.put_role_mapping.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.security.put_user.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.security.put_user.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.security.put_user.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.security.put_user.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.watcher.execute_watch.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.watcher.execute_watch.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.watcher.execute_watch.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.watcher.execute_watch.json diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.watcher.put_watch.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.watcher.put_watch.json similarity index 100% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/xpack.watcher.put_watch.json rename to src/plugins/console/server/lib/spec_definitions/json/overrides/xpack.watcher.put_watch.json diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 66d81d058fc77..b8af7c12d57fc 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -17,7 +17,6 @@ import { CoreSetup } from 'src/core/public'; import { CoreSetup as CoreSetup_2 } from 'kibana/public'; import { CoreStart } from 'kibana/public'; import { CoreStart as CoreStart_2 } from 'src/core/public'; -import * as CSS from 'csstype'; import { Datatable as Datatable_2 } from 'src/plugins/expressions'; import { Datatable as Datatable_3 } from 'src/plugins/expressions/common'; import { DatatableColumn as DatatableColumn_2 } from 'src/plugins/expressions'; @@ -72,13 +71,12 @@ import { Plugin } from 'src/core/public'; import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/public'; import { PluginInitializerContext as PluginInitializerContext_3 } from 'kibana/public'; import { PopoverAnchorPosition } from '@elastic/eui'; -import * as PropTypes from 'prop-types'; import { PublicContract } from '@kbn/utility-types'; import { PublicMethodsOf } from '@kbn/utility-types'; import { PublicUiSettingsParams } from 'src/core/server/types'; import { RangeFilter as RangeFilter_2 } from 'src/plugins/data/public'; import React from 'react'; -import * as React_3 from 'react'; +import * as React_2 from 'react'; import { RecursiveReadonly } from '@kbn/utility-types'; import { Request as Request_2 } from '@hapi/hapi'; import { RequestAdapter } from 'src/plugins/inspector/common'; diff --git a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx index 4655bbf8e91a5..cc796ad749f0b 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx @@ -77,7 +77,7 @@ function FilterBarUI(props: Props) { const button = ( setIsAddFilterPopoverOpen(true)} data-test-subj="addFilter" className="globalFilterBar__addButton" diff --git a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap index 0ab3f8a4e3466..1e7b59d8a9e76 100644 --- a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap +++ b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap @@ -1169,7 +1169,6 @@ exports[`Inspector Data View component should render single table without select > { // Stub out a minimal mapping of 4 fields let mapping; + beforeAll(async () => { + await initAngularBootstrap(); + }); beforeAll(() => setScopedHistory(createBrowserHistory())); beforeEach(() => { angular.element.prototype.slice = jest.fn(function (index) { diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table.test.js b/src/plugins/discover/public/application/angular/doc_table/doc_table.test.js index 1db35ddf18089..097f32965b141 100644 --- a/src/plugins/discover/public/application/angular/doc_table/doc_table.test.js +++ b/src/plugins/discover/public/application/angular/doc_table/doc_table.test.js @@ -17,6 +17,7 @@ import hits from '../../../__fixtures__/real_hits'; import { coreMock } from '../../../../../../core/public/mocks'; import { dataPluginMock } from '../../../../../data/public/mocks'; import { navigationPluginMock } from '../../../../../navigation/public/mocks'; +import { initAngularBootstrap } from '../../../../../kibana_legacy/public/angular_bootstrap'; import { setScopedHistory, setServices } from '../../../kibana_services'; import { getInnerAngularModule } from '../get_inner_angular'; @@ -54,6 +55,9 @@ describe('docTable', () => { const core = coreMock.createStart(); let $elem; + beforeAll(async () => { + await initAngularBootstrap(); + }); beforeAll(() => setScopedHistory(createBrowserHistory())); beforeEach(() => { angular.element.prototype.slice = jest.fn(() => { diff --git a/src/plugins/discover/public/application/angular/get_inner_angular.ts b/src/plugins/discover/public/application/angular/get_inner_angular.ts index 26d64d5adc8a3..992d82795302b 100644 --- a/src/plugins/discover/public/application/angular/get_inner_angular.ts +++ b/src/plugins/discover/public/application/angular/get_inner_angular.ts @@ -33,13 +33,12 @@ import { createDocViewerDirective } from './doc_viewer'; import { createDiscoverGridDirective } from './create_discover_grid_directive'; import { createRenderCompleteDirective } from './directives/render_complete'; import { - initAngularBootstrap, configureAppAngularModule, PrivateProvider, - PromiseServiceCreator, registerListenEventListener, watchMultiDecorator, } from '../../../../kibana_legacy/public'; +import { PromiseServiceCreator } from './helpers'; import { DiscoverStartPlugins } from '../../plugin'; import { getScopedHistory } from '../../kibana_services'; import { createDiscoverDirective } from './create_discover_directive'; @@ -54,7 +53,6 @@ export function getInnerAngularModule( deps: DiscoverStartPlugins, context: PluginInitializerContext ) { - initAngularBootstrap(); const module = initializeInnerAngularModule(name, core, deps.navigation, deps.data); configureAppAngularModule(module, { core, env: context.env }, true, getScopedHistory); return module; diff --git a/src/plugins/discover/public/application/angular/helpers/index.ts b/src/plugins/discover/public/application/angular/helpers/index.ts index 3d2c0b1c63b33..6a7f75b7e81a2 100644 --- a/src/plugins/discover/public/application/angular/helpers/index.ts +++ b/src/plugins/discover/public/application/angular/helpers/index.ts @@ -8,3 +8,4 @@ export { formatRow, formatTopLevelObject } from './row_formatter'; export { handleSourceColumnState } from './state_helpers'; +export { PromiseServiceCreator } from './promises'; diff --git a/src/plugins/kibana_legacy/public/angular/promises.d.ts b/src/plugins/discover/public/application/angular/helpers/promises.d.ts similarity index 100% rename from src/plugins/kibana_legacy/public/angular/promises.d.ts rename to src/plugins/discover/public/application/angular/helpers/promises.d.ts diff --git a/src/plugins/kibana_legacy/public/angular/promises.js b/src/plugins/discover/public/application/angular/helpers/promises.js similarity index 100% rename from src/plugins/kibana_legacy/public/angular/promises.js rename to src/plugins/discover/public/application/angular/helpers/promises.js diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index 3e31fe1d46d45..1e8a5cdac95ef 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -403,6 +403,7 @@ export class DiscoverPlugin } // this is used by application mount and tests const { getInnerAngularModule } = await import('./application/angular/get_inner_angular'); + await plugins.kibanaLegacy.loadAngularBootstrap(); const module = getInnerAngularModule( innerAngularName, core, @@ -473,6 +474,7 @@ export class DiscoverPlugin throw Error('Discover plugin getEmbeddableInjector: initializeServices is undefined'); } const { core, plugins } = await this.initializeServices(); + await getServices().kibanaLegacy.loadAngularBootstrap(); getServices().kibanaLegacy.loadFontAwesome(); const { getInnerAngularModuleEmbeddable } = await import( './application/angular/get_inner_angular' diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index a810b1f48a07c..54fe2a7b0bec2 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -11,7 +11,6 @@ import { ApiResponse } from '@elastic/elasticsearch/lib/Transport'; import { ApplicationStart as ApplicationStart_2 } from 'kibana/public'; import Boom from '@hapi/boom'; import { ConfigDeprecationProvider } from '@kbn/config'; -import * as CSS from 'csstype'; import { DetailedPeerCertificate } from 'tls'; import { EmbeddableStart as EmbeddableStart_2 } from 'src/plugins/embeddable/public/plugin'; import { EnvironmentMode } from '@kbn/config'; @@ -44,7 +43,6 @@ import { PackageInfo } from '@kbn/config'; import { Path } from 'history'; import { PeerCertificate } from 'tls'; import { PluginInitializerContext } from 'src/core/public'; -import * as PropTypes from 'prop-types'; import { PublicMethodsOf } from '@kbn/utility-types'; import { PublicUiSettingsParams } from 'src/core/server/types'; import React from 'react'; @@ -95,8 +93,7 @@ export interface Adapters { // // @public (undocumented) export class AddPanelAction implements Action_3 { - // Warning: (ae-forgotten-export) The symbol "React" needs to be exported by the entry point index.d.ts - constructor(getFactory: EmbeddableStart_2['getEmbeddableFactory'], getAllFactories: EmbeddableStart_2['getEmbeddableFactories'], overlays: OverlayStart_2, notifications: NotificationsStart_2, SavedObjectFinder: React_2.ComponentType, reportUiCounter?: ((appName: string, type: import("@kbn/analytics").UiCounterMetricType, eventNames: string | string[], count?: number | undefined) => void) | undefined); + constructor(getFactory: EmbeddableStart_2['getEmbeddableFactory'], getAllFactories: EmbeddableStart_2['getEmbeddableFactories'], overlays: OverlayStart_2, notifications: NotificationsStart_2, SavedObjectFinder: React.ComponentType, reportUiCounter?: ((appName: string, type: import("@kbn/analytics").UiCounterMetricType, eventNames: string | string[], count?: number | undefined) => void) | undefined); // (undocumented) execute(context: ActionExecutionContext_2): Promise; // (undocumented) diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts index 1152290f7d553..0a6f2cc2c2b18 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts @@ -62,7 +62,7 @@ export const UseArray = ({ const uniqueId = useRef(0); const form = useFormContext(); - const { __getFieldDefaultValue } = form; + const { getFieldDefaultValue } = form; const getNewItemAtIndex = useCallback( (index: number): ArrayItem => ({ @@ -75,7 +75,7 @@ export const UseArray = ({ const fieldDefaultValue = useMemo(() => { const defaultValues = readDefaultValueOnForm - ? (__getFieldDefaultValue(path) as any[]) + ? (getFieldDefaultValue(path) as any[]) : undefined; const getInitialItemsFromValues = (values: any[]): ArrayItem[] => @@ -88,13 +88,7 @@ export const UseArray = ({ return defaultValues ? getInitialItemsFromValues(defaultValues) : new Array(initialNumberOfItems).fill('').map((_, i) => getNewItemAtIndex(i)); - }, [ - path, - initialNumberOfItems, - readDefaultValueOnForm, - __getFieldDefaultValue, - getNewItemAtIndex, - ]); + }, [path, initialNumberOfItems, readDefaultValueOnForm, getFieldDefaultValue, getNewItemAtIndex]); // Create a new hook field with the "isIncludedInOutput" set to false so we don't use its value to build the final form data. // Apart from that the field behaves like a normal field and is hooked into the form validation lifecycle. diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx index 3a5fbaba8f3b8..45fa2e977a6c7 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx @@ -59,8 +59,7 @@ function UseFieldComp(props: Props { + describe('field.validate()', () => { + const EMPTY_VALUE = ' '; + + test('It should not invalidate a field with arrayItem validation when isBlocking is false', async () => { + let fieldHook: FieldHook; + + const TestField = ({ field }: { field: FieldHook }) => { + fieldHook = field; + return null; + }; + + const TestForm = () => { + const { form } = useForm(); + + return ( +
+ + + ); + }; + + registerTestBed(TestForm)(); + + let validateResponse: FieldValidateResponse; + + await act(async () => { + validateResponse = await fieldHook!.validate({ + value: EMPTY_VALUE, + validationType: VALIDATION_TYPES.ARRAY_ITEM, + }); + }); + + // validation fails for ARRAY_ITEM with a non-blocking validation error + expect(validateResponse!).toEqual({ + isValid: false, + errors: [ + { + code: 'ERR_FIELD_MISSING', + path: 'test-path', + message: 'error-message', + __isBlocking__: false, + validationType: 'arrayItem', + }, + ], + }); + + // expect the field to be valid because the validation error is non-blocking + expect(fieldHook!.isValid).toBe(true); + }); + + test('It should invalidate an arrayItem field when isBlocking is true', async () => { + let fieldHook: FieldHook; + + const TestField = ({ field }: { field: FieldHook }) => { + fieldHook = field; + return null; + }; + + const TestForm = () => { + const { form } = useForm(); + + return ( +
+ + + ); + }; + + registerTestBed(TestForm)(); + + let validateResponse: FieldValidateResponse; + + await act(async () => { + validateResponse = await fieldHook!.validate({ + value: EMPTY_VALUE, + validationType: VALIDATION_TYPES.ARRAY_ITEM, + }); + }); + + // validation fails for ARRAY_ITEM with a blocking validation error + expect(validateResponse!).toEqual({ + isValid: false, + errors: [ + { + code: 'ERR_FIELD_MISSING', + path: 'test-path', + message: 'error-message', + __isBlocking__: true, + validationType: 'arrayItem', + }, + ], + }); + + // expect the field to be invalid because the validation error is blocking + expect(fieldHook!.isValid).toBe(false); + }); + }); +}); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts index 0cf1bb3601667..77bb17d7b9e60 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts @@ -15,6 +15,7 @@ import { FieldValidateResponse, ValidationError, FormData, + ValidationConfig, } from '../types'; import { FIELD_TYPES, VALIDATION_TYPES } from '../constants'; @@ -189,10 +190,12 @@ export const useField = ( { formData, value: valueToValidate, + onlyBlocking: runOnlyBlockingValidations, validationTypeToValidate, }: { formData: any; value: I; + onlyBlocking: boolean; validationTypeToValidate?: string; }, clearFieldErrors: FieldHook['clearErrors'] @@ -203,10 +206,31 @@ export const useField = ( // By default, for fields that have an asynchronous validation // we will clear the errors as soon as the field value changes. - clearFieldErrors([VALIDATION_TYPES.FIELD, VALIDATION_TYPES.ASYNC]); + clearFieldErrors([ + validationTypeToValidate ?? VALIDATION_TYPES.FIELD, + VALIDATION_TYPES.ASYNC, + ]); cancelInflightValidation(); + const doByPassValidation = ({ + type: validationType, + isBlocking, + }: ValidationConfig) => { + if ( + typeof validationTypeToValidate !== 'undefined' && + validationType !== validationTypeToValidate + ) { + return true; + } + + if (runOnlyBlockingValidations && isBlocking === false) { + return true; + } + + return false; + }; + const runAsync = async () => { const validationErrors: ValidationError[] = []; @@ -219,10 +243,7 @@ export const useField = ( type: validationType = VALIDATION_TYPES.FIELD, } = validation; - if ( - typeof validationTypeToValidate !== 'undefined' && - validationType !== validationTypeToValidate - ) { + if (doByPassValidation(validation)) { continue; } @@ -265,10 +286,7 @@ export const useField = ( type: validationType = VALIDATION_TYPES.FIELD, } = validation; - if ( - typeof validationTypeToValidate !== 'undefined' && - validationType !== validationTypeToValidate - ) { + if (doByPassValidation(validation)) { continue; } @@ -344,6 +362,7 @@ export const useField = ( formData = __getFormData$().value, value: valueToValidate = value, validationType, + onlyBlocking = false, } = validationData; setIsValidated(true); @@ -377,6 +396,7 @@ export const useField = ( formData, value: valueToValidate, validationTypeToValidate: validationType, + onlyBlocking, }, clearErrors ); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx index 40fc179c73c3b..92a9876f1cd30 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx @@ -10,7 +10,7 @@ import React, { useEffect } from 'react'; import { act } from 'react-dom/test-utils'; import { registerTestBed, getRandomString, TestBed } from '../shared_imports'; - +import { emptyField } from '../../helpers/field_validators'; import { Form, UseField } from '../components'; import { FormSubmitHandler, @@ -18,7 +18,8 @@ import { FormHook, ValidationFunc, FieldConfig, -} from '../types'; + VALIDATION_TYPES, +} from '..'; import { useForm } from './use_form'; interface MyForm { @@ -501,4 +502,74 @@ describe('useForm() hook', () => { expect(isValid).toBeUndefined(); // Make sure it is "undefined" and not "false". }); }); + + describe('form.validate()', () => { + test('should not invalidate a field with arrayItem validation when validating a form', async () => { + const TestComp = () => { + const { form } = useForm(); + formHook = form; + + return ( +
+ + + ); + }; + + registerTestBed(TestComp)(); + + let isValid: boolean = false; + + await act(async () => { + isValid = await formHook!.validate(); + }); + + expect(isValid).toBe(true); + }); + + test('should invalidate a field with a blocking arrayItem validation when validating a form', async () => { + const TestComp = () => { + const { form } = useForm(); + formHook = form; + + return ( +
+ + + ); + }; + + registerTestBed(TestComp)(); + + let isValid: boolean = false; + + await act(async () => { + isValid = await formHook!.validate(); + }); + + expect(isValid).toBe(false); + }); + }); }); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts index fb334afb22b13..dcf2cb37d6542 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts @@ -151,14 +151,14 @@ export function useForm( }, [fieldsToArray]); const validateFields: FormHook['__validateFields'] = useCallback( - async (fieldNames) => { + async (fieldNames, onlyBlocking = false) => { const fieldsToValidate = fieldNames .map((name) => fieldsRefs.current[name]) .filter((field) => field !== undefined); const formData = getFormData$().value; const validationResult = await Promise.all( - fieldsToValidate.map((field) => field.validate({ formData })) + fieldsToValidate.map((field) => field.validate({ formData, onlyBlocking })) ); if (isMounted.current === false) { @@ -257,11 +257,6 @@ export function useForm( [getFormData$, updateFormData$, fieldsToArray] ); - const getFieldDefaultValue: FormHook['__getFieldDefaultValue'] = useCallback( - (fieldName) => get(defaultValueDeserialized.current, fieldName), - [] - ); - const readFieldConfigFromSchema: FormHook['__readFieldConfigFromSchema'] = useCallback( (fieldName) => { const config = (get(schema ?? {}, fieldName) as FieldConfig) || {}; @@ -315,7 +310,8 @@ export function useForm( if (fieldsToValidate.length === 0) { isFormValid = fieldsArray.every(isFieldValid); } else { - ({ isFormValid } = await validateFields(fieldsToValidate.map((field) => field.path))); + const fieldPathsToValidate = fieldsToValidate.map((field) => field.path); + ({ isFormValid } = await validateFields(fieldPathsToValidate, true)); } setIsValid(isFormValid); @@ -338,6 +334,11 @@ export function useForm( const getFields: FormHook['getFields'] = useCallback(() => fieldsRefs.current, []); + const getFieldDefaultValue: FormHook['getFieldDefaultValue'] = useCallback( + (fieldName) => get(defaultValueDeserialized.current, fieldName), + [] + ); + const submit: FormHook['submit'] = useCallback( async (e) => { if (e) { @@ -430,6 +431,7 @@ export function useForm( setFieldValue, setFieldErrors, getFields, + getFieldDefaultValue, getFormData, getErrors, reset, @@ -438,7 +440,6 @@ export function useForm( __updateFormDataAt: updateFormDataAt, __updateDefaultValueAt: updateDefaultValueAt, __readFieldConfigFromSchema: readFieldConfigFromSchema, - __getFieldDefaultValue: getFieldDefaultValue, __addField: addField, __removeField: removeField, __validateFields: validateFields, diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts index 75c918d4340f2..4e9ff29f0cdd3 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts @@ -36,6 +36,8 @@ export interface FormHook setFieldErrors: (fieldName: string, errors: ValidationError[]) => void; /** Access the fields on the form. */ getFields: () => FieldsMap; + /** Access the defaultValue for a specific field */ + getFieldDefaultValue: (path: string) => unknown; /** * Return the form data. It accepts an optional options object with an `unflatten` parameter (defaults to `true`). * If you are only interested in the raw form data, pass `unflatten: false` to the handler @@ -53,12 +55,13 @@ export interface FormHook __addField: (field: FieldHook) => void; __removeField: (fieldNames: string | string[]) => void; __validateFields: ( - fieldNames: string[] + fieldNames: string[], + /** Run only blocking validations */ + onlyBlocking?: boolean ) => Promise<{ areFieldsValid: boolean; isFormValid: boolean | undefined }>; __updateFormDataAt: (field: string, value: unknown) => void; __updateDefaultValueAt: (field: string, value: unknown) => void; __readFieldConfigFromSchema: (field: string) => FieldConfig; - __getFieldDefaultValue: (path: string) => unknown; } export type FormSchema = { @@ -137,6 +140,7 @@ export interface FieldHook { formData?: any; value?: I; validationType?: string; + onlyBlocking?: boolean; }) => FieldValidateResponse | Promise; reset: (options?: { resetValue?: boolean; defaultValue?: T }) => unknown | undefined; // Flag to indicate if the field value will be included in the form data outputted diff --git a/src/plugins/expression_reveal_image/.i18nrc.json b/src/plugins/expression_reveal_image/.i18nrc.json deleted file mode 100755 index 5b073e4374519..0000000000000 --- a/src/plugins/expression_reveal_image/.i18nrc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "prefix": "expressionRevealImage", - "paths": { - "expressionRevealImage": "." - }, - "translations": ["translations/ja-JP.json"] -} diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx index 6bd06528084ce..6405a81282471 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx @@ -132,7 +132,7 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { } ) => ( <> - + {name}   diff --git a/src/plugins/kibana_legacy/public/angular/angular_config.tsx b/src/plugins/kibana_legacy/public/angular/angular_config.tsx index daecfbc57ea99..48ee6d2db269e 100644 --- a/src/plugins/kibana_legacy/public/angular/angular_config.tsx +++ b/src/plugins/kibana_legacy/public/angular/angular_config.tsx @@ -13,6 +13,7 @@ import { ILocationProvider, IModule, IRootScopeService, + IRequestConfig, } from 'angular'; import $ from 'jquery'; import { set } from '@elastic/safer-lodash-set'; @@ -22,7 +23,6 @@ import { ChromeBreadcrumb, EnvironmentMode, PackageInfo } from 'kibana/public'; import { History } from 'history'; import { CoreStart } from 'kibana/public'; -import { isSystemApiRequest } from '../utils'; import { formatAngularHttpError, isAngularHttpError } from '../notify/lib'; export interface RouteConfiguration { @@ -38,6 +38,11 @@ export interface RouteConfiguration { requireUICapability?: string; } +function isSystemApiRequest(request: IRequestConfig) { + const { headers } = request; + return headers && !!headers['kbn-system-request']; +} + /** * Detects whether a given angular route is a dummy route that doesn't * require any action. There are two ways this can happen: diff --git a/src/plugins/kibana_legacy/public/angular/index.ts b/src/plugins/kibana_legacy/public/angular/index.ts index d9d8c0c19eb7b..369495698591d 100644 --- a/src/plugins/kibana_legacy/public/angular/index.ts +++ b/src/plugins/kibana_legacy/public/angular/index.ts @@ -6,8 +6,6 @@ * Side Public License, v 1. */ -// @ts-ignore -export { PromiseServiceCreator } from './promises'; // @ts-ignore export { watchMultiDecorator } from './watch_multi'; export * from './angular_config'; diff --git a/src/plugins/kibana_legacy/public/index.ts b/src/plugins/kibana_legacy/public/index.ts index 03adb768cde20..ea5172f78a68f 100644 --- a/src/plugins/kibana_legacy/public/index.ts +++ b/src/plugins/kibana_legacy/public/index.ts @@ -14,7 +14,6 @@ export const plugin = (initializerContext: PluginInitializerContext) => export * from './plugin'; -export { initAngularBootstrap } from './angular_bootstrap'; export { PaginateDirectiveProvider, PaginateControlsDirectiveProvider } from './paginate/paginate'; export * from './angular'; export * from './notify'; diff --git a/src/plugins/kibana_legacy/public/mocks.ts b/src/plugins/kibana_legacy/public/mocks.ts index 40834635cc570..6116c0682cb3b 100644 --- a/src/plugins/kibana_legacy/public/mocks.ts +++ b/src/plugins/kibana_legacy/public/mocks.ts @@ -22,6 +22,7 @@ const createStartContract = (): Start => ({ getHideWriteControls: jest.fn(), }, loadFontAwesome: jest.fn(), + loadAngularBootstrap: jest.fn(), }); export const kibanaLegacyPluginMock = { diff --git a/src/plugins/kibana_legacy/public/notify/index.ts b/src/plugins/kibana_legacy/public/notify/index.ts index a243059cb1918..d4dcaa77cc47a 100644 --- a/src/plugins/kibana_legacy/public/notify/index.ts +++ b/src/plugins/kibana_legacy/public/notify/index.ts @@ -6,5 +6,4 @@ * Side Public License, v 1. */ -export * from './toasts'; export * from './lib'; diff --git a/src/plugins/kibana_legacy/public/notify/toasts/TOAST_NOTIFICATIONS.md b/src/plugins/kibana_legacy/public/notify/toasts/TOAST_NOTIFICATIONS.md deleted file mode 100644 index de6a51f3927d1..0000000000000 --- a/src/plugins/kibana_legacy/public/notify/toasts/TOAST_NOTIFICATIONS.md +++ /dev/null @@ -1,100 +0,0 @@ -# Toast notifications - -Use this service to surface toasts in the bottom-right corner of the screen. After a brief delay, they'll disappear. They're useful for notifying the user of state changes. See [the EUI docs](https://elastic.github.io/eui/) for more information on toasts and their role within the UI. - -## Importing the module - -```js -import { toastNotifications } from 'ui/notify'; -``` - -## Interface - -### Adding toasts - -For convenience, there are several methods which predefine the appearance of different types of toasts. Use these methods so that the same types of toasts look similar to the user. - -#### Default - -Neutral toast. Tell the user a change in state has occurred, which is not necessarily good or bad. - -```js -toastNotifications.add('Copied to clipboard'); -``` - -#### Success - -Let the user know that an action was successful, such as saving or deleting an object. - -```js -toastNotifications.addSuccess('Your document was saved'); -``` - -#### Warning - -If something OK or good happened, but perhaps wasn't perfect, show a warning toast. - -```js -toastNotifications.addWarning('Your document was saved, but not its edit history'); -``` - -#### Danger - -When the user initiated an action but the action failed, show them a danger toast. - -```js -toastNotifications.addDanger('An error caused your document to be lost'); -``` - -### Removing a toast - -Toasts will automatically be dismissed after a brief delay, but if for some reason you want to dismiss a toast, you can use the returned toast from one of the `add` methods and then pass it to `remove`. - -```js -const toast = toastNotifications.add('Your document was saved'); -toastNotifications.remove(toast); -``` - -### Configuration options - -If you want to configure the toast further you can provide an object instead of a string. The properties of this object correspond to the `propTypes` accepted by the `EuiToast` component. Refer to [the EUI docs](https://elastic.github.io/eui/) for info on these `propTypes`. - -```js -toastNotifications.add({ - title: 'Your document was saved', - text: 'Only you have access to this document', - color: 'success', - iconType: 'check', - 'data-test-subj': 'saveDocumentSuccess', -}); -``` - -Because the underlying components are React, you can use JSX to pass in React elements to the `text` prop. This gives you total flexibility over the content displayed within the toast. - -```js -toastNotifications.add({ - title: 'Your document was saved', - text: ( -
-

- Only you have access to this document. Edit permissions. -

- - -
- ), -}); -``` - -## Use in functional tests - -Functional tests are commonly used to verify that a user action yielded a successful outcome. If you surface a toast to notify the user of this successful outcome, you can place a `data-test-subj` attribute on the toast and use it to check if the toast exists inside of your functional test. This acts as a proxy for verifying the successful outcome. - -```js -toastNotifications.addSuccess({ - title: 'Your document was saved', - 'data-test-subj': 'saveDocumentSuccess', -}); -``` diff --git a/src/plugins/kibana_legacy/public/notify/toasts/toast_notifications.test.ts b/src/plugins/kibana_legacy/public/notify/toasts/toast_notifications.test.ts deleted file mode 100644 index c2c5d9a4fc014..0000000000000 --- a/src/plugins/kibana_legacy/public/notify/toasts/toast_notifications.test.ts +++ /dev/null @@ -1,76 +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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { notificationServiceMock } from '../../../../../core/public/mocks'; -import { ToastNotifications } from './toast_notifications'; -import { Toast } from 'kibana/public'; -import { BehaviorSubject } from 'rxjs'; - -describe('ToastNotifications', () => { - describe('interface', () => { - function setup() { - const toastsMock = notificationServiceMock.createStartContract().toasts; - return { toastNotifications: new ToastNotifications(toastsMock), toastsMock }; - } - - describe('add method', () => { - test('adds a toast', () => { - const { toastNotifications, toastsMock } = setup(); - toastNotifications.add({}); - expect(toastsMock.add).toHaveBeenCalled(); - }); - }); - - describe('remove method', () => { - test('removes a toast', () => { - const { toastNotifications, toastsMock } = setup(); - const fakeToast = {} as Toast; - toastNotifications.remove(fakeToast); - expect(toastsMock.remove).toHaveBeenCalledWith(fakeToast); - }); - }); - - describe('onChange method', () => { - test('callback is called when observable changes', () => { - const toastsMock = notificationServiceMock.createStartContract().toasts; - const toasts$ = new BehaviorSubject([]); - toastsMock.get$.mockReturnValue(toasts$); - const toastNotifications = new ToastNotifications(toastsMock); - const onChangeSpy = jest.fn(); - toastNotifications.onChange(onChangeSpy); - toasts$.next([{ id: 'toast1' }]); - toasts$.next([]); - expect(onChangeSpy).toHaveBeenCalledTimes(2); - }); - }); - - describe('addSuccess method', () => { - test('adds a success toast', () => { - const { toastNotifications, toastsMock } = setup(); - toastNotifications.addSuccess({}); - expect(toastsMock.addSuccess).toHaveBeenCalled(); - }); - }); - - describe('addWarning method', () => { - test('adds a warning toast', () => { - const { toastNotifications, toastsMock } = setup(); - toastNotifications.addWarning({}); - expect(toastsMock.addWarning).toHaveBeenCalled(); - }); - }); - - describe('addDanger method', () => { - test('adds a danger toast', () => { - const { toastNotifications, toastsMock } = setup(); - toastNotifications.addWarning({}); - expect(toastsMock.addWarning).toHaveBeenCalled(); - }); - }); - }); -}); diff --git a/src/plugins/kibana_legacy/public/notify/toasts/toast_notifications.ts b/src/plugins/kibana_legacy/public/notify/toasts/toast_notifications.ts deleted file mode 100644 index e7ccbbca07b73..0000000000000 --- a/src/plugins/kibana_legacy/public/notify/toasts/toast_notifications.ts +++ /dev/null @@ -1,37 +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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { NotificationsSetup, Toast, ToastInput, ErrorToastOptions } from 'kibana/public'; - -export class ToastNotifications { - public list: Toast[] = []; - - private onChangeCallback?: () => void; - - constructor(private readonly toasts: NotificationsSetup['toasts']) { - toasts.get$().subscribe((list) => { - this.list = list; - - if (this.onChangeCallback) { - this.onChangeCallback(); - } - }); - } - - public onChange = (callback: () => void) => { - this.onChangeCallback = callback; - }; - - public add = (toastOrTitle: ToastInput) => this.toasts.add(toastOrTitle); - public remove = (toast: Toast) => this.toasts.remove(toast); - public addSuccess = (toastOrTitle: ToastInput) => this.toasts.addSuccess(toastOrTitle); - public addWarning = (toastOrTitle: ToastInput) => this.toasts.addWarning(toastOrTitle); - public addDanger = (toastOrTitle: ToastInput) => this.toasts.addDanger(toastOrTitle); - public addError = (error: Error, options: ErrorToastOptions) => - this.toasts.addError(error, options); -} diff --git a/src/plugins/kibana_legacy/public/plugin.ts b/src/plugins/kibana_legacy/public/plugin.ts index 337fdb80da7e4..f60130d367b58 100644 --- a/src/plugins/kibana_legacy/public/plugin.ts +++ b/src/plugins/kibana_legacy/public/plugin.ts @@ -33,6 +33,14 @@ export class KibanaLegacyPlugin { loadFontAwesome: async () => { await import('./font_awesome'); }, + /** + * Loads angular bootstrap modules. Should be removed once the last consumer has migrated to EUI + * @deprecated + */ + loadAngularBootstrap: async () => { + const { initAngularBootstrap } = await import('./angular_bootstrap'); + initAngularBootstrap(); + }, /** * @deprecated * Just exported for wiring up with dashboard mode, should not be used. diff --git a/src/plugins/kibana_legacy/public/utils/index.ts b/src/plugins/kibana_legacy/public/utils/index.ts index db3c0af6c8cb9..94233558b4627 100644 --- a/src/plugins/kibana_legacy/public/utils/index.ts +++ b/src/plugins/kibana_legacy/public/utils/index.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -export * from './system_api'; // @ts-ignore export { KbnAccessibleClickProvider } from './kbn_accessible_click'; // @ts-ignore diff --git a/src/plugins/kibana_legacy/public/utils/system_api.ts b/src/plugins/kibana_legacy/public/utils/system_api.ts deleted file mode 100644 index d0fe221935ba5..0000000000000 --- a/src/plugins/kibana_legacy/public/utils/system_api.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { IRequestConfig } from 'angular'; - -const SYSTEM_REQUEST_HEADER_NAME = 'kbn-system-request'; -const LEGACY_SYSTEM_API_HEADER_NAME = 'kbn-system-api'; - -/** - * Adds a custom header designating request as system API - * @param originalHeaders Object representing set of headers - * @return Object representing set of headers, with system API header added in - */ -export function addSystemApiHeader(originalHeaders: Record) { - const systemApiHeaders = { - [SYSTEM_REQUEST_HEADER_NAME]: true, - }; - return { - ...originalHeaders, - ...systemApiHeaders, - }; -} - -/** - * Returns true if request is a system API request; false otherwise - * - * @param request Object Request object created by $http service - * @return true if request is a system API request; false otherwise - */ -export function isSystemApiRequest(request: IRequestConfig) { - const { headers } = request; - return ( - headers && (!!headers[SYSTEM_REQUEST_HEADER_NAME] || !!headers[LEGACY_SYSTEM_API_HEADER_NAME]) - ); -} diff --git a/src/plugins/kibana_react/common/eui_styled_components.tsx b/src/plugins/kibana_react/common/eui_styled_components.tsx index 10cd168da6faa..62876a03c7d83 100644 --- a/src/plugins/kibana_react/common/eui_styled_components.tsx +++ b/src/plugins/kibana_react/common/eui_styled_components.tsx @@ -6,15 +6,14 @@ * Side Public License, v 1. */ +import type { DecoratorFn } from '@storybook/react'; import React from 'react'; import * as styledComponents from 'styled-components'; import { ThemedStyledComponentsModule, ThemeProvider, ThemeProviderProps } from 'styled-components'; - -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiThemeVars, euiLightVars, euiDarkVars } from '@kbn/ui-shared-deps/theme'; export interface EuiTheme { - eui: typeof euiLightVars | typeof euiDarkVars; + eui: typeof euiThemeVars; darkMode: boolean; } @@ -36,6 +35,16 @@ const EuiThemeProvider = < /> ); +/** + * Storybook decorator using the EUI theme provider. Uses the value from + * `globals` provided by the Storybook theme switcher. + */ +export const EuiThemeProviderDecorator: DecoratorFn = (storyFn, { globals }) => { + const darkMode = globals.euiTheme === 'v8.dark' || globals.euiTheme === 'v7.dark'; + + return {storyFn()}; +}; + const { default: euiStyled, css, diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx index 523bf07f828c9..f4ca53a9a974e 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx @@ -42,7 +42,7 @@ export function TopNavMenuItem(props: TopNavMenuData) { {upperFirst(props.label || props.id!)} ) : ( - + {upperFirst(props.label || props.id!)} ); diff --git a/src/plugins/screenshot_mode/.i18nrc.json b/src/plugins/screenshot_mode/.i18nrc.json deleted file mode 100644 index 79643fbb63d30..0000000000000 --- a/src/plugins/screenshot_mode/.i18nrc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "prefix": "screenshotMode", - "paths": { - "screenshotMode": "." - }, - "translations": ["translations/ja-JP.json"] -} diff --git a/src/plugins/timelion/public/plugin.ts b/src/plugins/timelion/public/plugin.ts index 6f8dbfdcc6704..63ea9a38e2795 100644 --- a/src/plugins/timelion/public/plugin.ts +++ b/src/plugins/timelion/public/plugin.ts @@ -19,7 +19,7 @@ import { AppNavLinkStatus, } from '../../../core/public'; import { Panel } from './panels/panel'; -import { initAngularBootstrap } from '../../kibana_legacy/public'; +import { KibanaLegacyStart } from '../../kibana_legacy/public'; import { createKbnUrlTracker } from '../../kibana_utils/public'; import { DataPublicPluginStart, esFilters, DataPublicPluginSetup } from '../../data/public'; import { NavigationPublicPluginStart } from '../../navigation/public'; @@ -41,6 +41,7 @@ export interface TimelionPluginStartDependencies { visualizations: VisualizationsStart; visTypeTimelion: VisTypeTimelionPluginStart; savedObjects: SavedObjectsStart; + kibanaLegacy: KibanaLegacyStart; } /** @internal */ @@ -91,7 +92,6 @@ export class TimelionPlugin stopUrlTracker(); }; - initAngularBootstrap(); core.application.register({ id: 'timelion', title: 'Timelion', @@ -103,6 +103,7 @@ export class TimelionPlugin visTypeTimelion.isUiEnabled === false ? AppNavLinkStatus.hidden : AppNavLinkStatus.default, mount: async (params: AppMountParameters) => { const [coreStart, pluginsStart] = await core.getStartServices(); + await pluginsStart.kibanaLegacy.loadAngularBootstrap(); this.currentHistory = params.history; appMounted(); diff --git a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.test.js b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.test.js index 65e26ddf6e03f..cbc3db6585a7d 100644 --- a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.test.js +++ b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table.test.js @@ -15,7 +15,7 @@ import { round } from 'lodash'; import { getFieldFormatsRegistry } from '../../../../data/public/test_utils'; import { coreMock } from '../../../../../core/public/mocks'; -import { initAngularBootstrap } from '../../../../kibana_legacy/public'; +import { initAngularBootstrap } from '../../../../kibana_legacy/public/angular_bootstrap'; import { setUiSettings } from '../../../../data/public/services'; import { UI_SETTINGS } from '../../../../data/public/'; import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../../../../share/public'; @@ -60,10 +60,12 @@ describe('Table Vis - AggTable Directive', function () { initTableVisLegacyModule(tableVisModule); }; + beforeAll(async () => { + await initAngularBootstrap(); + }); beforeEach(() => { setUiSettings(core.uiSettings); setFormatService(getFieldFormatsRegistry(core)); - initAngularBootstrap(); initLocalAngular(); angular.mock.module('kibana/table_vis'); angular.mock.inject(($injector, config) => { diff --git a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.test.js b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.test.js index 1c6630e30e5f7..ba04b2f449f6d 100644 --- a/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.test.js +++ b/src/plugins/vis_type_table/public/legacy/agg_table/agg_table_group.test.js @@ -13,11 +13,11 @@ import expect from '@kbn/expect'; import { getFieldFormatsRegistry } from '../../../../data/public/test_utils'; import { coreMock } from '../../../../../core/public/mocks'; -import { initAngularBootstrap } from '../../../../kibana_legacy/public'; import { setUiSettings } from '../../../../data/public/services'; import { setFormatService } from '../../services'; import { getInnerAngular } from '../get_inner_angular'; import { initTableVisLegacyModule } from '../table_vis_legacy_module'; +import { initAngularBootstrap } from '../../../../kibana_legacy/public/angular_bootstrap'; import { tabifiedData } from './tabified_data'; const uiSettings = new Map(); @@ -40,10 +40,12 @@ describe('Table Vis - AggTableGroup Directive', function () { initTableVisLegacyModule(tableVisModule); }; + beforeAll(async () => { + await initAngularBootstrap(); + }); beforeEach(() => { setUiSettings(core.uiSettings); setFormatService(getFieldFormatsRegistry(core)); - initAngularBootstrap(); initLocalAngular(); angular.mock.module('kibana/table_vis'); angular.mock.inject(($injector) => { diff --git a/src/plugins/vis_type_table/public/legacy/get_inner_angular.ts b/src/plugins/vis_type_table/public/legacy/get_inner_angular.ts index 09fde318ee4df..412dd904a5e87 100644 --- a/src/plugins/vis_type_table/public/legacy/get_inner_angular.ts +++ b/src/plugins/vis_type_table/public/legacy/get_inner_angular.ts @@ -16,7 +16,6 @@ import 'angular-recursion'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { CoreStart, IUiSettingsClient, PluginInitializerContext } from 'kibana/public'; import { - initAngularBootstrap, PaginateDirectiveProvider, PaginateControlsDirectiveProvider, PrivateProvider, @@ -24,8 +23,6 @@ import { KbnAccessibleClickProvider, } from '../../../kibana_legacy/public'; -initAngularBootstrap(); - const thirdPartyAngularDependencies = ['ngSanitize', 'ui.bootstrap', 'RecursionHelper']; export function getAngularModule(name: string, core: CoreStart, context: PluginInitializerContext) { diff --git a/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.test.ts b/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.test.ts index 77148803e7978..3feff52f86792 100644 --- a/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.test.ts +++ b/src/plugins/vis_type_table/public/legacy/paginated_table/paginated_table.test.ts @@ -12,6 +12,7 @@ import $ from 'jquery'; import 'angular-sanitize'; import 'angular-mocks'; +import { initAngularBootstrap } from '../../../../kibana_legacy/public/angular_bootstrap'; import { getAngularModule } from '../get_inner_angular'; import { initTableVisLegacyModule } from '../table_vis_legacy_module'; import { coreMock } from '../../../../../core/public/mocks'; @@ -56,6 +57,10 @@ describe('Table Vis - Paginated table', () => { const defaultPerPage = 10; let paginatedTable: any; + beforeAll(async () => { + await initAngularBootstrap(); + }); + const initLocalAngular = () => { const tableVisModule = getAngularModule( 'kibana/table_vis', diff --git a/src/plugins/vis_type_table/public/legacy/table_vis_controller.test.ts b/src/plugins/vis_type_table/public/legacy/table_vis_controller.test.ts index 36a9cc9cce77f..f4a742ea16cb4 100644 --- a/src/plugins/vis_type_table/public/legacy/table_vis_controller.test.ts +++ b/src/plugins/vis_type_table/public/legacy/table_vis_controller.test.ts @@ -13,6 +13,7 @@ import $ from 'jquery'; import { getAngularModule } from './get_inner_angular'; import { initTableVisLegacyModule } from './table_vis_legacy_module'; +import { initAngularBootstrap } from '../../../kibana_legacy/public/angular_bootstrap'; import { tableVisLegacyTypeDefinition } from './table_vis_legacy_type'; import { Vis } from '../../../visualizations/public'; import { stubFields } from '../../../data/public/stubs'; @@ -76,6 +77,9 @@ describe('Table Vis - Controller', () => { initTableVisLegacyModule(tableVisModule); }; + beforeAll(async () => { + await initAngularBootstrap(); + }); beforeEach(initLocalAngular); beforeEach(angular.mock.module('kibana/table_vis')); diff --git a/src/plugins/vis_type_table/public/legacy/vis_controller.ts b/src/plugins/vis_type_table/public/legacy/vis_controller.ts index ee446c58c0013..ec198aa96f1f9 100644 --- a/src/plugins/vis_type_table/public/legacy/vis_controller.ts +++ b/src/plugins/vis_type_table/public/legacy/vis_controller.ts @@ -56,6 +56,7 @@ export function getTableVisualizationControllerClass( async initLocalAngular() { if (!this.tableVisModule) { const [coreStart, { kibanaLegacy }] = await core.getStartServices(); + await kibanaLegacy.loadAngularBootstrap(); this.tableVisModule = getAngularModule(innerAngularName, coreStart, context); initTableVisLegacyModule(this.tableVisModule); kibanaLegacy.loadFontAwesome(); diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js index f167bc35c06e9..a232a1dc03ae3 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/cumulative_sum.js @@ -23,6 +23,7 @@ import { EuiFormRow, EuiSpacer, } from '@elastic/eui'; +import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; export function CumulativeSumAgg(props) { const { model, siblings, fields, indexPattern } = props; @@ -70,7 +71,7 @@ export function CumulativeSumAgg(props) { onChange={handleSelectChange('field')} metrics={siblings} metric={model} - fields={fields[indexPattern]} + fields={fields[getIndexPatternKey(indexPattern)]} value={model.field} exclude={[METRIC_TYPES.TOP_HIT]} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js index 9bed7015b0245..616f40128ff22 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/derivative.js @@ -25,6 +25,7 @@ import { EuiSpacer, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; export const DerivativeAgg = (props) => { const { siblings, fields, indexPattern } = props; @@ -80,7 +81,7 @@ export const DerivativeAgg = (props) => { onChange={handleSelectChange('field')} metrics={siblings} metric={model} - fields={fields[indexPattern]} + fields={fields[getIndexPatternKey(indexPattern)]} value={model.field} exclude={[METRIC_TYPES.TOP_HIT]} fullWidth diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js index 79f70f45d6256..a3ce43f97a36a 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/moving_average.js @@ -26,6 +26,7 @@ import { EuiFieldNumber, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; const DEFAULTS = { model_type: MODEL_TYPES.UNWEIGHTED, @@ -141,7 +142,7 @@ export const MovingAverageAgg = (props) => { onChange={handleSelectChange('field')} metrics={siblings} metric={model} - fields={fields[indexPattern]} + fields={fields[getIndexPatternKey(indexPattern)]} value={model.field} exclude={[METRIC_TYPES.TOP_HIT]} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js index 156a042abb4e2..c974f5d5f05f5 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_only.js @@ -23,6 +23,7 @@ import { EuiSpacer, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; export const PositiveOnlyAgg = (props) => { const { siblings, fields, indexPattern } = props; @@ -74,7 +75,7 @@ export const PositiveOnlyAgg = (props) => { onChange={handleSelectChange('field')} metrics={siblings} metric={model} - fields={fields[indexPattern]} + fields={fields[getIndexPatternKey(indexPattern)]} value={model.field} exclude={[METRIC_TYPES.TOP_HIT]} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js index a553b1a4c6671..efc2a72c3dd67 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/serial_diff.js @@ -24,6 +24,7 @@ import { EuiSpacer, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; export const SerialDiffAgg = (props) => { const { siblings, fields, indexPattern, model } = props; @@ -74,7 +75,7 @@ export const SerialDiffAgg = (props) => { onChange={handleSelectChange('field')} metrics={siblings} metric={model} - fields={fields[indexPattern]} + fields={fields[getIndexPatternKey(indexPattern)]} value={model.field} exclude={[METRIC_TYPES.TOP_HIT]} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js index 9a30988d252e5..d2b3f45a70164 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_sibling.js @@ -27,6 +27,7 @@ import { EuiSpacer, } from '@elastic/eui'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; const StandardSiblingAggUi = (props) => { const { siblings, intl, fields, indexPattern } = props; @@ -147,7 +148,7 @@ const StandardSiblingAggUi = (props) => { onChange={handleSelectChange('field')} exclude={[METRIC_TYPES.PERCENTILE, METRIC_TYPES.TOP_HIT]} metrics={siblings} - fields={fields[indexPattern]} + fields={fields[getIndexPatternKey(indexPattern)]} metric={model} value={model.field} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js index 546c09cdf34fd..b9ef2d8913574 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { useMemo, useEffect } from 'react'; import { AggRow } from './agg_row'; import { AggSelect } from './agg_select'; import { FieldSelect } from './field_select'; @@ -62,6 +62,7 @@ const getAggWithOptions = (field = {}, fieldTypesRestriction) => { }, ]; case KBN_FIELD_TYPES.STRING: + case KBN_FIELD_TYPES.DATE: return [ { label: i18n.translate('visTypeTimeseries.topHit.aggWithOptions.concatenate', { @@ -91,16 +92,18 @@ const getOrderOptions = () => [ }, ]; +const AGG_WITH_KEY = 'agg_with'; const ORDER_DATE_RESTRICT_FIELDS = [KBN_FIELD_TYPES.DATE]; +const getModelDefaults = () => ({ + size: 1, + order: 'desc', + [AGG_WITH_KEY]: 'noop', +}); + const TopHitAggUi = (props) => { const { fields, series, panel } = props; - const defaults = { - size: 1, - agg_with: 'noop', - order: 'desc', - }; - const model = { ...defaults, ...props.model }; + const model = useMemo(() => ({ ...getModelDefaults(), ...props.model }), [props.model]); const indexPattern = series.override_index_pattern ? series.series_index_pattern : panel.index_pattern; @@ -110,7 +113,7 @@ const TopHitAggUi = (props) => { PANEL_TYPES.METRIC, PANEL_TYPES.MARKDOWN, ].includes(panel.type) - ? [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.STRING] + ? [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.STRING, KBN_FIELD_TYPES.DATE] : [KBN_FIELD_TYPES.NUMBER]; const handleChange = createChangeHandler(props.onChange, model); @@ -124,13 +127,23 @@ const TopHitAggUi = (props) => { const htmlId = htmlIdGenerator(); const selectedAggWithOption = aggWithOptions.find((option) => { - return model.agg_with === option.value; + return model[AGG_WITH_KEY] === option.value; }); const selectedOrderOption = orderOptions.find((option) => { return model.order === option.value; }); + useEffect(() => { + const defaultFn = aggWithOptions?.[0]?.value; + const aggWith = model[AGG_WITH_KEY]; + if (aggWith && defaultFn && aggWith !== defaultFn && !selectedAggWithOption) { + handleChange({ + [AGG_WITH_KEY]: defaultFn, + }); + } + }, [model, selectedAggWithOption, aggWithOptions, handleChange]); + return ( { { )} options={aggWithOptions} selectedOptions={selectedAggWithOption ? [selectedAggWithOption] : []} - onChange={handleSelectChange('agg_with')} + onChange={handleSelectChange(AGG_WITH_KEY)} singleSelection={{ asPlainText: true }} + data-test-subj="topHitAggregateWithComboBox" /> @@ -231,6 +245,7 @@ const TopHitAggUi = (props) => { onChange={handleSelectChange('order_by')} indexPattern={indexPattern} fields={fields} + data-test-subj="topHitOrderByFieldSelect" /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js index b9d554e254bcc..ba06b0fffd307 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/vars.js @@ -15,6 +15,7 @@ import { AddDeleteButtons } from '../add_delete_buttons'; import { collectionActions } from '../lib/collection_actions'; import { MetricSelect } from './metric_select'; import { EuiFlexGroup, EuiFlexItem, EuiFieldText } from '@elastic/eui'; +import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; export const newVariable = (opts) => ({ id: uuid.v1(), name: '', field: '', ...opts }); @@ -59,7 +60,7 @@ export class CalculationVars extends Component { metrics={this.props.metrics} metric={this.props.model} value={row.field} - fields={this.props.fields[this.props.indexPattern]} + fields={this.props.fields[getIndexPatternKey(this.props.indexPattern)]} includeSiblings={this.props.includeSiblings} exclude={this.props.exclude} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js index c1d82a182e509..9bccc13d19269 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js @@ -16,7 +16,7 @@ export const createTickFormatter = (format = '0,0.[00]', template, getConfig = n const fieldFormats = getFieldFormats(); if (!template) template = '{{value}}'; - const render = handlebars.compile(template, { knownHelpersOnly: true }); + const render = handlebars.compile(template, { noEscape: true, knownHelpersOnly: true }); let formatter; if (isDuration(format)) { diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.tsx b/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.tsx index 9ba0822402562..3633f8add7457 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.tsx @@ -207,6 +207,7 @@ export class TablePanelConfig extends Component< diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap b/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap index 562c463f6c83c..ce381a0e539d0 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap +++ b/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap @@ -78,6 +78,7 @@ exports[`src/legacy/core_plugins/metrics/public/components/splits/terms.test.js labelType="label" > @@ -100,6 +101,7 @@ exports[`src/legacy/core_plugins/metrics/public/components/splits/terms.test.js labelType="label" > diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js b/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js index 7db6a75e2392c..9c097de38d56a 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js +++ b/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js @@ -27,6 +27,7 @@ import { import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { KBN_FIELD_TYPES } from '../../../../../data/public'; import { STACKED_OPTIONS } from '../../visualizations/constants'; +import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; const DEFAULTS = { terms_direction: 'desc', terms_size: 10, terms_order_by: '_count' }; @@ -75,10 +76,11 @@ export const SplitByTermsUI = ({ }), }, ]; + const fieldsSelector = getIndexPatternKey(indexPattern); const selectedDirectionOption = dirOptions.find((option) => { return model.terms_direction === option.value; }); - const selectedField = find(fields[indexPattern], ({ name }) => name === model.terms_field); + const selectedField = find(fields[fieldsSelector], ({ name }) => name === model.terms_field); const selectedFieldType = get(selectedField, 'type'); if ( @@ -144,6 +146,7 @@ export const SplitByTermsUI = ({ @@ -160,6 +163,7 @@ export const SplitByTermsUI = ({ @@ -198,7 +202,7 @@ export const SplitByTermsUI = ({ metrics={metrics} clearable={false} additionalOptions={[defaultCount, terms]} - fields={fields[indexPattern]} + fields={fields[fieldsSelector]} onChange={handleSelectChange('terms_order_by')} restrict="basic" value={model.terms_order_by} diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js index 4dd8f672c9ea3..4db038de912f5 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js @@ -58,7 +58,11 @@ class TableVis extends Component { renderRow = (row) => { const { model } = this.props; - let rowDisplay = model.pivot_type === 'date' ? this.dateFormatter.convert(row.key) : row.key; + + let rowDisplay = getValueOrEmpty( + model.pivot_type === 'date' ? this.dateFormatter.convert(row.key) : row.key + ); + if (model.drilldown_url) { const url = replaceVars(model.drilldown_url, {}, { key: row.key }); rowDisplay = {rowDisplay}; @@ -98,7 +102,7 @@ class TableVis extends Component { }); return ( - {getValueOrEmpty(rowDisplay)} + {rowDisplay} {columns} ); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.js index 16e7b9d6072cb..13b890189325c 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.js @@ -111,7 +111,7 @@ export const bucketTransform = { docs: { top_hits: { size: bucket.size, - _source: { includes: [bucket.field] }, + fields: [bucket.field], }, }, }, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_agg_value.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_agg_value.js index 32d17ef6d6cb7..90df3f2675959 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_agg_value.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_agg_value.js @@ -45,10 +45,10 @@ export const getAggValue = (row, metric) => { } const hits = get(row, [metric.id, 'docs', 'hits', 'hits'], []); - const values = hits.map((doc) => get(doc, `_source.${metric.field}`)); + const values = hits.map((doc) => doc.fields[metric.field]); const aggWith = (metric.agg_with && aggFns[metric.agg_with]) || aggFns.noop; - return aggWith(values); + return aggWith(values.flat()); case METRIC_TYPES.COUNT: return get(row, 'doc_count', null); default: diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_agg_value.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_agg_value.test.js index a23c57f567563..ecbdd1563c304 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_agg_value.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_agg_value.test.js @@ -67,11 +67,7 @@ describe('getAggValue', () => { doc_count: 1, docs: { hits: { - hits: [ - { _source: { example: { value: 25 } } }, - { _source: { example: { value: 25 } } }, - { _source: { example: { value: 25 } } }, - ], + hits: [{ fields: { 'example.value': [25, 25, 25] } }], }, }, }, diff --git a/test/examples/expressions_explorer/expressions.ts b/test/examples/expressions_explorer/expressions.ts index 4c240653b5fdd..9aef64a392a7b 100644 --- a/test/examples/expressions_explorer/expressions.ts +++ b/test/examples/expressions_explorer/expressions.ts @@ -22,7 +22,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { await retry.try(async () => { const text = await testSubjects.getVisibleText('expressionResult'); expect(text).to.be( - '{\n "type": "render",\n "as": "markdown",\n "value": {\n "content": "## expressions explorer",\n "font": {\n "type": "style",\n "spec": {\n "fontFamily": "\'Open Sans\', Helvetica, Arial, sans-serif",\n "fontWeight": "normal",\n "fontStyle": "normal",\n "textDecoration": "none",\n "textAlign": "left",\n "fontSize": "14px",\n "lineHeight": "1"\n },\n "css": "font-family:\'Open Sans\', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:left;font-size:14px;line-height:1"\n },\n "openLinksInNewTab": false\n }\n}' + '{\n "type": "render",\n "as": "markdown_vis",\n "value": {\n "visType": "markdown",\n "visParams": {\n "markdown": "## expressions explorer",\n "openLinksInNewTab": false,\n "fontSize": 12\n }\n }\n}' ); }); }); diff --git a/test/functional/apps/discover/_field_data.ts b/test/functional/apps/discover/_field_data.ts index 5ab6495686726..ec9f9cf65e0fa 100644 --- a/test/functional/apps/discover/_field_data.ts +++ b/test/functional/apps/discover/_field_data.ts @@ -33,8 +33,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await PageObjects.common.navigateToApp('discover'); }); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/104466 - describe.skip('field data', function () { + + describe('field data', function () { it('search php should show the correct hit count', async function () { const expectedHitCount = '445'; await retry.try(async function () { diff --git a/test/functional/apps/discover/index.ts b/test/functional/apps/discover/index.ts index b396f172f6961..a17bf53e7f478 100644 --- a/test/functional/apps/discover/index.ts +++ b/test/functional/apps/discover/index.ts @@ -12,7 +12,8 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const browser = getService('browser'); - describe('discover app', function () { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/104466 + describe.skip('discover app', function () { this.tags('ciGroup6'); before(function () { diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index ca310493960f5..49b2ad8f9646a 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -24,6 +24,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'timePicker', 'visChart', 'common', + 'settings', ]); describe('visual builder', function describeIndexTests() { @@ -44,14 +45,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); describe('metric', () => { + const { visualBuilder } = PageObjects; + beforeEach(async () => { - await PageObjects.visualBuilder.resetPage(); - await PageObjects.visualBuilder.clickMetric(); - await PageObjects.visualBuilder.checkMetricTabIsPresent(); - await PageObjects.visualBuilder.clickPanelOptions('metric'); - await PageObjects.visualBuilder.setMetricsDataTimerangeMode('Last value'); - await PageObjects.visualBuilder.setDropLastBucket(true); - await PageObjects.visualBuilder.clickDataTab('metric'); + await visualBuilder.resetPage(); + await visualBuilder.clickMetric(); + await visualBuilder.checkMetricTabIsPresent(); + await visualBuilder.clickPanelOptions('metric'); + await visualBuilder.setMetricsDataTimerangeMode('Last value'); + await visualBuilder.setDropLastBucket(true); + await visualBuilder.clickDataTab('metric'); }); it('should not have inspector enabled', async () => { @@ -59,28 +62,98 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should show correct data', async () => { - const value = await PageObjects.visualBuilder.getMetricValue(); + const value = await visualBuilder.getMetricValue(); expect(value).to.eql('156'); }); it('should show correct data with Math Aggregation', async () => { - await PageObjects.visualBuilder.createNewAgg(); - await PageObjects.visualBuilder.selectAggType('math', 1); - await PageObjects.visualBuilder.fillInVariable(); - await PageObjects.visualBuilder.fillInExpression('params.test + 1'); - const value = await PageObjects.visualBuilder.getMetricValue(); + await visualBuilder.createNewAgg(); + await visualBuilder.selectAggType('math', 1); + await visualBuilder.fillInVariable(); + await visualBuilder.fillInExpression('params.test + 1'); + const value = await visualBuilder.getMetricValue(); expect(value).to.eql('157'); }); it('should populate fields for basic functions', async () => { - const { visualBuilder } = PageObjects; - await visualBuilder.selectAggType('Average'); await visualBuilder.setFieldForAggregation('machine.ram'); const isFieldForAggregationValid = await visualBuilder.checkFieldForAggregationValidity(); expect(isFieldForAggregationValid).to.be(true); }); + + it('should show correct data for Value Count with Entire time range mode', async () => { + await visualBuilder.selectAggType('Value Count'); + await visualBuilder.setFieldForAggregation('machine.ram'); + + await visualBuilder.clickPanelOptions('metric'); + await visualBuilder.setMetricsDataTimerangeMode('Entire time range'); + + const value = await visualBuilder.getMetricValue(); + expect(value).to.eql('13,492'); + }); + + it('should show same data for kibana and string index pattern modes', async () => { + await visualBuilder.selectAggType('Max'); + await visualBuilder.setFieldForAggregation('machine.ram'); + const kibanaIndexPatternModeValue = await visualBuilder.getMetricValue(); + + await visualBuilder.clickPanelOptions('metric'); + await visualBuilder.switchIndexPatternSelectionMode(false); + const stringIndexPatternModeValue = await visualBuilder.getMetricValue(); + + expect(kibanaIndexPatternModeValue).to.eql(stringIndexPatternModeValue); + expect(kibanaIndexPatternModeValue).to.eql('32,212,254,720'); + }); + + describe('Color rules', () => { + beforeEach(async () => { + await visualBuilder.selectAggType('Min'); + await visualBuilder.setFieldForAggregation('machine.ram'); + + await visualBuilder.clickPanelOptions('metric'); + await visualBuilder.setColorRuleOperator('>= greater than or equal'); + await visualBuilder.setColorRuleValue(0); + }); + + it('should apply color rules to visualization background', async () => { + await visualBuilder.setColorPickerValue('#FFCFDF'); + + const backGroundStyle = await visualBuilder.getBackgroundStyle(); + expect(backGroundStyle).to.eql('background-color: rgb(255, 207, 223);'); + }); + + it('should apply color rules to metric value', async () => { + await visualBuilder.setColorPickerValue('#AD7DE6', 1); + + const backGroundStyle = await visualBuilder.getMetricValueStyle(); + expect(backGroundStyle).to.eql('color: rgb(173, 125, 230);'); + }); + }); + + describe('Top Hit aggregation', () => { + beforeEach(async () => { + await visualBuilder.selectAggType('Top Hit'); + await visualBuilder.setTopHitOrderByField('@timestamp'); + }); + + it('should show correct data for string type field', async () => { + await visualBuilder.setFieldForAggregation('machine.os.raw'); + await visualBuilder.setTopHitAggregateWithOption('Concatenate'); + + const value = await visualBuilder.getMetricValue(); + expect(value).to.eql('win 7'); + }); + + it('should show correct data for runtime field', async () => { + await visualBuilder.setFieldForAggregation('hello_world_runtime_field'); + await visualBuilder.setTopHitAggregateWithOption('Concatenate'); + + const value = await visualBuilder.getMetricValue(); + expect(value).to.eql('hello world'); + }); + }); }); describe('gauge', () => { diff --git a/test/functional/apps/visualize/_tsvb_table.ts b/test/functional/apps/visualize/_tsvb_table.ts index abe3b799e4711..de0771d3c8ec5 100644 --- a/test/functional/apps/visualize/_tsvb_table.ts +++ b/test/functional/apps/visualize/_tsvb_table.ts @@ -10,12 +10,14 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getPageObjects }: FtrProviderContext) { +export default function ({ getPageObjects, getService }: FtrProviderContext) { const { visualBuilder, visualize, visChart } = getPageObjects([ 'visualBuilder', 'visualize', 'visChart', ]); + const findService = getService('find'); + const retry = getService('retry'); describe('visual builder', function describeIndexTests() { before(async () => { @@ -43,6 +45,19 @@ export default function ({ getPageObjects }: FtrProviderContext) { expect(tableData).to.be(EXPECTED); }); + it('should display drilldown urls', async () => { + const baseURL = 'http://elastic.co/foo/'; + + await visualBuilder.clickPanelOptions('table'); + await visualBuilder.setDrilldownUrl(`${baseURL}{{key}}`); + + await retry.try(async () => { + const links = await findService.allByCssSelector(`a[href="${baseURL}ios"]`); + + expect(links.length).to.be(1); + }); + }); + it('should display correct values on changing metrics aggregation', async () => { const EXPECTED = 'OS Cardinality\nwin 8 12\nwin xp 9\nwin 7 8\nios 5\nosx 3'; diff --git a/test/functional/apps/visualize/_tsvb_time_series.ts b/test/functional/apps/visualize/_tsvb_time_series.ts index a0c9d806facc6..cc57d58348180 100644 --- a/test/functional/apps/visualize/_tsvb_time_series.ts +++ b/test/functional/apps/visualize/_tsvb_time_series.ts @@ -155,7 +155,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('Clicking on the chart', () => { it(`should create a filter`, async () => { - await visualBuilder.setMetricsGroupByTerms('machine.os.raw'); + await visualBuilder.setMetricsGroupByTerms('machine.os.raw', { + include: 'win 7', + exclude: 'ios', + }); await visualBuilder.clickSeriesOption(); await testSubjects.click('visualizeSaveButton'); diff --git a/test/functional/fixtures/kbn_archiver/visualize.json b/test/functional/fixtures/kbn_archiver/visualize.json index 660da856964b4..225dc0592e87d 100644 --- a/test/functional/fixtures/kbn_archiver/visualize.json +++ b/test/functional/fixtures/kbn_archiver/visualize.json @@ -3,6 +3,7 @@ "fieldAttrs": "{\"utc_time\":{\"customLabel\":\"UTC time\"}}", "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}", "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", + "runtimeFieldMap":"{\"hello_world_runtime_field\":{\"type\":\"keyword\",\"script\":{\"source\":\"emit('hello world')\"}}}", "timeFieldName": "@timestamp", "title": "logstash-*" }, @@ -301,4 +302,4 @@ "references": [], "type": "index-pattern", "version": "WzE1LDFd" -} \ No newline at end of file +} diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index ea11560e37b6f..fd89a88658b3a 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -277,6 +277,13 @@ export class VisualBuilderPageObject extends FtrService { await this.comboBox.setElement(formatterEl, formatter, { clickWithMouse: true }); } + public async setDrilldownUrl(value: string) { + const drilldownEl = await this.testSubjects.find('drilldownUrl'); + + await drilldownEl.clearValue(); + await drilldownEl.type(value); + } + /** * set duration formatter additional settings * @@ -568,6 +575,42 @@ export class VisualBuilderPageObject extends FtrService { await this.testSubjects.existOrFail('euiColorPickerPopover', { timeout: 5000 }); } + public async setColorPickerValue(colorHex: string, nth: number = 0): Promise { + const picker = await this.find.allByCssSelector('.tvbColorPicker button'); + await picker[nth].clickMouseButton(); + await this.checkColorPickerPopUpIsPresent(); + await this.find.setValue('.euiColorPicker input', colorHex); + await this.visChart.waitForVisualizationRenderingStabilized(); + } + + public async setColorRuleOperator(condition: string): Promise { + await this.retry.try(async () => { + await this.comboBox.clearInputField('colorRuleOperator'); + await this.comboBox.set('colorRuleOperator', condition); + }); + } + + public async setColorRuleValue(value: number): Promise { + await this.retry.try(async () => { + const colorRuleValueInput = await this.find.byCssSelector( + '[data-test-subj="colorRuleValue"]' + ); + await colorRuleValueInput.type(value.toString()); + }); + } + + public async getBackgroundStyle(): Promise { + await this.visChart.waitForVisualizationRenderingStabilized(); + const visualization = await this.find.byClassName('tvbVis'); + return await visualization.getAttribute('style'); + } + + public async getMetricValueStyle(): Promise { + await this.visChart.waitForVisualizationRenderingStabilized(); + const metricValue = await this.find.byCssSelector('[data-test-subj="tsvbMetricValue"]'); + return await metricValue.getAttribute('style'); + } + public async changePanelPreview(nth: number = 0): Promise { const prevRenderingCount = await this.visChart.getVisualizationRenderingCount(); const changePreviewBtnArray = await this.testSubjects.findAll('AddActivatePanelBtn'); @@ -628,7 +671,10 @@ export class VisualBuilderPageObject extends FtrService { return await this.find.allByCssSelector('.tvbSeriesEditor'); } - public async setMetricsGroupByTerms(field: string) { + public async setMetricsGroupByTerms( + field: string, + filtering: { include?: string; exclude?: string } = {} + ) { const groupBy = await this.find.byCssSelector( '.tvbAggRow--split [data-test-subj="comboBoxInput"]' ); @@ -636,6 +682,22 @@ export class VisualBuilderPageObject extends FtrService { await this.common.sleep(1000); const byField = await this.testSubjects.find('groupByField'); await this.comboBox.setElement(byField, field); + + await this.setMetricsGroupByFiltering(filtering.include, filtering.exclude); + } + + public async setMetricsGroupByFiltering(include?: string, exclude?: string) { + const setFilterValue = async (value: string | undefined, subjectKey: string) => { + if (typeof value === 'string') { + const valueSubject = await this.testSubjects.find(subjectKey); + + await valueSubject.clearValue(); + await valueSubject.type(value); + } + }; + + await setFilterValue(include, 'groupByInclude'); + await setFilterValue(exclude, 'groupByExclude'); } public async checkSelectedMetricsGroupByValue(value: string) { @@ -654,4 +716,15 @@ export class VisualBuilderPageObject extends FtrService { const dataTimeRangeMode = await this.testSubjects.find('dataTimeRangeMode'); return await this.comboBox.isOptionSelected(dataTimeRangeMode, value); } + + public async setTopHitAggregateWithOption(option: string): Promise { + await this.comboBox.set('topHitAggregateWithComboBox', option); + } + + public async setTopHitOrderByField(timeField: string) { + await this.retry.try(async () => { + await this.comboBox.clearInputField('topHitOrderByFieldSelect'); + await this.comboBox.set('topHitOrderByFieldSelect', timeField); + }); + } } diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json index b704274a58aa4..e92dc717ae25e 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json @@ -6,7 +6,8 @@ "types": [ "node", "jest", - "react" + "react", + "@emotion/react/types/css-prop" ] }, "include": [ diff --git a/test/scripts/jenkins_build_load_testing.sh b/test/scripts/jenkins_build_load_testing.sh index d7c7bda83c9ef..667540515fc83 100755 --- a/test/scripts/jenkins_build_load_testing.sh +++ b/test/scripts/jenkins_build_load_testing.sh @@ -53,6 +53,9 @@ echo "cloud.auth: ${USER_FROM_VAULT}:${PASS_FROM_VAULT}" >> cfg/metricbeat/metri cp cfg/metricbeat/metricbeat.yml $KIBANA_DIR/metricbeat-install/metricbeat.yml # Disable system monitoring: enabled for now to have more data #mv $KIBANA_DIR/metricbeat-install/modules.d/system.yml $KIBANA_DIR/metricbeat-install/modules.d/system.yml.disabled +echo " -> Building puppeteer project" +cd puppeteer +yarn install && yarn build popd # doesn't persist, also set in kibanaPipeline.groovy diff --git a/test/tsconfig.json b/test/tsconfig.json index 8cf33d93a4067..dccbe8d715c51 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -6,7 +6,7 @@ "emitDeclarationOnly": true, "declaration": true, "declarationMap": true, - "types": ["node", "resize-observer-polyfill"] + "types": ["node", "resize-observer-polyfill", "@emotion/react/types/css-prop"] }, "include": [ "**/*", diff --git a/tsconfig.base.json b/tsconfig.base.json index cc8b66848a394..0c8fec7c88cda 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -12,7 +12,10 @@ // Allows for importing from `kibana` package for the exported types. "kibana": ["./kibana"], "kibana/public": ["src/core/public"], - "kibana/server": ["src/core/server"] + "kibana/server": ["src/core/server"], + "@emotion/core": [ + "typings/@emotion" + ], }, // Support .tsx files and transform JSX into calls to React.createElement "jsx": "react", @@ -62,7 +65,8 @@ "flot", "jest-styled-components", "@testing-library/jest-dom", - "resize-observer-polyfill" + "resize-observer-polyfill", + "@emotion/react/types/css-prop" ] } } diff --git a/tsconfig.json b/tsconfig.json index f6df8fcbb6406..082325306e379 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -76,7 +76,6 @@ { "path": "./x-pack/plugins/canvas/tsconfig.json" }, { "path": "./x-pack/plugins/cases/tsconfig.json" }, { "path": "./x-pack/plugins/cloud/tsconfig.json" }, - { "path": "./x-pack/plugins/console_extensions/tsconfig.json" }, { "path": "./x-pack/plugins/data_enhanced/tsconfig.json" }, { "path": "./x-pack/plugins/dashboard_mode/tsconfig.json" }, { "path": "./x-pack/plugins/discover_enhanced/tsconfig.json" }, diff --git a/tsconfig.refs.json b/tsconfig.refs.json index e08b50cc055c1..bbaf18d29ae2a 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -63,7 +63,6 @@ { "path": "./x-pack/plugins/canvas/tsconfig.json" }, { "path": "./x-pack/plugins/cases/tsconfig.json" }, { "path": "./x-pack/plugins/cloud/tsconfig.json" }, - { "path": "./x-pack/plugins/console_extensions/tsconfig.json" }, { "path": "./x-pack/plugins/dashboard_enhanced/tsconfig.json" }, { "path": "./x-pack/plugins/data_enhanced/tsconfig.json" }, { "path": "./x-pack/plugins/dashboard_mode/tsconfig.json" }, diff --git a/typings/@emotion/index.d.ts b/typings/@emotion/index.d.ts new file mode 100644 index 0000000000000..2a5e63a3e29ef --- /dev/null +++ b/typings/@emotion/index.d.ts @@ -0,0 +1,12 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// Stub @emotion/core +// Remove when @storybook has moved to @emotion v11 +// https://github.com/storybookjs/storybook/issues/13145 +export {}; diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts index 4e4cd4419a5a2..5e3dd2019d0a0 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts @@ -16,12 +16,13 @@ const createAlertingAuthorizationMock = () => { ensureAuthorized: jest.fn(), filterByRuleTypeAuthorization: jest.fn(), getFindAuthorizationFilter: jest.fn(), + getAugmentedRuleTypesWithAuthorization: jest.fn(), }; return mocked; }; export const alertingAuthorizationMock: { - create: () => AlertingAuthorizationMock; + create: () => jest.Mocked>; } = { create: createAlertingAuthorizationMock, }; diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts index c07148f03c684..4b1fc7f1a7ccb 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts @@ -1944,4 +1944,184 @@ describe('AlertingAuthorization', () => { `); }); }); + + describe('getAugmentedRuleTypesWithAuthorization', () => { + const myOtherAppAlertType: RegistryAlertType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + recoveryActionGroup: RecoveredActionGroup, + id: 'myOtherAppAlertType', + name: 'myOtherAppAlertType', + producer: 'alerts', + enabledInLicense: true, + isExportable: true, + }; + const myAppAlertType: RegistryAlertType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + recoveryActionGroup: RecoveredActionGroup, + id: 'myAppAlertType', + name: 'myAppAlertType', + producer: 'myApp', + enabledInLicense: true, + isExportable: true, + }; + const mySecondAppAlertType: RegistryAlertType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + recoveryActionGroup: RecoveredActionGroup, + id: 'mySecondAppAlertType', + name: 'mySecondAppAlertType', + producer: 'myApp', + enabledInLicense: true, + isExportable: true, + }; + const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType, mySecondAppAlertType]); + + test('it returns authorized rule types given a set of feature ids', async () => { + const { authorization } = mockSecurity(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'find'), + authorized: true, + }, + ], + }, + }); + const alertAuthorization = new AlertingAuthorization({ + request, + authorization, + alertTypeRegistry, + features, + auditLogger, + getSpace, + exemptConsumerIds, + }); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + await expect( + alertAuthorization.getAugmentedRuleTypesWithAuthorization( + ['myApp'], + [ReadOperations.Find, ReadOperations.Get, WriteOperations.Update], + AlertingAuthorizationEntity.Alert + ) + ).resolves.toMatchInlineSnapshot(` + Object { + "authorizedRuleTypes": Set { + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "myApp": Object { + "all": false, + "read": true, + }, + }, + "defaultActionGroupId": "default", + "enabledInLicense": true, + "id": "myOtherAppAlertType", + "isExportable": true, + "minimumLicenseRequired": "basic", + "name": "myOtherAppAlertType", + "producer": "alerts", + "recoveryActionGroup": Object { + "id": "recovered", + "name": "Recovered", + }, + }, + }, + "hasAllRequested": false, + "username": "some-user", + } + `); + }); + + test('it returns all authorized if user has read, get and update alert privileges', async () => { + const { authorization } = mockSecurity(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'update'), + authorized: true, + }, + ], + }, + }); + const alertAuthorization = new AlertingAuthorization({ + request, + authorization, + alertTypeRegistry, + features, + auditLogger, + getSpace, + exemptConsumerIds, + }); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + await expect( + alertAuthorization.getAugmentedRuleTypesWithAuthorization( + ['myApp'], + [ReadOperations.Find, ReadOperations.Get, WriteOperations.Update], + AlertingAuthorizationEntity.Alert + ) + ).resolves.toMatchInlineSnapshot(` + Object { + "authorizedRuleTypes": Set { + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "myApp": Object { + "all": true, + "read": true, + }, + }, + "defaultActionGroupId": "default", + "enabledInLicense": true, + "id": "myOtherAppAlertType", + "isExportable": true, + "minimumLicenseRequired": "basic", + "name": "myOtherAppAlertType", + "producer": "alerts", + "recoveryActionGroup": Object { + "id": "recovered", + "name": "Recovered", + }, + }, + }, + "hasAllRequested": false, + "username": "some-user", + } + `); + }); + }); }); diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts index 52cef9a402e35..50a1b9d84ff6d 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts @@ -124,20 +124,41 @@ export class AlertingAuthorization { return new Set(); }); - this.allPossibleConsumers = this.featuresIds.then((featuresIds) => - featuresIds.size + this.allPossibleConsumers = this.featuresIds.then((featuresIds) => { + return featuresIds.size ? asAuthorizedConsumers([...this.exemptConsumerIds, ...featuresIds], { read: true, all: true, }) - : {} - ); + : {}; + }); } private shouldCheckAuthorization(): boolean { return this.authorization?.mode?.useRbacForRequest(this.request) ?? false; } + /* + * This method exposes the private 'augmentRuleTypesWithAuthorization' to be + * used by the RAC/Alerts client + */ + public async getAugmentedRuleTypesWithAuthorization( + featureIds: readonly string[], + operations: Array, + authorizationEntity: AlertingAuthorizationEntity + ): Promise<{ + username?: string; + hasAllRequested: boolean; + authorizedRuleTypes: Set; + }> { + return this.augmentRuleTypesWithAuthorization( + this.alertTypeRegistry.list(), + operations, + authorizationEntity, + new Set(featureIds) + ); + } + public async ensureAuthorized({ ruleTypeId, consumer, operation, entity }: EnsureAuthorizedOpts) { const { authorization } = this; @@ -339,13 +360,14 @@ export class AlertingAuthorization { private async augmentRuleTypesWithAuthorization( ruleTypes: Set, operations: Array, - authorizationEntity: AlertingAuthorizationEntity + authorizationEntity: AlertingAuthorizationEntity, + featuresIds?: Set ): Promise<{ username?: string; hasAllRequested: boolean; authorizedRuleTypes: Set; }> { - const featuresIds = await this.featuresIds; + const fIds = featuresIds ?? (await this.featuresIds); if (this.authorization && this.shouldCheckAuthorization()) { const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest( this.request @@ -363,7 +385,7 @@ export class AlertingAuthorization { // as we can't ask ES for the user's individual privileges we need to ask for each feature // and ruleType in the system whether this user has this privilege for (const ruleType of ruleTypesWithAuthorization) { - for (const feature of featuresIds) { + for (const feature of fIds) { for (const operation of operations) { privilegeToRuleType.set( this.authorization!.actions.alerting.get( @@ -420,7 +442,7 @@ export class AlertingAuthorization { return { hasAllRequested: true, authorizedRuleTypes: this.augmentWithAuthorizedConsumers( - new Set([...ruleTypes].filter((ruleType) => featuresIds.has(ruleType.producer))), + new Set([...ruleTypes].filter((ruleType) => fIds.has(ruleType.producer))), await this.allPossibleConsumers ), }; diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerting/server/index.ts index 72e3325107f31..957bd89f52f36 100644 --- a/x-pack/plugins/alerting/server/index.ts +++ b/x-pack/plugins/alerting/server/index.ts @@ -34,6 +34,13 @@ export { FindResult } from './alerts_client'; export { PublicAlertInstance as AlertInstance } from './alert_instance'; export { parseDuration } from './lib'; export { getEsErrorMessage } from './lib/errors'; +export { + ReadOperations, + AlertingAuthorizationFilterType, + AlertingAuthorization, + WriteOperations, + AlertingAuthorizationEntity, +} from './authorization'; export const plugin = (initContext: PluginInitializerContext) => new AlertingPlugin(initContext); diff --git a/x-pack/plugins/apm/.storybook/jest_setup.js b/x-pack/plugins/apm/.storybook/jest_setup.js new file mode 100644 index 0000000000000..32071b8aa3f62 --- /dev/null +++ b/x-pack/plugins/apm/.storybook/jest_setup.js @@ -0,0 +1,11 @@ +/* + * 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 { setGlobalConfig } from '@storybook/testing-react'; +import * as globalStorybookConfig from './preview'; + +setGlobalConfig(globalStorybookConfig); diff --git a/x-pack/plugins/apm/.storybook/preview.js b/x-pack/plugins/apm/.storybook/preview.js new file mode 100644 index 0000000000000..18343c15a6465 --- /dev/null +++ b/x-pack/plugins/apm/.storybook/preview.js @@ -0,0 +1,10 @@ +/* + * 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 { EuiThemeProviderDecorator } from '../../../../src/plugins/kibana_react/common'; + +export const decorators = [EuiThemeProviderDecorator]; diff --git a/x-pack/plugins/apm/common/alert_types.ts b/x-pack/plugins/apm/common/alert_types.ts index ad233c7f6df92..9476396a7aefa 100644 --- a/x-pack/plugins/apm/common/alert_types.ts +++ b/x-pack/plugins/apm/common/alert_types.ts @@ -10,6 +10,8 @@ import type { ValuesType } from 'utility-types'; import type { ActionGroup } from '../../alerting/common'; import { ANOMALY_SEVERITY, ANOMALY_THRESHOLD } from './ml_constants'; +export const APM_SERVER_FEATURE_ID = 'apm'; + export enum AlertType { ErrorCount = 'apm.error_rate', // ErrorRate was renamed to ErrorCount but the key is kept as `error_rate` for backwards-compat. TransactionErrorRate = 'apm.transaction_error_rate', @@ -44,7 +46,7 @@ export const ALERT_TYPES_CONFIG: Record< actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: THRESHOLD_MET_GROUP_ID, minimumLicenseRequired: 'basic', - producer: 'apm', + producer: APM_SERVER_FEATURE_ID, isExportable: true, }, [AlertType.TransactionDuration]: { @@ -54,7 +56,7 @@ export const ALERT_TYPES_CONFIG: Record< actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: THRESHOLD_MET_GROUP_ID, minimumLicenseRequired: 'basic', - producer: 'apm', + producer: APM_SERVER_FEATURE_ID, isExportable: true, }, [AlertType.TransactionDurationAnomaly]: { @@ -64,7 +66,7 @@ export const ALERT_TYPES_CONFIG: Record< actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: THRESHOLD_MET_GROUP_ID, minimumLicenseRequired: 'basic', - producer: 'apm', + producer: APM_SERVER_FEATURE_ID, isExportable: true, }, [AlertType.TransactionErrorRate]: { @@ -74,7 +76,7 @@ export const ALERT_TYPES_CONFIG: Record< actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: THRESHOLD_MET_GROUP_ID, minimumLicenseRequired: 'basic', - producer: 'apm', + producer: APM_SERVER_FEATURE_ID, isExportable: true, }, }; diff --git a/x-pack/plugins/apm/jest.config.js b/x-pack/plugins/apm/jest.config.js index caa8256cdb7ea..5bce9bbfb5b1b 100644 --- a/x-pack/plugins/apm/jest.config.js +++ b/x-pack/plugins/apm/jest.config.js @@ -11,4 +11,6 @@ module.exports = { preset: '@kbn/test', rootDir: path.resolve(__dirname, '../../..'), roots: ['/x-pack/plugins/apm'], + setupFiles: ['/x-pack/plugins/apm/.storybook/jest_setup.js'], + testPathIgnorePatterns: ['/x-pack/plugins/apm/e2e/'], }; diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json index ae4510b10acd4..9f661f13a491e 100644 --- a/x-pack/plugins/apm/kibana.json +++ b/x-pack/plugins/apm/kibana.json @@ -7,7 +7,6 @@ "data", "embeddable", "features", - "fleet", "infra", "licensing", "observability", @@ -24,11 +23,15 @@ "security", "spaces", "taskManager", - "usageCollection" + "usageCollection", + "fleet" ], "server": true, "ui": true, - "configPath": ["xpack", "apm"], + "configPath": [ + "xpack", + "apm" + ], "requiredBundles": [ "fleet", "home", @@ -38,4 +41,4 @@ "ml", "observability" ] -} +} \ No newline at end of file diff --git a/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx b/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx index 35863d8099394..eef3271d5932d 100644 --- a/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx @@ -6,11 +6,15 @@ */ import React, { useCallback, useMemo } from 'react'; -import { useParams } from 'react-router-dom'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { AlertType } from '../../../../common/alert_types'; +import { + AlertType, + APM_SERVER_FEATURE_ID, +} from '../../../../common/alert_types'; import { getInitialAlertValues } from '../get_initial_alert_values'; import { ApmPluginStartDeps } from '../../../plugin'; +import { useServiceName } from '../../../hooks/use_service_name'; +import { ApmServiceContextProvider } from '../../../context/apm_service/apm_service_context'; interface Props { addFlyoutVisible: boolean; setAddFlyoutVisibility: React.Dispatch>; @@ -19,7 +23,7 @@ interface Props { export function AlertingFlyout(props: Props) { const { addFlyoutVisible, setAddFlyoutVisibility, alertType } = props; - const { serviceName } = useParams<{ serviceName?: string }>(); + const serviceName = useServiceName(); const { services } = useKibana(); const initialValues = getInitialAlertValues(alertType, serviceName); @@ -31,7 +35,7 @@ export function AlertingFlyout(props: Props) { () => alertType && services.triggersActionsUi.getAddAlertFlyout({ - consumer: 'apm', + consumer: APM_SERVER_FEATURE_ID, onClose: onCloseAddFlyout, alertTypeId: alertType, canChangeTrigger: false, @@ -40,5 +44,11 @@ export function AlertingFlyout(props: Props) { /* eslint-disable-next-line react-hooks/exhaustive-deps */ [alertType, onCloseAddFlyout, services.triggersActionsUi] ); - return <>{addFlyoutVisible && addAlertFlyout}; + return ( + <> + {addFlyoutVisible && ( + {addAlertFlyout} + )} + + ); } diff --git a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.stories.tsx b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.stories.tsx index 83874e9584510..23afb9646dea7 100644 --- a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.stories.tsx +++ b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.stories.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import { ErrorCountAlertTrigger } from '.'; -import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; import { mockApmPluginContextValue, @@ -20,19 +19,15 @@ export default { component: ErrorCountAlertTrigger, decorators: [ (Story: React.ComponentClass) => ( - - - -
- -
-
-
-
+ + +
+ +
+
+
), ], }; diff --git a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx index fdfed6eb0d685..811353067ab60 100644 --- a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; -import { useParams } from 'react-router-dom'; +import { defaults } from 'lodash'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { asInteger } from '../../../../common/utils/formatters'; @@ -18,6 +18,7 @@ import { ChartPreview } from '../chart_preview'; import { EnvironmentField, IsAboveField, ServiceField } from '../fields'; import { getAbsoluteTimeRange } from '../helper'; import { ServiceAlertTrigger } from '../service_alert_trigger'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; export interface AlertParams { windowSize: number; @@ -35,49 +36,55 @@ interface Props { export function ErrorCountAlertTrigger(props: Props) { const { setAlertParams, setAlertProperty, alertParams } = props; - const { serviceName } = useParams<{ serviceName?: string }>(); + + const { serviceName: serviceNameFromContext } = useApmServiceContext(); + const { urlParams } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, environment: environmentFromUrl } = urlParams; const { environmentOptions } = useEnvironmentsFetcher({ - serviceName, + serviceName: serviceNameFromContext, start, end, }); - const { threshold, windowSize, windowUnit, environment } = alertParams; + const params = defaults( + { + ...alertParams, + }, + { + threshold: 25, + windowSize: 1, + windowUnit: 'm', + environment: environmentFromUrl || ENVIRONMENT_ALL.value, + serviceName: serviceNameFromContext, + } + ); const { data } = useFetcher( (callApmApi) => { - if (windowSize && windowUnit) { + if (params.windowSize && params.windowUnit) { return callApmApi({ endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_count', params: { query: { - ...getAbsoluteTimeRange(windowSize, windowUnit), - environment, - serviceName, + ...getAbsoluteTimeRange(params.windowSize, params.windowUnit), + environment: params.environment, + serviceName: params.serviceName, }, }, }); } }, - [windowSize, windowUnit, environment, serviceName] + [ + params.windowSize, + params.windowUnit, + params.environment, + params.serviceName, + ] ); - const defaults = { - threshold: 25, - windowSize: 1, - windowUnit: 'm', - environment: urlParams.environment || ENVIRONMENT_ALL.value, - }; - - const params = { - ...defaults, - ...alertParams, - }; - const fields = [ - , + , ); return ( void; @@ -18,13 +17,10 @@ interface Props { } export function ServiceAlertTrigger(props: Props) { - const { serviceName } = useParams<{ serviceName?: string }>(); - const { fields, setAlertParams, defaults, chartPreview } = props; const params: Record = { ...defaults, - serviceName, }; useEffect(() => { diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx index b4c78b54f329b..8f2713685127e 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx @@ -7,9 +7,8 @@ import { EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { map } from 'lodash'; +import { map, defaults } from 'lodash'; import React from 'react'; -import { useParams } from 'react-router-dom'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { getDurationFormatter } from '../../../../common/utils/formatters'; @@ -72,46 +71,60 @@ interface Props { export function TransactionDurationAlertTrigger(props: Props) { const { setAlertParams, alertParams, setAlertProperty } = props; const { urlParams } = useUrlParams(); - const { transactionTypes, transactionType } = useApmServiceContext(); - const { serviceName } = useParams<{ serviceName?: string }>(); - const { start, end } = urlParams; + + const { start, end, environment: environmentFromUrl } = urlParams; + + const { + transactionTypes, + transactionType: transactionTypeFromContext, + serviceName: serviceNameFromContext, + } = useApmServiceContext(); + + const params = defaults( + { + ...alertParams, + }, + { + aggregationType: 'avg', + environment: environmentFromUrl || ENVIRONMENT_ALL.value, + threshold: 1500, + windowSize: 5, + windowUnit: 'm', + transactionType: transactionTypeFromContext, + serviceName: serviceNameFromContext, + } + ); + const { environmentOptions } = useEnvironmentsFetcher({ - serviceName, + serviceName: params.serviceName, start, end, }); - const { - aggregationType, - environment, - threshold, - windowSize, - windowUnit, - } = alertParams; const { data } = useFetcher( (callApmApi) => { - if (windowSize && windowUnit) { + if (params.windowSize && params.windowUnit) { return callApmApi({ endpoint: 'GET /api/apm/alerts/chart_preview/transaction_duration', params: { query: { - ...getAbsoluteTimeRange(windowSize, windowUnit), - aggregationType, - environment, - serviceName, - transactionType: alertParams.transactionType, + ...getAbsoluteTimeRange(params.windowSize, params.windowUnit), + aggregationType: params.aggregationType, + environment: params.environment, + serviceName: params.serviceName, + transactionType: params.transactionType, }, }, }); } }, [ - aggregationType, - environment, - serviceName, - alertParams.transactionType, - windowSize, - windowUnit, + params.aggregationType, + params.environment, + params.serviceName, + params.transactionType, + params.windowSize, + params.windowUnit, ] ); @@ -122,7 +135,7 @@ export function TransactionDurationAlertTrigger(props: Props) { const yTickFormat = getResponseTimeTickFormatter(formatter); // The threshold from the form is in ms. Convert to µs. - const thresholdMs = threshold * 1000; + const thresholdMs = params.threshold * 1000; const chartPreview = ( ); - if (!transactionTypes.length || !serviceName) { + if (!transactionTypes.length || !params.serviceName) { return null; } - const defaults = { - threshold: 1500, - aggregationType: 'avg', - windowSize: 5, - windowUnit: 'm', - transactionType, - environment: urlParams.environment || ENVIRONMENT_ALL.value, - }; - - const params = { - ...defaults, - ...alertParams, - }; - const fields = [ - , + , ({ text: key, value: key }))} @@ -206,7 +205,7 @@ export function TransactionDurationAlertTrigger(props: Props) { return ( void; setAlertProperty: (key: string, value: any) => void; } @@ -47,35 +47,36 @@ interface Props { export function TransactionDurationAnomalyAlertTrigger(props: Props) { const { setAlertParams, alertParams, setAlertProperty } = props; const { urlParams } = useUrlParams(); - const { transactionTypes } = useApmServiceContext(); - const { serviceName } = useParams<{ serviceName?: string }>(); - const { start, end, transactionType } = urlParams; + const { + serviceName: serviceNameFromContext, + transactionType: transactionTypeFromContext, + transactionTypes, + } = useApmServiceContext(); + + const { start, end, environment: environmentFromUrl } = urlParams; + + const params = defaults( + { + ...alertParams, + }, + { + windowSize: 15, + windowUnit: 'm', + transactionType: transactionTypeFromContext, + environment: environmentFromUrl || ENVIRONMENT_ALL.value, + anomalySeverityType: ANOMALY_SEVERITY.CRITICAL, + serviceName: serviceNameFromContext, + } + ); + const { environmentOptions } = useEnvironmentsFetcher({ - serviceName, + serviceName: params.serviceName, start, end, }); - if (serviceName && !transactionTypes.length) { - return null; - } - - const defaults: Params = { - windowSize: 15, - windowUnit: 'm', - transactionType: transactionType || transactionTypes[0], - serviceName, - environment: urlParams.environment || ENVIRONMENT_ALL.value, - anomalySeverityType: ANOMALY_SEVERITY.CRITICAL, - }; - - const params = { - ...defaults, - ...alertParams, - }; - const fields = [ - , + , ({ text: key, value: key }))} @@ -107,7 +108,7 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) { return ( diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx index c6f9c4efd98b6..4eb0b0e797571 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { useParams } from 'react-router-dom'; +import { defaults } from 'lodash'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { asPercent } from '../../../../common/utils/formatters'; @@ -42,63 +42,65 @@ interface Props { export function TransactionErrorRateAlertTrigger(props: Props) { const { setAlertParams, alertParams, setAlertProperty } = props; const { urlParams } = useUrlParams(); - const { transactionTypes } = useApmServiceContext(); - const { serviceName } = useParams<{ serviceName?: string }>(); - const { start, end, transactionType } = urlParams; + const { + transactionType: transactionTypeFromContext, + transactionTypes, + serviceName: serviceNameFromContext, + } = useApmServiceContext(); + + const { start, end, environment: environmentFromUrl } = urlParams; + + const params = defaults, AlertParams>( + { + threshold: 30, + windowSize: 5, + windowUnit: 'm', + transactionType: transactionTypeFromContext, + environment: environmentFromUrl || ENVIRONMENT_ALL.value, + serviceName: serviceNameFromContext, + }, + alertParams + ); + const { environmentOptions } = useEnvironmentsFetcher({ - serviceName, + serviceName: serviceNameFromContext, start, end, }); - const { threshold, windowSize, windowUnit, environment } = alertParams; - - const thresholdAsPercent = (threshold ?? 0) / 100; + const thresholdAsPercent = (params.threshold ?? 0) / 100; const { data } = useFetcher( (callApmApi) => { - if (windowSize && windowUnit) { + if (params.windowSize && params.windowUnit) { return callApmApi({ endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_rate', params: { query: { - ...getAbsoluteTimeRange(windowSize, windowUnit), - environment, - serviceName, - transactionType: alertParams.transactionType, + ...getAbsoluteTimeRange(params.windowSize, params.windowUnit), + environment: params.environment, + serviceName: params.serviceName, + transactionType: params.transactionType, }, }, }); } }, [ - alertParams.transactionType, - environment, - serviceName, - windowSize, - windowUnit, + params.transactionType, + params.environment, + params.serviceName, + params.windowSize, + params.windowUnit, ] ); - if (serviceName && !transactionTypes.length) { + if (params.serviceName && !transactionTypes.length) { return null; } - const defaultParams = { - threshold: 30, - windowSize: 5, - windowUnit: 'm', - transactionType: transactionType || transactionTypes[0], - environment: urlParams.environment || ENVIRONMENT_ALL.value, - }; - - const params = { - ...defaultParams, - ...alertParams, - }; - const fields = [ - , + , ({ text: key, value: key }))} @@ -141,7 +143,7 @@ export function TransactionErrorRateAlertTrigger(props: Props) { return ( - - - {ANALYZE_MESSAGE}

}> - - {ANALYZE_DATA} - -
-
- - + {ANALYZE_MESSAGE}

}> + - {i18n.translate('xpack.apm.addDataButtonLabel', { - defaultMessage: 'Add data', - })} -
-
-
+ {ANALYZE_DATA} +
+ + + {i18n.translate('xpack.apm.addDataButtonLabel', { + defaultMessage: 'Add data', + })} + + ); } diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx index 1aad25fc89c0b..8263db648cd39 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__stories__/MapTooltip.stories.tsx @@ -7,53 +7,50 @@ import { storiesOf } from '@storybook/react'; import React from 'react'; -import { EuiThemeProvider } from '../../../../../../../../../src/plugins/kibana_react/common'; import { MapToolTip } from '../MapToolTip'; import { COUNTRY_NAME, TRANSACTION_DURATION_COUNTRY } from '../useLayerList'; -storiesOf('app/RumDashboard/VisitorsRegionMap', module) - .addDecorator((storyFn) => {storyFn()}) - .add( - 'Tooltip', - () => { - const loadFeatureProps = async () => { - return [ +storiesOf('app/RumDashboard/VisitorsRegionMap', module).add( + 'Tooltip', + () => { + const loadFeatureProps = async () => { + return [ + { + getPropertyKey: () => COUNTRY_NAME, + getRawValue: () => 'United States', + }, + { + getPropertyKey: () => TRANSACTION_DURATION_COUNTRY, + getRawValue: () => 2434353, + }, + ]; + }; + return ( + COUNTRY_NAME, - getRawValue: () => 'United States', - }, - { - getPropertyKey: () => TRANSACTION_DURATION_COUNTRY, - getRawValue: () => 2434353, - }, - ]; - }; - return ( - - ); + actions: [], + }, + ]} + /> + ); + }, + { + info: { + propTables: false, + source: false, }, - { - info: { - propTables: false, - source: false, - }, - } - ); + } +); diff --git a/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/index.stories.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/index.stories.tsx index cd5fa5db89a31..02ecf902f00a3 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/index.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/AgentConfigurationCreateEdit/index.stories.tsx @@ -8,7 +8,6 @@ import { storiesOf } from '@storybook/react'; import React from 'react'; import { CoreStart } from 'kibana/public'; -import { EuiThemeProvider } from '../../../../../../../../../src/plugins/kibana_react/common'; import { AgentConfiguration } from '../../../../../../common/agent_configuration/configuration_types'; import { FETCH_STATUS } from '../../../../../hooks/use_fetcher'; import { createCallApmApi } from '../../../../../services/rest/createCallApmApi'; @@ -37,13 +36,11 @@ storiesOf( }; return ( - - - {storyFn()} - - + + {storyFn()} + ); }) .add( @@ -67,7 +64,6 @@ storiesOf( propTablesExclude: [ AgentConfigurationCreateEdit, ApmPluginContext.Provider, - EuiThemeProvider, ], source: false, }, diff --git a/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/index.tsx index 1ca7f46a0b26f..32c93e43175df 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/index.tsx @@ -18,7 +18,6 @@ import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import React from 'react'; import { useLocation } from 'react-router-dom'; -import { useTrackPageview } from '../../../../../../observability/public'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { useFetcher } from '../../../../hooks/use_fetcher'; import { createAgentConfigurationHref } from '../../../shared/Links/apm/agentConfigurationLinks'; @@ -34,9 +33,6 @@ export function AgentConfigurations() { { preservePreviousData: false, showToastOnError: false } ); - useTrackPageview({ app: 'apm', path: 'agent_configuration' }); - useTrackPageview({ app: 'apm', path: 'agent_configuration', delay: 15000 }); - const hasConfigurations = !isEmpty(data.configurations); return ( diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/confirm_switch_modal.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/confirm_switch_modal.tsx index 9900093253d2a..5bb93c251777f 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/schema/confirm_switch_modal.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/schema/confirm_switch_modal.tsx @@ -15,20 +15,20 @@ import { htmlIdGenerator, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useUiTracker } from '../../../../../../observability/public'; import { ElasticDocsLink } from '../../../shared/Links/ElasticDocsLink'; interface Props { onConfirm: () => void; onCancel: () => void; unsupportedConfigs: Array<{ key: string; value: string }>; - isLoading: boolean; } export function ConfirmSwitchModal({ onConfirm, onCancel, unsupportedConfigs, - isLoading, }: Props) { + const trackApmEvent = useUiTracker({ app: 'apm' }); const [isConfirmChecked, setIsConfirmChecked] = useState(false); const hasUnsupportedConfigs = !!unsupportedConfigs.length; return ( @@ -50,9 +50,13 @@ export function ConfirmSwitchModal({ } )} defaultFocusedButton="confirm" - onConfirm={onConfirm} + onConfirm={() => { + trackApmEvent({ + metric: 'confirm_data_stream_switch', + }); + onConfirm(); + }} confirmButtonDisabled={!isConfirmChecked} - isLoading={isLoading} >

{i18n.translate('xpack.apm.settings.schema.confirm.descriptionText', { @@ -135,7 +139,6 @@ export function ConfirmSwitchModal({ onChange={(e) => { setIsConfirmChecked(e.target.checked); }} - disabled={isLoading} />

diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/index.tsx index fee072470f05a..5a67ce28e9e1a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/schema/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/schema/index.tsx @@ -8,6 +8,8 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { NotificationsStart } from 'kibana/public'; +import moment from 'moment'; +import { useLocalStorage } from '../../../../hooks/useLocalStorage'; import { SchemaOverview } from './schema_overview'; import { ConfirmSwitchModal } from './confirm_switch_modal'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; @@ -19,10 +21,22 @@ import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plug type FleetMigrationCheckResponse = APIReturnType<'GET /api/apm/fleet/migration_check'>; +const APM_DATA_STREAMS_MIGRATION_STATUS_LS = { + value: '', + expiry: '', +}; + export function Schema() { + const [ + apmDataStreamsMigrationStatus, + setApmDataStreamsMigrationStatus, + ] = useLocalStorage( + 'apm.dataStreamsMigrationStatus', + APM_DATA_STREAMS_MIGRATION_STATUS_LS + ); + const { toasts } = useApmPluginContext().core.notifications; const [isSwitchActive, setIsSwitchActive] = useState(false); - const [isLoadingMigration, setIsLoadingMigration] = useState(false); const [isLoadingConfirmation, setIsLoadingConfirmation] = useState(false); const [unsupportedConfigs, setUnsupportedConfigs] = useState< Array<{ key: string; value: any }> @@ -42,6 +56,19 @@ export function Schema() { const hasCloudAgentPolicy = !!data.has_cloud_agent_policy; const hasCloudApmPackagePolicy = !!data.has_cloud_apm_package_policy; const hasRequiredRole = !!data.has_required_role; + + function updateLocalStorage(newStatus: FETCH_STATUS) { + setApmDataStreamsMigrationStatus({ + value: newStatus, + expiry: moment().add(5, 'minutes').toISOString(), + }); + } + + const { value: localStorageValue, expiry } = apmDataStreamsMigrationStatus; + const isMigrating = + localStorageValue === FETCH_STATUS.LOADING && + moment(expiry).valueOf() > moment.now(); + return ( <> {isSwitchActive && ( { - setIsLoadingMigration(true); - const apmPackagePolicy = await createCloudApmPackagePolicy(toasts); + setIsSwitchActive(false); + const apmPackagePolicy = await createCloudApmPackagePolicy( + toasts, + updateLocalStorage + ); if (!apmPackagePolicy) { - setIsLoadingMigration(false); return; } - setIsSwitchActive(false); refetch(); }} onCancel={() => { - if (isLoadingMigration) { - return; - } setIsSwitchActive(false); }} unsupportedConfigs={unsupportedConfigs} @@ -112,8 +137,10 @@ async function getUnsupportedApmServerConfigs( } async function createCloudApmPackagePolicy( - toasts: NotificationsStart['toasts'] + toasts: NotificationsStart['toasts'], + updateLocalStorage: (status: FETCH_STATUS) => void ) { + updateLocalStorage(FETCH_STATUS.LOADING); try { const { cloud_apm_package_policy: cloudApmPackagePolicy, @@ -121,8 +148,10 @@ async function createCloudApmPackagePolicy( endpoint: 'POST /api/apm/fleet/cloud_apm_package_policy', signal: null, }); + updateLocalStorage(FETCH_STATUS.SUCCESS); return cloudApmPackagePolicy; } catch (error) { + updateLocalStorage(FETCH_STATUS.FAILURE); toasts.addDanger({ title: i18n.translate( 'xpack.apm.settings.createApmPackagePolicy.errorToast.title', diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/migration_in_progress_panel.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/migration_in_progress_panel.tsx new file mode 100644 index 0000000000000..854d1dd823d23 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/schema/migration_in_progress_panel.tsx @@ -0,0 +1,36 @@ +/* + * 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 } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { EuiCard } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +export function MigrationInProgressPanel() { + return ( + + + } + title={i18n.translate( + 'xpack.apm.settings.schema.migrationInProgressPanelTitle', + { defaultMessage: 'Switching to data streams...' } + )} + description={i18n.translate( + 'xpack.apm.settings.schema.migrationInProgressPanelDescription', + { + defaultMessage: + "We're now creating a Fleet Server instance to contain the new APM Server while shutting down the old APM server instance. Within minutes you should see your data pour into the app again.", + } + )} + /> + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/schema.stories.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/schema.stories.tsx index 7cac4ba97e723..b22260ffabe46 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/schema/schema.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/schema/schema.stories.tsx @@ -5,31 +5,99 @@ * 2.0. */ -import type { Story } from '@storybook/react'; +import type { Meta, Story } from '@storybook/react'; import React, { ComponentType } from 'react'; +import { MemoryRouter } from 'react-router-dom'; import { CoreStart } from '../../../../../../../../src/core/public'; import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import { createCallApmApi } from '../../../../services/rest/createCallApmApi'; import { Schema } from './'; +interface Args { + hasCloudAgentPolicy: boolean; + hasCloudApmPackagePolicy: boolean; + cloudApmMigrationEnabled: boolean; + hasRequiredRole: boolean; + isMigrating: boolean; +} + export default { title: 'app/Settings/Schema', component: Schema, + argTypes: { + hasCloudAgentPolicy: { + control: { + type: 'boolean', + options: [true, false], + defaultValue: true, + }, + }, + hasCloudApmPackagePolicy: { + control: { + type: 'boolean', + options: [true, false], + defaultValue: false, + }, + }, + cloudApmMigrationEnabled: { + control: { + type: 'boolean', + options: [true, false], + defaultValue: true, + }, + }, + hasRequiredRole: { + control: { + type: 'boolean', + options: [true, false], + defaultValue: true, + }, + }, + isMigrating: { + control: { + type: 'boolean', + options: [true, false], + defaultValue: false, + }, + }, + }, decorators: [ - (StoryComponent: ComponentType) => { + (StoryComponent: ComponentType, { args }: Meta) => { + if (args?.isMigrating) { + const expiryDate = new Date(); + expiryDate.setMinutes(expiryDate.getMinutes() + 5); + window.localStorage.setItem( + 'apm.dataStreamsMigrationStatus', + JSON.stringify({ + value: 'loading', + expiry: expiryDate.toISOString(), + }) + ); + } else { + window.localStorage.removeItem('apm.dataStreamsMigrationStatus'); + } const coreMock = ({ http: { + basePath: { prepend: () => {} }, get: () => { - return {}; + return { + has_cloud_agent_policy: args?.hasCloudAgentPolicy, + has_cloud_apm_package_policy: args?.hasCloudApmPackagePolicy, + cloud_apm_migration_enabled: args?.cloudApmMigrationEnabled, + has_required_role: args?.hasRequiredRole, + }; }, }, + uiSettings: { get: () => '' }, } as unknown) as CoreStart; createCallApmApi(coreMock); return ( - + + + ); }, diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx index 7a874ed5b8037..a9a0b824cc828 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx @@ -7,7 +7,6 @@ import { EuiButton, - EuiCallOut, EuiCard, EuiFlexGroup, EuiFlexItem, @@ -24,9 +23,11 @@ import { APMLink } from '../../../shared/Links/apm/APMLink'; import { ElasticDocsLink } from '../../../shared/Links/ElasticDocsLink'; import { useFleetCloudAgentPolicyHref } from '../../../shared/Links/kibana'; import rocketLaunchGraphic from './blog-rocket-720x420.png'; +import { MigrationInProgressPanel } from './migration_in_progress_panel'; interface Props { onSwitch: () => void; + isMigrating: boolean; isMigrated: boolean; isLoading: boolean; isLoadingConfirmation: boolean; @@ -36,6 +37,7 @@ interface Props { } export function SchemaOverview({ onSwitch, + isMigrating, isMigrated, isLoading, isLoadingConfirmation, @@ -58,6 +60,15 @@ export function SchemaOverview({ ); } + if (isMigrating && !isMigrated) { + return ( + <> + + + + ); + } + if (isMigrated) { return ( <> @@ -125,7 +136,7 @@ export function SchemaOverview({ - + } title={i18n.translate( @@ -154,7 +165,7 @@ export function SchemaOverview({ } /> - + - - - - - -

- {i18n.translate( - 'xpack.apm.settings.schema.migrate.calloutNote.message', - { - defaultMessage: - 'If you have custom dashboards, machine learning jobs, or source maps that use classic APM indices, you must reconfigure them for data streams.', - } - )} -

-
-
- -
); } diff --git a/x-pack/plugins/apm/public/components/app/correlations/index.tsx b/x-pack/plugins/apm/public/components/app/correlations/index.tsx index 2b32ece14e5cd..9ad5088bb0bcf 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/index.tsx @@ -104,16 +104,7 @@ export function Correlations() { width: '20%', }); } - if (urlParams.transactionName) { - properties.push({ - label: i18n.translate('xpack.apm.correlations.transactionLabel', { - defaultMessage: 'Transaction', - }), - fieldName: TRANSACTION_NAME, - val: urlParams.transactionName, - width: '20%', - }); - } + if (urlParams.environment) { properties.push({ label: i18n.translate('xpack.apm.correlations.environmentLabel', { @@ -125,6 +116,17 @@ export function Correlations() { }); } + if (urlParams.transactionName) { + properties.push({ + label: i18n.translate('xpack.apm.correlations.transactionLabel', { + defaultMessage: 'Transaction', + }), + fieldName: TRANSACTION_NAME, + val: urlParams.transactionName, + width: '20%', + }); + } + return properties; }, [serviceName, urlParams.environment, urlParams.transactionName]); diff --git a/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations.tsx index f9536353747ee..4bd20f51977c6 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations.tsx @@ -36,6 +36,7 @@ import { useCorrelations } from './use_correlations'; import { push } from '../../shared/Links/url_helpers'; import { useUiTracker } from '../../../../../observability/public'; import { asPreciseDecimal } from '../../../../common/utils/formatters'; +import { LatencyCorrelationsHelpPopover } from './ml_latency_correlations_help_popover'; const DEFAULT_PERCENTILE_THRESHOLD = 95; const isErrorMessage = (arg: unknown): arg is Error => { @@ -151,7 +152,7 @@ export function MlLatencyCorrelations({ onClose }: Props) { 'xpack.apm.correlations.latencyCorrelations.correlationsTable.correlationColumnDescription', { defaultMessage: - 'The impact of a field on the latency of the service, ranging from 0 to 1.', + 'The correlation score [0-1] of an attribute; the greater the score, the more an attribute increases latency.', } )} > @@ -263,20 +264,6 @@ export function MlLatencyCorrelations({ onClose }: Props) { return ( <> - -

- {i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.description', - { - defaultMessage: - 'What is slowing down my service? Correlations will help discover a slower performance in a particular cohort of your data.', - } - )} -

-
- - - {!isRunning && ( @@ -320,6 +307,9 @@ export function MlLatencyCorrelations({ onClose }: Props) { + + +
diff --git a/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations_help_popover.tsx b/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations_help_popover.tsx new file mode 100644 index 0000000000000..1f9a41c1139cd --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations_help_popover.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { HelpPopover, HelpPopoverButton } from '../help_popover/help_popover'; + +export function LatencyCorrelationsHelpPopover() { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + return ( + { + setIsPopoverOpen(!isPopoverOpen); + }} + /> + } + closePopover={() => setIsPopoverOpen(false)} + isOpen={isPopoverOpen} + title={i18n.translate('xpack.apm.correlations.latencyPopoverTitle', { + defaultMessage: 'Latency correlations', + })} + > +

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.stories.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.stories.tsx index 8cc16dd801c25..d434a155c9cf4 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.stories.tsx @@ -11,7 +11,6 @@ import { ApmPluginContext, ApmPluginContextValue, } from '../../../../context/apm_plugin/apm_plugin_context'; -import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { ErrorDistribution } from './'; export default { @@ -28,13 +27,11 @@ export default { }; return ( - - - - - - - + + + + + ); }, ], diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/exception_stacktrace.stories.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/exception_stacktrace.stories.tsx index f21c189584d31..9468202edf4d6 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/exception_stacktrace.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/exception_stacktrace.stories.tsx @@ -7,7 +7,6 @@ import { Story } from '@storybook/react'; import React, { ComponentProps, ComponentType } from 'react'; -import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { ExceptionStacktrace } from './exception_stacktrace'; type Args = ComponentProps; @@ -15,13 +14,6 @@ type Args = ComponentProps; export default { title: 'app/ErrorGroupDetails/DetailView/ExceptionStacktrace', component: ExceptionStacktrace, - decorators: [ - (StoryComponent: ComponentType) => ( - - - - ), - ], }; export const JavaWithLongLines: Story = (args) => ( diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx index 344393d42506f..225186cab12c8 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx @@ -17,7 +17,6 @@ import { import { i18n } from '@kbn/i18n'; import React from 'react'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; -import { useTrackPageview } from '../../../../../observability/public'; import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useErrorGroupDistributionFetcher } from '../../../hooks/use_error_group_distribution_fetcher'; @@ -128,9 +127,6 @@ export function ErrorGroupDetails({ groupId, }); - useTrackPageview({ app: 'apm', path: 'error_group_details' }); - useTrackPageview({ app: 'apm', path: 'error_group_details', delay: 15000 }); - if (!errorGroupData || !errorDistributionData) { return ; } diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx index 4c622758e6c8b..8d8d0cb9c107c 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx @@ -14,7 +14,6 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { useTrackPageview } from '../../../../../observability/public'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useErrorGroupDistributionFetcher } from '../../../hooks/use_error_group_distribution_fetcher'; import { useFetcher } from '../../../hooks/use_fetcher'; @@ -60,12 +59,6 @@ export function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) { [environment, kuery, serviceName, start, end, sortField, sortDirection] ); - useTrackPageview({ - app: 'apm', - path: 'error_group_overview', - }); - useTrackPageview({ app: 'apm', path: 'error_group_overview', delay: 15000 }); - if (!errorDistributionData || !errorGroupListData) { return null; } diff --git a/x-pack/plugins/apm/public/components/app/help_popover/help_popover.tsx b/x-pack/plugins/apm/public/components/app/help_popover/help_popover.tsx new file mode 100644 index 0000000000000..def310f1d8140 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/help_popover/help_popover.tsx @@ -0,0 +1,72 @@ +/* + * 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, { ReactNode } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiButtonIcon, + EuiLinkButtonProps, + EuiPopover, + EuiPopoverProps, + EuiPopoverTitle, + EuiText, +} from '@elastic/eui'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; + +const PopoverContent = euiStyled(EuiText)` + max-width: 480px; + max-height: 40vh; +`; + +export function HelpPopoverButton({ + onClick, +}: { + onClick: EuiLinkButtonProps['onClick']; +}) { + return ( + + ); +} + +export function HelpPopover({ + anchorPosition, + button, + children, + closePopover, + isOpen, + title, +}: { + anchorPosition?: EuiPopoverProps['anchorPosition']; + button: EuiPopoverProps['button']; + children: ReactNode; + closePopover: EuiPopoverProps['closePopover']; + isOpen: EuiPopoverProps['isOpen']; + title?: string; +}) { + return ( + + {title && {title}} + + {children} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/help_popover/index.tsx b/x-pack/plugins/apm/public/components/app/help_popover/index.tsx new file mode 100644 index 0000000000000..b1d53722c7bb5 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/help_popover/index.tsx @@ -0,0 +1,8 @@ +/* + * 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 { HelpPopoverButton, HelpPopover } from './help_popover'; diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx index cac94885511c1..ef0a5f2df0434 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx @@ -9,7 +9,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect } from 'react'; import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; -import { useTrackPageview } from '../../../../../observability/public'; import { useAnomalyDetectionJobsContext } from '../../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; @@ -92,13 +91,6 @@ export function ServiceInventory() { const { core } = useApmPluginContext(); const { servicesData, servicesStatus } = useServicesFetcher(); - // The page is called "service inventory" to avoid confusion with the - // "service overview", but this is tracked in some dashboards because it's the - // initial landing page for APM, so it stays as "services_overview" (plural.) - // for backward compatibility. - useTrackPageview({ app: 'apm', path: 'services_overview' }); - useTrackPageview({ app: 'apm', path: 'services_overview', delay: 15000 }); - const { anomalyDetectionJobsData, anomalyDetectionJobsStatus, diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/Popover.stories.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/Popover.stories.tsx index 6b7626514d03f..324a38ea5db39 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/Popover.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/Popover.stories.tsx @@ -8,7 +8,6 @@ import cytoscape from 'cytoscape'; import { CoreStart } from 'kibana/public'; import React, { ComponentType } from 'react'; -import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import { MockUrlParamsContextProvider } from '../../../../context/url_params_context/mock_url_params_context_provider'; import { createCallApmApi } from '../../../../services/rest/createCallApmApi'; @@ -38,15 +37,13 @@ export default { createCallApmApi(coreMock); return ( - - - -
- -
-
-
-
+ + +
+ +
+
+
); }, ], diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/service_stats_list.stories.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/service_stats_list.stories.tsx index a8f004a7295d9..f1a89043f826e 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/service_stats_list.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/service_stats_list.stories.tsx @@ -5,20 +5,12 @@ * 2.0. */ -import React, { ComponentType } from 'react'; -import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; +import React from 'react'; import { ServiceStatsList } from './ServiceStatsList'; export default { title: 'app/ServiceMap/Popover/ServiceStatsList', component: ServiceStatsList, - decorators: [ - (Story: ComponentType) => ( - - - - ), - ], }; export function Example() { diff --git a/x-pack/plugins/apm/public/components/app/service_map/__stories__/Cytoscape.stories.tsx b/x-pack/plugins/apm/public/components/app/service_map/__stories__/Cytoscape.stories.tsx index 8bc0d7239e9c5..7ce9c3e943613 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/__stories__/Cytoscape.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/__stories__/Cytoscape.stories.tsx @@ -6,21 +6,13 @@ */ import cytoscape from 'cytoscape'; -import React, { ComponentType } from 'react'; -import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; +import React from 'react'; import { Cytoscape } from '../Cytoscape'; import { Centerer } from './centerer'; export default { title: 'app/ServiceMap/Cytoscape', component: Cytoscape, - decorators: [ - (Story: ComponentType) => ( - - - - ), - ], }; export function Example() { diff --git a/x-pack/plugins/apm/public/components/app/service_map/__stories__/cytoscape_example_data.stories.tsx b/x-pack/plugins/apm/public/components/app/service_map/__stories__/cytoscape_example_data.stories.tsx index 45de632a152d4..192447ef7591a 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/__stories__/cytoscape_example_data.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/__stories__/cytoscape_example_data.stories.tsx @@ -16,8 +16,7 @@ import { EuiSpacer, EuiToolTip, } from '@elastic/eui'; -import React, { ComponentType, useEffect, useState } from 'react'; -import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; +import React, { useEffect, useState } from 'react'; import { Cytoscape } from '../Cytoscape'; import { Centerer } from './centerer'; import exampleResponseHipsterStore from './example_response_hipster_store.json'; @@ -42,13 +41,6 @@ function getHeight() { export default { title: 'app/ServiceMap/Example data', component: Cytoscape, - decorators: [ - (Story: ComponentType) => ( - - - - ), - ], }; export function GenerateMap() { diff --git a/x-pack/plugins/apm/public/components/app/service_map/index.tsx b/x-pack/plugins/apm/public/components/app/service_map/index.tsx index 582eafe7553af..22adb10512d7a 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/index.tsx @@ -13,7 +13,6 @@ import { } from '@elastic/eui'; import React, { PropsWithChildren, ReactNode } from 'react'; import { isActivePlatinumLicense } from '../../../../common/license_check'; -import { useTrackPageview } from '../../../../../observability/public'; import { invalidLicenseMessage, SERVICE_MAP_TIMEOUT_ERROR, @@ -106,9 +105,6 @@ export function ServiceMap({ const PADDING_BOTTOM = 24; const heightWithPadding = height - PADDING_BOTTOM; - useTrackPageview({ app: 'apm', path: 'service_map' }); - useTrackPageview({ app: 'apm', path: 'service_map', delay: 15000 }); - if (!license) { return null; } diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index fce543b05c6c3..374b2d59ea347 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -7,7 +7,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import React from 'react'; -import { useTrackPageview } from '../../../../../observability/public'; import { isRumAgentName, isIosAgentName } from '../../../../common/agent_name'; import { AnnotationsContextProvider } from '../../../context/annotations/annotations_context'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; @@ -35,9 +34,6 @@ interface ServiceOverviewProps { export function ServiceOverview({ serviceName }: ServiceOverviewProps) { const { agentName } = useApmServiceContext(); - useTrackPageview({ app: 'apm', path: 'service_overview' }); - useTrackPageview({ app: 'apm', path: 'service_overview', delay: 15000 }); - // The default EuiFlexGroup breaks at 768, but we want to break at 992, so we // observe the window width and set the flex directions of rows accordingly const { isMedium } = useBreakPoints(); diff --git a/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx index d280b36a603ba..ccb5fea72432c 100644 --- a/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx @@ -6,7 +6,6 @@ */ import React from 'react'; -import { useTrackPageview } from '../../../../../observability/public'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; @@ -43,9 +42,6 @@ export function TraceOverview() { [environment, kuery, start, end] ); - useTrackPageview({ app: 'apm', path: 'traces_overview' }); - useTrackPageview({ app: 'apm', path: 'traces_overview', delay: 15000 }); - return ( <> diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx index 1e13e224a511a..40f50e768e76e 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx @@ -9,7 +9,6 @@ import { EuiHorizontalRule, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { flatten, isEmpty } from 'lodash'; import React from 'react'; import { useHistory } from 'react-router-dom'; -import { useTrackPageview } from '../../../../../observability/public'; import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; @@ -41,9 +40,6 @@ export function TransactionDetails() { } = useWaterfallFetcher(); const { transactionName } = urlParams; - useTrackPageview({ app: 'apm', path: 'transaction_details' }); - useTrackPageview({ app: 'apm', path: 'transaction_details', delay: 15000 }); - const selectedSample = flatten( distributionData.buckets.map((bucket) => bucket.samples) ).find( diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/WaterfallContainer.stories.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/WaterfallContainer.stories.tsx index 5ea2fca2dfa32..20ca3194fbfdf 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/WaterfallContainer.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/WaterfallContainer.stories.tsx @@ -7,7 +7,6 @@ import React, { ComponentType } from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { EuiThemeProvider } from '../../../../../../../../../src/plugins/kibana_react/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { TraceAPIResponse } from '../../../../../../server/lib/traces/get_trace'; import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; @@ -27,11 +26,9 @@ export default { decorators: [ (Story: ComponentType) => ( - - - - - + + + ), ], diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx index 041c12822357c..2435e5fc5a1f6 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx @@ -17,7 +17,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { Location } from 'history'; import React from 'react'; import { useLocation } from 'react-router-dom'; -import { useTrackPageview } from '../../../../../observability/public'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { IUrlParams } from '../../../context/url_params_context/types'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; @@ -62,8 +61,6 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) { // redirect to first transaction type useRedirect(getRedirectLocation({ location, transactionType, urlParams })); - useTrackPageview({ app: 'apm', path: 'transaction_overview' }); - useTrackPageview({ app: 'apm', path: 'transaction_overview', delay: 15000 }); const { transactionListData, transactionListStatus, diff --git a/x-pack/plugins/apm/public/components/routing/app_root.tsx b/x-pack/plugins/apm/public/components/routing/app_root.tsx index 8fc59a01eeca0..a924c1f31cbef 100644 --- a/x-pack/plugins/apm/public/components/routing/app_root.tsx +++ b/x-pack/plugins/apm/public/components/routing/app_root.tsx @@ -9,7 +9,7 @@ import { ApmRoute } from '@elastic/apm-rum-react'; import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import React from 'react'; -import { Route, Router, Switch } from 'react-router-dom'; +import { Route, RouteComponentProps, Router, Switch } from 'react-router-dom'; import { DefaultTheme, ThemeProvider } from 'styled-components'; import { APP_WRAPPER_CLASS } from '../../../../../../src/core/public'; import { @@ -17,20 +17,21 @@ import { RedirectAppLinks, useUiSetting$, } from '../../../../../../src/plugins/kibana_react/public'; +import { HeaderMenuPortal } from '../../../../observability/public'; import { ScrollToTopOnPathChange } from '../../components/app/Main/ScrollToTopOnPathChange'; +import { AnomalyDetectionJobsContextProvider } from '../../context/anomaly_detection_jobs/anomaly_detection_jobs_context'; import { ApmPluginContext, ApmPluginContextValue, } from '../../context/apm_plugin/apm_plugin_context'; +import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context'; import { LicenseProvider } from '../../context/license/license_context'; import { UrlParamsProvider } from '../../context/url_params_context/url_params_context'; import { useApmBreadcrumbs } from '../../hooks/use_apm_breadcrumbs'; import { ApmPluginStartDeps } from '../../plugin'; -import { HeaderMenuPortal } from '../../../../observability/public'; import { ApmHeaderActionMenu } from '../shared/apm_header_action_menu'; -import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context'; -import { AnomalyDetectionJobsContextProvider } from '../../context/anomaly_detection_jobs/anomaly_detection_jobs_context'; import { apmRouteConfig } from './apm_route_config'; +import { TelemetryWrapper } from './telemetry_wrapper'; export function ApmAppRoot({ apmPluginContextValue, @@ -62,9 +63,18 @@ export function ApmAppRoot({ - {apmRouteConfig.map((route, i) => ( - - ))} + {apmRouteConfig.map((route, i) => { + const { component, render, ...rest } = route; + return ( + { + return TelemetryWrapper({ route, props }); + }} + /> + ); + })} diff --git a/x-pack/plugins/apm/public/components/routing/telemetry_wrapper.tsx b/x-pack/plugins/apm/public/components/routing/telemetry_wrapper.tsx new file mode 100644 index 0000000000000..fc3fc6a338d18 --- /dev/null +++ b/x-pack/plugins/apm/public/components/routing/telemetry_wrapper.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { useTrackPageview } from '../../../../observability/public'; +import { APMRouteDefinition } from '../../application/routes'; +import { redirectTo } from './redirect_to'; + +export function TelemetryWrapper({ + route, + props, +}: { + route: APMRouteDefinition; + props: RouteComponentProps; +}) { + const { component, render, path } = route; + const pathAsString = path as string; + + useTrackPageview({ app: 'apm', path: pathAsString }); + useTrackPageview({ app: 'apm', path: pathAsString, delay: 15000 }); + + if (component) { + return React.createElement(component, props); + } + if (render) { + return <>{render(props)}; + } + return <>{redirectTo('/')}; +} diff --git a/x-pack/plugins/apm/public/components/shared/agent_icon/agent_icon.stories.tsx b/x-pack/plugins/apm/public/components/shared/agent_icon/agent_icon.stories.tsx index bc41fd58ea5d2..68c3edabfa44e 100644 --- a/x-pack/plugins/apm/public/components/shared/agent_icon/agent_icon.stories.tsx +++ b/x-pack/plugins/apm/public/components/shared/agent_icon/agent_icon.stories.tsx @@ -7,30 +7,22 @@ import { EuiCard, + EuiCodeBlock, EuiFlexGroup, - EuiImage, EuiFlexItem, + EuiImage, EuiSpacer, EuiToolTip, - EuiCodeBlock, } from '@elastic/eui'; -import React, { ComponentType } from 'react'; -import { AgentIcon } from './index'; -import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; +import React from 'react'; import { AGENT_NAMES } from '../../../../common/agent_name'; -import { getAgentIcon } from './get_agent_icon'; import { useTheme } from '../../../hooks/use_theme'; +import { getAgentIcon } from './get_agent_icon'; +import { AgentIcon } from './index'; export default { title: 'shared/icons', component: AgentIcon, - decorators: [ - (Story: ComponentType) => ( - - - - ), - ], }; export function AgentIcons() { diff --git a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx index ca73f6ddd05b3..4abd36a277311 100644 --- a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx +++ b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx @@ -66,7 +66,6 @@ export function AlertingPopoverAndFlyout({ const button = ( (); + const serviceName = useServiceName(); const { search } = window.location; const { application, http } = core; const { basePath } = http; @@ -41,7 +41,7 @@ export function ApmHeaderActionMenu() { return ( - + {i18n.translate('xpack.apm.settingsLinkLabel', { defaultMessage: 'Settings', })} diff --git a/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/custom_tooltip.stories.tsx b/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/custom_tooltip.stories.tsx index 0eb5b0e84ff39..6128526c577e4 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/custom_tooltip.stories.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/custom_tooltip.stories.tsx @@ -6,8 +6,7 @@ */ import { TooltipInfo } from '@elastic/charts'; -import React, { ComponentType } from 'react'; -import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; +import React from 'react'; import { getDurationFormatter } from '../../../../../common/utils/formatters'; import { MainStatsServiceInstanceItem } from '../../../app/service_overview/service_overview_instances_chart_and_table'; import { CustomTooltip } from './custom_tooltip'; @@ -25,13 +24,6 @@ function getLatencyFormatter(props: TooltipInfo) { export default { title: 'shared/charts/InstancesLatencyDistributionChart/CustomTooltip', component: CustomTooltip, - decorators: [ - (Story: ComponentType) => ( - - - - ), - ], }; export function Example(props: TooltipInfo) { diff --git a/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/instances_latency_distribution_chart.stories.tsx b/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/instances_latency_distribution_chart.stories.tsx index c574645d485d5..80bfcca05aabc 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/instances_latency_distribution_chart.stories.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/instances_latency_distribution_chart.stories.tsx @@ -5,8 +5,7 @@ * 2.0. */ -import React, { ComponentType } from 'react'; -import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; +import React from 'react'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { InstancesLatencyDistributionChart, @@ -16,13 +15,6 @@ import { export default { title: 'shared/charts/InstancesLatencyDistributionChart', component: InstancesLatencyDistributionChart, - decorators: [ - (Story: ComponentType) => ( - - - - ), - ], }; export function Example({ items }: InstancesLatencyDistributionChartProps) { diff --git a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx index d1dcd831eadd7..ff2b95667a63a 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx @@ -8,7 +8,6 @@ import { StoryContext } from '@storybook/react'; import React, { ComponentType } from 'react'; import { MemoryRouter, Route } from 'react-router-dom'; -import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types'; import { @@ -76,20 +75,18 @@ export default { - - - - - - - - - + + + + + + + diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/span_icon.stories.tsx b/x-pack/plugins/apm/public/components/shared/span_icon/span_icon.stories.tsx index b053f441e9632..7d2e2fbefc359 100644 --- a/x-pack/plugins/apm/public/components/shared/span_icon/span_icon.stories.tsx +++ b/x-pack/plugins/apm/public/components/shared/span_icon/span_icon.stories.tsx @@ -6,32 +6,23 @@ */ import { - EuiImage, EuiCard, + EuiCodeBlock, EuiFlexGroup, EuiFlexItem, + EuiImage, EuiSpacer, - EuiCodeBlock, EuiToolTip, } from '@elastic/eui'; -import React, { ComponentType } from 'react'; -import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; +import React from 'react'; +import { getSpanIcon, spanTypeIcons } from './get_span_icon'; import { SpanIcon } from './index'; -import { getSpanIcon } from './get_span_icon'; -import { spanTypeIcons } from './get_span_icon'; const spanTypes = Object.keys(spanTypeIcons); export default { title: 'shared/icons', component: SpanIcon, - decorators: [ - (Story: ComponentType) => ( - - - - ), - ], }; export function SpanIcons() { diff --git a/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx index 54914580aefbd..cb826763425c2 100644 --- a/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx @@ -18,6 +18,7 @@ import { useServiceAgentNameFetcher } from './use_service_agent_name_fetcher'; import { IUrlParams } from '../url_params_context/types'; import { APIReturnType } from '../../services/rest/createCallApmApi'; import { useServiceAlertsFetcher } from './use_service_alerts_fetcher'; +import { useServiceName } from '../../hooks/use_service_name'; export type APMServiceAlert = ValuesType< APIReturnType<'GET /api/apm/services/{serviceName}/alerts'>['alerts'] @@ -28,6 +29,7 @@ export const APMServiceContext = createContext<{ transactionType?: string; transactionTypes: string[]; alerts: APMServiceAlert[]; + serviceName?: string; }>({ transactionTypes: [], alerts: [] }); export function ApmServiceContextProvider({ @@ -36,9 +38,12 @@ export function ApmServiceContextProvider({ children: ReactNode; }) { const { urlParams } = useUrlParams(); - const { agentName } = useServiceAgentNameFetcher(); - const transactionTypes = useServiceTransactionTypesFetcher(); + const serviceName = useServiceName(); + + const { agentName } = useServiceAgentNameFetcher(serviceName); + + const transactionTypes = useServiceTransactionTypesFetcher(serviceName); const transactionType = getTransactionType({ urlParams, @@ -46,7 +51,7 @@ export function ApmServiceContextProvider({ agentName, }); - const { alerts } = useServiceAlertsFetcher(transactionType); + const { alerts } = useServiceAlertsFetcher({ serviceName, transactionType }); return ( diff --git a/x-pack/plugins/apm/public/context/apm_service/use_service_agent_name_fetcher.ts b/x-pack/plugins/apm/public/context/apm_service/use_service_agent_name_fetcher.ts index ceb6767898f06..82198eb73b3cb 100644 --- a/x-pack/plugins/apm/public/context/apm_service/use_service_agent_name_fetcher.ts +++ b/x-pack/plugins/apm/public/context/apm_service/use_service_agent_name_fetcher.ts @@ -5,12 +5,10 @@ * 2.0. */ -import { useParams } from 'react-router-dom'; import { useFetcher } from '../../hooks/use_fetcher'; import { useUrlParams } from '../url_params_context/use_url_params'; -export function useServiceAgentNameFetcher() { - const { serviceName } = useParams<{ serviceName?: string }>(); +export function useServiceAgentNameFetcher(serviceName?: string) { const { urlParams } = useUrlParams(); const { start, end } = urlParams; const { data, error, status } = useFetcher( diff --git a/x-pack/plugins/apm/public/context/apm_service/use_service_alerts_fetcher.tsx b/x-pack/plugins/apm/public/context/apm_service/use_service_alerts_fetcher.tsx index b07e6562a2154..54c95319afea2 100644 --- a/x-pack/plugins/apm/public/context/apm_service/use_service_alerts_fetcher.tsx +++ b/x-pack/plugins/apm/public/context/apm_service/use_service_alerts_fetcher.tsx @@ -5,13 +5,18 @@ * 2.0. */ -import { useParams } from 'react-router-dom'; import { useApmPluginContext } from '../apm_plugin/use_apm_plugin_context'; import { useUrlParams } from '../url_params_context/use_url_params'; import { useFetcher } from '../../hooks/use_fetcher'; import type { APMServiceAlert } from './apm_service_context'; -export function useServiceAlertsFetcher(transactionType?: string) { +export function useServiceAlertsFetcher({ + serviceName, + transactionType, +}: { + serviceName?: string; + transactionType?: string; +}) { const { plugins: { observability }, } = useApmPluginContext(); @@ -19,7 +24,6 @@ export function useServiceAlertsFetcher(transactionType?: string) { const { urlParams: { start, end, environment }, } = useUrlParams(); - const { serviceName } = useParams<{ serviceName?: string }>(); const experimentalAlertsEnabled = observability.isAlertingExperienceEnabled(); diff --git a/x-pack/plugins/apm/public/context/apm_service/use_service_transaction_types_fetcher.tsx b/x-pack/plugins/apm/public/context/apm_service/use_service_transaction_types_fetcher.tsx index ba70295ae70ca..b22c233b0c24b 100644 --- a/x-pack/plugins/apm/public/context/apm_service/use_service_transaction_types_fetcher.tsx +++ b/x-pack/plugins/apm/public/context/apm_service/use_service_transaction_types_fetcher.tsx @@ -5,14 +5,12 @@ * 2.0. */ -import { useParams } from 'react-router-dom'; import { useFetcher } from '../../hooks/use_fetcher'; import { useUrlParams } from '../url_params_context/use_url_params'; const INITIAL_DATA = { transactionTypes: [] }; -export function useServiceTransactionTypesFetcher() { - const { serviceName } = useParams<{ serviceName?: string }>(); +export function useServiceTransactionTypesFetcher(serviceName?: string) { const { urlParams } = useUrlParams(); const { start, end } = urlParams; const { data = INITIAL_DATA } = useFetcher( diff --git a/x-pack/plugins/apm/public/hooks/use_service_name.tsx b/x-pack/plugins/apm/public/hooks/use_service_name.tsx new file mode 100644 index 0000000000000..c003bf5223a32 --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_service_name.tsx @@ -0,0 +1,16 @@ +/* + * 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 { useRouteMatch } from 'react-router-dom'; + +export function useServiceName(): string | undefined { + const match = useRouteMatch<{ serviceName?: string }>( + '/services/:serviceName' + ); + + return match ? match.params.serviceName : undefined; +} diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 0cd5009570613..91b045b8db46f 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -74,7 +74,7 @@ export interface ApmPluginStartDeps { ml?: MlPluginStart; triggersActionsUi: TriggersAndActionsUIPublicPluginStart; observability: ObservabilityPublicStart; - fleet: FleetStart; + fleet?: FleetStart; } export class ApmPlugin implements Plugin { @@ -311,20 +311,21 @@ export class ApmPlugin implements Plugin { } public start(core: CoreStart, plugins: ApmPluginStartDeps) { const { fleet } = plugins; - - const agentEnrollmentExtensionData = getApmEnrollmentFlyoutData(); - - fleet.registerExtension({ - package: 'apm', - view: 'agent-enrollment-flyout', - title: agentEnrollmentExtensionData.title, - Component: agentEnrollmentExtensionData.Component, - }); - - fleet.registerExtension({ - package: 'apm', - view: 'package-detail-assets', - Component: LazyApmCustomAssetsExtension, - }); + if (fleet) { + const agentEnrollmentExtensionData = getApmEnrollmentFlyoutData(); + + fleet.registerExtension({ + package: 'apm', + view: 'agent-enrollment-flyout', + title: agentEnrollmentExtensionData.title, + Component: agentEnrollmentExtensionData.Component, + }); + + fleet.registerExtension({ + package: 'apm', + view: 'package-detail-assets', + Component: LazyApmCustomAssetsExtension, + }); + } } } diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/config_agent.stories.tsx b/x-pack/plugins/apm/public/tutorial/config_agent/config_agent.stories.tsx index 33f171ab88247..0d4d3748422ea 100644 --- a/x-pack/plugins/apm/public/tutorial/config_agent/config_agent.stories.tsx +++ b/x-pack/plugins/apm/public/tutorial/config_agent/config_agent.stories.tsx @@ -18,6 +18,7 @@ interface Args { onPrem: boolean; hasFleetPoliciesWithApmIntegration: boolean; hasCloudPolicyWithApmIntegration: boolean; + isFleetEnabled: boolean; } const policyElasticAgentOnCloudAgent: APIResponseType['fleetAgents'][0] = { @@ -47,6 +48,7 @@ function Wrapper({ apmAgent, onPrem, hasCloudPolicyWithApmIntegration, + isFleetEnabled, }: Args) { const http = ({ get: () => ({ @@ -56,6 +58,7 @@ function Wrapper({ ? [policyElasticAgentOnCloudAgent] : []), ], + isFleetEnabled, cloudStandaloneSetup: { apmServerUrl: 'cloud_url', secretToken: 'foo', @@ -80,6 +83,7 @@ Integration.args = { onPrem: true, hasFleetPoliciesWithApmIntegration: false, hasCloudPolicyWithApmIntegration: false, + isFleetEnabled: true, }; export default { @@ -113,5 +117,8 @@ export default { hasCloudPolicyWithApmIntegration: { control: { type: 'boolean', options: [true, false] }, }, + isFleetEnabled: { + control: { type: 'boolean', options: [true, false], defaultValue: true }, + }, }, }; diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/get_policy_options.test.ts b/x-pack/plugins/apm/public/tutorial/config_agent/get_policy_options.test.ts index 90c9aab80f6f5..c6dc7265f3d3e 100644 --- a/x-pack/plugins/apm/public/tutorial/config_agent/get_policy_options.test.ts +++ b/x-pack/plugins/apm/public/tutorial/config_agent/get_policy_options.test.ts @@ -41,6 +41,7 @@ describe('getPolicyOptions', () => { apmServerUrl: 'cloud_url', secretToken: 'cloud_token', }, + isFleetEnabled: true, }; const options = getPolicyOptions({ isCloudEnabled: true, @@ -65,6 +66,7 @@ describe('getPolicyOptions', () => { apmServerUrl: 'cloud_url', secretToken: 'cloud_token', }, + isFleetEnabled: true, }; const options = getPolicyOptions({ isCloudEnabled: true, @@ -109,6 +111,7 @@ describe('getPolicyOptions', () => { apmServerUrl: 'cloud_url', secretToken: 'cloud_token', }, + isFleetEnabled: true, }; const options = getPolicyOptions({ isCloudEnabled: true, @@ -151,6 +154,7 @@ describe('getPolicyOptions', () => { const data: APIResponseType = { fleetAgents: [], cloudStandaloneSetup: undefined, + isFleetEnabled: true, }; const options = getPolicyOptions({ isCloudEnabled: true, @@ -173,6 +177,7 @@ describe('getPolicyOptions', () => { const data: APIResponseType = { fleetAgents, cloudStandaloneSetup: undefined, + isFleetEnabled: true, }; const options = getPolicyOptions({ isCloudEnabled: true, @@ -213,6 +218,7 @@ describe('getPolicyOptions', () => { const data: APIResponseType = { fleetAgents: [policyElasticAgentOnCloudAgent, ...fleetAgents], cloudStandaloneSetup: undefined, + isFleetEnabled: true, }; const options = getPolicyOptions({ isCloudEnabled: true, @@ -256,6 +262,7 @@ describe('getPolicyOptions', () => { const data: APIResponseType = { fleetAgents: [], cloudStandaloneSetup: undefined, + isFleetEnabled: true, }; const options = getPolicyOptions({ isCloudEnabled: false, @@ -278,6 +285,7 @@ describe('getPolicyOptions', () => { const data: APIResponseType = { fleetAgents, cloudStandaloneSetup: undefined, + isFleetEnabled: true, }; const options = getPolicyOptions({ isCloudEnabled: false, diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/index.test.tsx b/x-pack/plugins/apm/public/tutorial/config_agent/index.test.tsx index 8f8afe58506a6..cb49cee108bd1 100644 --- a/x-pack/plugins/apm/public/tutorial/config_agent/index.test.tsx +++ b/x-pack/plugins/apm/public/tutorial/config_agent/index.test.tsx @@ -7,6 +7,10 @@ import { fireEvent, render, screen } from '@testing-library/react'; import { HttpStart } from 'kibana/public'; import React from 'react'; +import { + expectTextsInDocument, + expectTextsNotInDocument, +} from '../../utils/testHelpers'; import TutorialConfigAgent from './'; const policyElasticAgentOnCloudAgent = { @@ -32,68 +36,32 @@ const fleetAgents = [ ]; describe('TutorialConfigAgent', () => { - it('renders loading component while API is being called', () => { - const component = render( - - ); - expect(component.getByTestId('loading')).toBeInTheDocument(); + beforeAll(() => { + // Mocks console.error so it won't polute tests output when testing the api throwing error + jest.spyOn(console, 'error').mockImplementation(() => null); }); - it('updates commands when a different policy is selected', async () => { - const component = render( - - ); - expect( - await screen.findByText('Default Standalone configuration') - ).toBeInTheDocument(); - let commands = component.getByTestId('commands').innerHTML; - expect(commands).not.toEqual(''); - expect(commands).toMatchInlineSnapshot(` - "java -javaagent:/path/to/elastic-apm-agent-<version>.jar \\\\ - -Delastic.apm.service_name=my-application \\\\ - -Delastic.apm.server_urls=http://localhost:8200 \\\\ - -Delastic.apm.secret_token= \\\\ - -Delastic.apm.environment=production \\\\ - -Delastic.apm.application_packages=org.example \\\\ - -jar my-application.jar" - `); - fireEvent.click(component.getByTestId('comboBoxToggleListButton')); - fireEvent.click(component.getByText('agent foo')); - commands = component.getByTestId('commands').innerHTML; - expect(commands).not.toEqual(''); - expect(commands).toMatchInlineSnapshot(` - "java -javaagent:/path/to/elastic-apm-agent-<version>.jar \\\\ - -Delastic.apm.service_name=my-application \\\\ - -Delastic.apm.server_urls=foo \\\\ - -Delastic.apm.secret_token=foo \\\\ - -Delastic.apm.environment=production \\\\ - -Delastic.apm.application_packages=org.example \\\\ - -jar my-application.jar" - `); + afterAll(() => { + jest.restoreAllMocks(); }); - describe('running on prem', () => { - it('selects defaul standalone by defauls', async () => { + + describe('when fleet plugin is enabled', () => { + it('renders loading component while API is being called', () => { + const component = render( + + ); + expect(component.getByTestId('loading')).toBeInTheDocument(); + }); + it('updates commands when a different policy is selected', async () => { const component = render( { get: jest.fn().mockReturnValue({ cloudStandaloneSetup: undefined, fleetAgents, + isFleetEnabled: true, }), } as unknown) as HttpStart } @@ -112,10 +81,7 @@ describe('TutorialConfigAgent', () => { expect( await screen.findByText('Default Standalone configuration') ).toBeInTheDocument(); - expect( - component.getByTestId('policySelector_onPrem') - ).toBeInTheDocument(); - const commands = component.getByTestId('commands').innerHTML; + let commands = component.getByTestId('commands').innerHTML; expect(commands).not.toEqual(''); expect(commands).toMatchInlineSnapshot(` "java -javaagent:/path/to/elastic-apm-agent-<version>.jar \\\\ @@ -126,21 +92,238 @@ describe('TutorialConfigAgent', () => { -Delastic.apm.application_packages=org.example \\\\ -jar my-application.jar" `); + + fireEvent.click(component.getByTestId('comboBoxToggleListButton')); + fireEvent.click(component.getByText('agent foo')); + commands = component.getByTestId('commands').innerHTML; + expect(commands).not.toEqual(''); + expect(commands).toMatchInlineSnapshot(` + "java -javaagent:/path/to/elastic-apm-agent-<version>.jar \\\\ + -Delastic.apm.service_name=my-application \\\\ + -Delastic.apm.server_urls=foo \\\\ + -Delastic.apm.secret_token=foo \\\\ + -Delastic.apm.environment=production \\\\ + -Delastic.apm.application_packages=org.example \\\\ + -jar my-application.jar" + `); + }); + describe('running on prem', () => { + it('selects defaul standalone by defauls', async () => { + const component = render( + + ); + expect( + await screen.findByText('Default Standalone configuration') + ).toBeInTheDocument(); + expect( + component.getByTestId('policySelector_onPrem') + ).toBeInTheDocument(); + const commands = component.getByTestId('commands').innerHTML; + expect(commands).not.toEqual(''); + expect(commands).toMatchInlineSnapshot(` + "java -javaagent:/path/to/elastic-apm-agent-<version>.jar \\\\ + -Delastic.apm.service_name=my-application \\\\ + -Delastic.apm.server_urls=http://localhost:8200 \\\\ + -Delastic.apm.secret_token= \\\\ + -Delastic.apm.environment=production \\\\ + -Delastic.apm.application_packages=org.example \\\\ + -jar my-application.jar" + `); + }); + it('shows get started with fleet link when there are no fleet agents', async () => { + const component = render( + + ); + expect( + await screen.findByText('Default Standalone configuration') + ).toBeInTheDocument(); + expect( + component.getByTestId('policySelector_onPrem') + ).toBeInTheDocument(); + const commands = component.getByTestId('commands').innerHTML; + expect(commands).not.toEqual(''); + expect(commands).toMatchInlineSnapshot(` + "java -javaagent:/path/to/elastic-apm-agent-<version>.jar \\\\ + -Delastic.apm.service_name=my-application \\\\ + -Delastic.apm.server_urls=http://localhost:8200 \\\\ + -Delastic.apm.secret_token= \\\\ + -Delastic.apm.environment=production \\\\ + -Delastic.apm.application_packages=org.example \\\\ + -jar my-application.jar" + `); + expectTextsInDocument(component, ['Get started with fleet']); + }); + }); + describe('running on cloud', () => { + it('selects defaul standalone by defauls', async () => { + const component = render( + + ); + expect( + await screen.findByText('Default Standalone configuration') + ).toBeInTheDocument(); + expect( + component.getByTestId('policySelector_cloud') + ).toBeInTheDocument(); + const commands = component.getByTestId('commands').innerHTML; + expect(commands).not.toEqual(''); + expect(commands).toMatchInlineSnapshot(` + "java -javaagent:/path/to/elastic-apm-agent-<version>.jar \\\\ + -Delastic.apm.service_name=my-application \\\\ + -Delastic.apm.server_urls=cloud_url \\\\ + -Delastic.apm.secret_token=cloud_token \\\\ + -Delastic.apm.environment=production \\\\ + -Delastic.apm.application_packages=org.example \\\\ + -jar my-application.jar" + `); + }); + it('selects policy elastic agent on cloud when available by default', async () => { + const component = render( + + ); + expect( + await screen.findByText('Elastic Cloud agent policy') + ).toBeInTheDocument(); + expect( + component.getByTestId('policySelector_policy-elastic-agent-on-cloud') + ).toBeInTheDocument(); + const commands = component.getByTestId('commands').innerHTML; + expect(commands).not.toEqual(''); + expect(commands).toMatchInlineSnapshot(` + "java -javaagent:/path/to/elastic-apm-agent-<version>.jar \\\\ + -Delastic.apm.service_name=my-application \\\\ + -Delastic.apm.server_urls=apm_cloud_url \\\\ + -Delastic.apm.secret_token=apm_cloud_token \\\\ + -Delastic.apm.environment=production \\\\ + -Delastic.apm.application_packages=org.example \\\\ + -jar my-application.jar" + `); + }); + + it('shows default standalone option when api throws an error', async () => { + const component = render( + { + throw new Error('Boom'); + }, + } as unknown) as HttpStart + } + basePath="http://localhost:5601" + isCloudEnabled + /> + ); + expect( + await screen.findByText('Default Standalone configuration') + ).toBeInTheDocument(); + const commands = component.getByTestId('commands').innerHTML; + expect(commands).not.toEqual(''); + expect(commands).toMatchInlineSnapshot(` + "java -javaagent:/path/to/elastic-apm-agent-<version>.jar \\\\ + -Delastic.apm.service_name=my-application \\\\ + -Delastic.apm.server_urls=http://localhost:8200 \\\\ + -Delastic.apm.secret_token= \\\\ + -Delastic.apm.environment=production \\\\ + -Delastic.apm.application_packages=org.example \\\\ + -jar my-application.jar" + `); + }); }); }); - describe('running on cloud', () => { - it('selects defaul standalone by defauls', async () => { + describe('when fleet plugin is disabled', () => { + it('hides fleet links', async () => { const component = render( + ); + + expectTextsNotInDocument(component, [ + 'Get started with fleet', + 'Manage fleet policies', + ]); + }); + it('shows default standalone on prem', async () => { + const component = render( + { expect( await screen.findByText('Default Standalone configuration') ).toBeInTheDocument(); - expect(component.getByTestId('policySelector_cloud')).toBeInTheDocument(); + expect( + component.getByTestId('policySelector_onPrem') + ).toBeInTheDocument(); const commands = component.getByTestId('commands').innerHTML; expect(commands).not.toEqual(''); expect(commands).toMatchInlineSnapshot(` "java -javaagent:/path/to/elastic-apm-agent-<version>.jar \\\\ -Delastic.apm.service_name=my-application \\\\ - -Delastic.apm.server_urls=cloud_url \\\\ - -Delastic.apm.secret_token=cloud_token \\\\ + -Delastic.apm.server_urls=http://localhost:8200 \\\\ + -Delastic.apm.secret_token= \\\\ -Delastic.apm.environment=production \\\\ -Delastic.apm.application_packages=org.example \\\\ -jar my-application.jar" `); }); - it('selects policy elastic agent on cloud when available by default', async () => { + it('shows default standalone on cloud', async () => { const component = render( { apmServerUrl: 'cloud_url', secretToken: 'cloud_token', }, - fleetAgents: [...fleetAgents, policyElasticAgentOnCloudAgent], + fleetAgents: [], + isFleetEnabled: false, }), } as unknown) as HttpStart } @@ -184,18 +370,16 @@ describe('TutorialConfigAgent', () => { /> ); expect( - await screen.findByText('Elastic Cloud agent policy') - ).toBeInTheDocument(); - expect( - component.getByTestId('policySelector_policy-elastic-agent-on-cloud') + await screen.findByText('Default Standalone configuration') ).toBeInTheDocument(); + expect(component.getByTestId('policySelector_cloud')).toBeInTheDocument(); const commands = component.getByTestId('commands').innerHTML; expect(commands).not.toEqual(''); expect(commands).toMatchInlineSnapshot(` "java -javaagent:/path/to/elastic-apm-agent-<version>.jar \\\\ -Delastic.apm.service_name=my-application \\\\ - -Delastic.apm.server_urls=apm_cloud_url \\\\ - -Delastic.apm.secret_token=apm_cloud_token \\\\ + -Delastic.apm.server_urls=cloud_url \\\\ + -Delastic.apm.secret_token=cloud_token \\\\ -Delastic.apm.environment=production \\\\ -Delastic.apm.application_packages=org.example \\\\ -jar my-application.jar" diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/index.tsx b/x-pack/plugins/apm/public/tutorial/config_agent/index.tsx index 755c3eca55868..d38d51f01c67b 100644 --- a/x-pack/plugins/apm/public/tutorial/config_agent/index.tsx +++ b/x-pack/plugins/apm/public/tutorial/config_agent/index.tsx @@ -46,16 +46,43 @@ interface Props { isCloudEnabled: boolean; } +const INITIAL_STATE = { + fleetAgents: [], + cloudStandaloneSetup: undefined, + isFleetEnabled: false, +}; + +function getFleetLink({ + isFleetEnabled, + hasFleetAgents, + basePath, +}: { + isFleetEnabled: boolean; + hasFleetAgents: boolean; + basePath: string; +}) { + if (!isFleetEnabled) { + return; + } + + return hasFleetAgents + ? { + label: MANAGE_FLEET_POLICIES_LABEL, + href: `${basePath}/app/fleet#/policies`, + } + : { + label: GET_STARTED_WITH_FLEET_LABEL, + href: `${basePath}/app/integrations#/detail/apm-0.3.0/overview`, + }; +} + function TutorialConfigAgent({ variantId, http, basePath, isCloudEnabled, }: Props) { - const [data, setData] = useState({ - fleetAgents: [], - cloudStandaloneSetup: undefined, - }); + const [data, setData] = useState(INITIAL_STATE); const [isLoading, setIsLoading] = useState(true); const [selectedOption, setSelectedOption] = useState(); @@ -68,6 +95,7 @@ function TutorialConfigAgent({ setData(response as APIResponseType); } } catch (e) { + setIsLoading(false); console.error('Error while fetching fleet agents.', e); } } @@ -105,15 +133,6 @@ function TutorialConfigAgent({ }); const hasFleetAgents = !!data.fleetAgents.length; - const fleetLink = hasFleetAgents - ? { - label: MANAGE_FLEET_POLICIES_LABEL, - href: `${basePath}/app/fleet#/policies`, - } - : { - label: GET_STARTED_WITH_FLEET_LABEL, - href: `${basePath}/app/integrations#/detail/apm-0.3.0/overview`, - }; return ( <> @@ -125,7 +144,11 @@ function TutorialConfigAgent({ onChange={(newSelectedOption) => setSelectedOption(newSelectedOption) } - fleetLink={fleetLink} + fleetLink={getFleetLink({ + isFleetEnabled: data.isFleetEnabled, + hasFleetAgents, + basePath, + })} /> diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/policy_selector.tsx b/x-pack/plugins/apm/public/tutorial/config_agent/policy_selector.tsx index 3a0c6d70db82b..25ce7042c4c97 100644 --- a/x-pack/plugins/apm/public/tutorial/config_agent/policy_selector.tsx +++ b/x-pack/plugins/apm/public/tutorial/config_agent/policy_selector.tsx @@ -21,7 +21,7 @@ interface Props { options: PolicyOption[]; selectedOption?: PolicyOption; onChange: (selectedOption?: PolicyOption) => void; - fleetLink: { + fleetLink?: { label: string; href: string; }; @@ -58,9 +58,11 @@ export function PolicySelector({ { defaultMessage: 'Choose policy' } )} labelAppend={ - - {fleetLink.label} - + fleetLink && ( + + {fleetLink.label} + + ) } helpText={i18n.translate( 'xpack.apm.tutorial.agent_config.choosePolicy.helper', diff --git a/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/index.tsx b/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/index.tsx index 8a81b7a994e76..6fcf13345538f 100644 --- a/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/index.tsx +++ b/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/index.tsx @@ -42,6 +42,7 @@ function TutorialFleetInstructions({ http, basePath, isDarkTheme }: Props) { const response = await http.get('/api/apm/fleet/has_data'); setData(response as APIResponseType); } catch (e) { + setIsLoading(false); console.error('Error while fetching fleet details.', e); } setIsLoading(false); diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index fb0610dffb92e..f3e2bba2d9789 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -6,8 +6,9 @@ */ import { i18n } from '@kbn/i18n'; +import { SubFeaturePrivilegeGroupType } from '../../features/common'; import { LicenseType } from '../../licensing/common/types'; -import { AlertType } from '../common/alert_types'; +import { AlertType, APM_SERVER_FEATURE_ID } from '../common/alert_types'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { LicensingPluginSetup, @@ -15,14 +16,14 @@ import { } from '../../licensing/server'; export const APM_FEATURE = { - id: 'apm', + id: APM_SERVER_FEATURE_ID, name: i18n.translate('xpack.apm.featureRegistry.apmFeatureName', { defaultMessage: 'APM and User Experience', }), order: 900, category: DEFAULT_APP_CATEGORIES.observability, - app: ['apm', 'ux', 'kibana'], - catalogue: ['apm'], + app: [APM_SERVER_FEATURE_ID, 'ux', 'kibana'], + catalogue: [APM_SERVER_FEATURE_ID], management: { insightsAndAlerting: ['triggersActions'], }, @@ -30,9 +31,9 @@ export const APM_FEATURE = { // see x-pack/plugins/features/common/feature_kibana_privileges.ts privileges: { all: { - app: ['apm', 'ux', 'kibana'], - api: ['apm', 'apm_write'], - catalogue: ['apm'], + app: [APM_SERVER_FEATURE_ID, 'ux', 'kibana'], + api: [APM_SERVER_FEATURE_ID, 'apm_write', 'rac'], + catalogue: [APM_SERVER_FEATURE_ID], savedObject: { all: [], read: [], @@ -41,9 +42,6 @@ export const APM_FEATURE = { rule: { all: Object.values(AlertType), }, - alert: { - all: Object.values(AlertType), - }, }, management: { insightsAndAlerting: ['triggersActions'], @@ -51,9 +49,9 @@ export const APM_FEATURE = { ui: ['show', 'save', 'alerting:show', 'alerting:save'], }, read: { - app: ['apm', 'ux', 'kibana'], - api: ['apm'], - catalogue: ['apm'], + app: [APM_SERVER_FEATURE_ID, 'ux', 'kibana'], + api: [APM_SERVER_FEATURE_ID, 'rac'], + catalogue: [APM_SERVER_FEATURE_ID], savedObject: { all: [], read: [], @@ -62,9 +60,6 @@ export const APM_FEATURE = { rule: { read: Object.values(AlertType), }, - alert: { - read: Object.values(AlertType), - }, }, management: { insightsAndAlerting: ['triggersActions'], @@ -72,6 +67,60 @@ export const APM_FEATURE = { ui: ['show', 'alerting:show', 'alerting:save'], }, }, + subFeatures: [ + { + name: i18n.translate('xpack.apm.featureRegistry.manageAlertsName', { + defaultMessage: 'Alerts', + }), + privilegeGroups: [ + { + groupType: 'mutually_exclusive' as SubFeaturePrivilegeGroupType, + privileges: [ + { + id: 'alerts_all', + name: i18n.translate( + 'xpack.apm.featureRegistry.subfeature.alertsAllName', + { + defaultMessage: 'All', + } + ), + includeIn: 'all' as 'all', + alerting: { + alert: { + all: Object.values(AlertType), + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + { + id: 'alerts_read', + name: i18n.translate( + 'xpack.apm.featureRegistry.subfeature.alertsReadName', + { + defaultMessage: 'Read', + } + ), + includeIn: 'read' as 'read', + alerting: { + alert: { + read: Object.values(AlertType), + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + ], + }, + ], + }, + ], }; interface Feature { diff --git a/x-pack/plugins/apm/server/index.test.ts b/x-pack/plugins/apm/server/index.test.ts index 6052ec921f9f9..56c9825db5a5c 100644 --- a/x-pack/plugins/apm/server/index.test.ts +++ b/x-pack/plugins/apm/server/index.test.ts @@ -30,7 +30,7 @@ describe('mergeConfigs', () => { expect(mergeConfigs(apmOssConfig, apmConfig)).toEqual({ 'apm_oss.errorIndices': 'logs-apm*,apm-*-error-*', - 'apm_oss.indexPattern': 'apm-*', + 'apm_oss.indexPattern': 'traces-apm*,logs-apm*,metrics-apm*,apm-*', 'apm_oss.metricsIndices': 'metrics-apm*,apm-*-metric-*', 'apm_oss.spanIndices': 'traces-apm*,apm-*-span-*', 'apm_oss.transactionIndices': 'traces-apm*,apm-*-transaction-*', diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index 8ec92bfa7a1b5..9031f454f4a7f 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -74,7 +74,7 @@ export function mergeConfigs( 'apm_oss.metricsIndices': apmOssConfig.metricsIndices, 'apm_oss.sourcemapIndices': apmOssConfig.sourcemapIndices, 'apm_oss.onboardingIndices': apmOssConfig.onboardingIndices, - 'apm_oss.indexPattern': apmOssConfig.indexPattern, // TODO: add data stream indices: traces-apm*,logs-apm*,metrics-apm*. Blocked by https://github.com/elastic/kibana/issues/87851 + 'apm_oss.indexPattern': apmOssConfig.indexPattern, /* eslint-enable @typescript-eslint/naming-convention */ 'xpack.apm.serviceMapEnabled': apmConfig.serviceMapEnabled, 'xpack.apm.serviceMapFingerprintBucketSize': @@ -119,12 +119,17 @@ export function mergeConfigs( 'apm_oss.metricsIndices' ] = `metrics-apm*,${mergedConfig['apm_oss.metricsIndices']}`; + mergedConfig[ + 'apm_oss.indexPattern' + ] = `traces-apm*,logs-apm*,metrics-apm*,${mergedConfig['apm_oss.indexPattern']}`; + return mergedConfig; } export const plugin = (initContext: PluginInitializerContext) => new APMPlugin(initContext); +export { APM_SERVER_FEATURE_ID } from '../common/alert_types'; export { APMPlugin } from './plugin'; export { APMPluginSetup } from './types'; export { APMServerRouteRepository } from './routes/get_global_apm_server_route_repository'; diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts index 7548d6eba060a..35c80df2ca31c 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts @@ -17,7 +17,11 @@ import { getEnvironmentEsField, getEnvironmentLabel, } from '../../../common/environment_filter_values'; -import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; +import { + AlertType, + APM_SERVER_FEATURE_ID, + ALERT_TYPES_CONFIG, +} from '../../../common/alert_types'; import { PROCESSOR_EVENT, SERVICE_ENVIRONMENT, @@ -69,7 +73,7 @@ export function registerErrorCountAlertType({ apmActionVariables.interval, ], }, - producer: 'apm', + producer: APM_SERVER_FEATURE_ID, minimumLicenseRequired: 'basic', isExportable: true, executor: async ({ services, params }) => { diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts index ca7806251f75e..ff202669fe1da 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts @@ -17,7 +17,11 @@ import { getEnvironmentLabel, getEnvironmentEsField, } from '../../../common/environment_filter_values'; -import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; +import { + AlertType, + APM_SERVER_FEATURE_ID, + ALERT_TYPES_CONFIG, +} from '../../../common/alert_types'; import { PROCESSOR_EVENT, SERVICE_NAME, @@ -77,7 +81,7 @@ export function registerTransactionDurationAlertType({ apmActionVariables.interval, ], }, - producer: 'apm', + producer: APM_SERVER_FEATURE_ID, minimumLicenseRequired: 'basic', isExportable: true, executor: async ({ services, params }) => { diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts index 718ffd9c92167..36fd9c3fac58d 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts @@ -17,7 +17,11 @@ import { getEnvironmentLabel, } from '../../../common/environment_filter_values'; import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; -import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; +import { + AlertType, + ALERT_TYPES_CONFIG, + APM_SERVER_FEATURE_ID, +} from '../../../common/alert_types'; import { EVENT_OUTCOME, PROCESSOR_EVENT, @@ -75,7 +79,7 @@ export function registerTransactionErrorRateAlertType({ apmActionVariables.interval, ], }, - producer: 'apm', + producer: APM_SERVER_FEATURE_ID, minimumLicenseRequired: 'basic', isExportable: true, executor: async ({ services, params: alertParams }) => { diff --git a/x-pack/plugins/apm/server/lib/alerts/test_utils/index.ts b/x-pack/plugins/apm/server/lib/alerts/test_utils/index.ts index 9dc22844bb629..679f33707b5b5 100644 --- a/x-pack/plugins/apm/server/lib/alerts/test_utils/index.ts +++ b/x-pack/plugins/apm/server/lib/alerts/test_utils/index.ts @@ -10,7 +10,7 @@ import { of } from 'rxjs'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; import type { RuleDataClient } from '../../../../../rule_registry/server'; import { PluginSetupContract as AlertingPluginSetupContract } from '../../../../../alerting/server'; -import { APMConfig } from '../../..'; +import { APMConfig, APM_SERVER_FEATURE_ID } from '../../..'; export const createRuleTypeMocks = () => { let alertExecutor: (...args: any[]) => Promise; @@ -38,6 +38,9 @@ export const createRuleTypeMocks = () => { const services = { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), + savedObjectsClient: { + get: () => ({ attributes: { consumer: APM_SERVER_FEATURE_ID } }), + }, alertInstanceFactory: jest.fn(() => ({ scheduleActions })), alertWithLifecycle: jest.fn(), logger: loggerMock, @@ -68,6 +71,13 @@ export const createRuleTypeMocks = () => { return alertExecutor({ services, params, + rule: { + consumer: APM_SERVER_FEATURE_ID, + name: 'name', + producer: 'producer', + ruleTypeId: 'ruleTypeId', + ruleTypeName: 'ruleTypeName', + }, startedAt: new Date(), }); }, diff --git a/x-pack/plugins/apm/server/lib/fleet/create_cloud_apm_package_policy.ts b/x-pack/plugins/apm/server/lib/fleet/create_cloud_apm_package_policy.ts index 9e3095a8d1bca..c336e5dc95ba6 100644 --- a/x-pack/plugins/apm/server/lib/fleet/create_cloud_apm_package_policy.ts +++ b/x-pack/plugins/apm/server/lib/fleet/create_cloud_apm_package_policy.ts @@ -14,15 +14,20 @@ import { APM_SERVER_SCHEMA_SAVED_OBJECT_TYPE, APM_SERVER_SCHEMA_SAVED_OBJECT_ID, } from '../../../common/apm_saved_object_constants'; -import { APMPluginStartDependencies } from '../../types'; +import { + APMPluginSetupDependencies, + APMPluginStartDependencies, +} from '../../types'; import { getApmPackagePolicyDefinition } from './get_apm_package_policy_definition'; export async function createCloudApmPackgePolicy({ + cloudPluginSetup, fleetPluginStart, savedObjectsClient, esClient, logger, }: { + cloudPluginSetup: APMPluginSetupDependencies['cloud']; fleetPluginStart: NonNullable; savedObjectsClient: SavedObjectsClientContract; esClient: ElasticsearchClient; @@ -35,9 +40,10 @@ export async function createCloudApmPackgePolicy({ const apmServerSchema: Record = JSON.parse( (attributes as { schemaJson: string }).schemaJson ); - const apmPackagePolicyDefinition = getApmPackagePolicyDefinition( - apmServerSchema - ); + const apmPackagePolicyDefinition = getApmPackagePolicyDefinition({ + apmServerSchema, + cloudPluginSetup, + }); logger.info(`Fleet migration on Cloud - apmPackagePolicy create start`); const apmPackagePolicy = await fleetPluginStart.packagePolicyService.create( savedObjectsClient, diff --git a/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts b/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts index fb88a092cb265..291b2fa2af99d 100644 --- a/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts +++ b/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts @@ -5,16 +5,21 @@ * 2.0. */ +import { APMPluginSetupDependencies } from '../../types'; import { POLICY_ELASTIC_AGENT_ON_CLOUD, APM_PACKAGE_NAME, } from './get_cloud_apm_package_policy'; +interface GetApmPackagePolicyDefinitionOptions { + apmServerSchema: Record; + cloudPluginSetup: APMPluginSetupDependencies['cloud']; +} export function getApmPackagePolicyDefinition( - apmServerSchema: Record + options: GetApmPackagePolicyDefinitionOptions ) { return { - name: 'apm', + name: 'Elastic APM', namespace: 'default', enabled: true, policy_id: POLICY_ELASTIC_AGENT_ON_CLOUD, @@ -24,27 +29,28 @@ export function getApmPackagePolicyDefinition( type: 'apm', enabled: true, streams: [], - vars: getApmPackageInputVars(apmServerSchema), + vars: getApmPackageInputVars(options), }, ], package: { name: APM_PACKAGE_NAME, - version: '0.3.0-dev.1', + version: '0.3.0', title: 'Elastic APM', }, }; } -function getApmPackageInputVars(apmServerSchema: Record) { +function getApmPackageInputVars(options: GetApmPackagePolicyDefinitionOptions) { + const { apmServerSchema } = options; const apmServerConfigs = Object.entries( apmConfigMapping - ).map(([key, { name, type }]) => ({ key, name, type })); + ).map(([key, { name, type, getValue }]) => ({ key, name, type, getValue })); const inputVars: Record< string, { type: string; value: any } - > = apmServerConfigs.reduce((acc, { key, name, type }) => { - const value = apmServerSchema[key] ?? ''; // defaults to an empty string to be edited in Fleet UI + > = apmServerConfigs.reduce((acc, { key, name, type, getValue }) => { + const value = (getValue ? getValue(options) : apmServerSchema[key]) ?? ''; // defaults to an empty string to be edited in Fleet UI return { ...acc, [name]: { type, value }, @@ -55,7 +61,11 @@ function getApmPackageInputVars(apmServerSchema: Record) { export const apmConfigMapping: Record< string, - { name: string; type: string } + { + name: string; + type: string; + getValue?: (options: GetApmPackagePolicyDefinitionOptions) => any; + } > = { 'apm-server.host': { name: 'host', @@ -64,6 +74,7 @@ export const apmConfigMapping: Record< 'apm-server.url': { name: 'url', type: 'text', + getValue: ({ cloudPluginSetup }) => cloudPluginSetup?.apm?.url, }, 'apm-server.secret_token': { name: 'secret_token', diff --git a/x-pack/plugins/apm/server/lib/fleet/sync_agent_configs_to_apm_package_policies.ts b/x-pack/plugins/apm/server/lib/fleet/sync_agent_configs_to_apm_package_policies.ts index 4294c5b82cd63..1365ddc28ddb2 100644 --- a/x-pack/plugins/apm/server/lib/fleet/sync_agent_configs_to_apm_package_policies.ts +++ b/x-pack/plugins/apm/server/lib/fleet/sync_agent_configs_to_apm_package_policies.ts @@ -10,6 +10,7 @@ import { CoreStart, SavedObjectsClientContract, } from 'kibana/server'; +import { TelemetryUsageCounter } from '../../routes/typings'; import { APMPluginStartDependencies } from '../../types'; import { getInternalSavedObjectsClient } from '../helpers/get_internal_saved_objects_client'; import { Setup } from '../helpers/setup_request'; @@ -21,11 +22,19 @@ export async function syncAgentConfigsToApmPackagePolicies({ core, fleetPluginStart, setup, + telemetryUsageCounter, }: { core: { setup: CoreSetup; start: () => Promise }; fleetPluginStart: NonNullable; setup: Setup; + telemetryUsageCounter?: TelemetryUsageCounter; }) { + if (telemetryUsageCounter) { + telemetryUsageCounter.incrementCounter({ + counterName: 'sync_agent_config_to_apm_package_policies', + counterType: 'success', + }); + } const coreStart = await core.start(); const esClient = coreStart.elasticsearch.client.asInternalUser; const [ diff --git a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts index 607a7e6227a9d..a2944d6241d2d 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts @@ -19,7 +19,8 @@ export async function createStaticIndexPattern( setup: Setup, config: APMRouteHandlerResources['config'], savedObjectsClient: InternalSavedObjectsClient, - spaceId: string | undefined + spaceId: string | undefined, + overwrite = false ): Promise { return withApmSpan('create_static_index_pattern', async () => { // don't autocreate APM index pattern if it's been disabled via the config @@ -45,7 +46,7 @@ export async function createStaticIndexPattern( }, { id: APM_STATIC_INDEX_PATTERN_ID, - overwrite: false, + overwrite, namespace: spaceId, } ) diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index 647330eade1f5..3a7eb738dd3b2 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -15,10 +15,10 @@ import { Plugin, PluginInitializerContext, } from 'src/core/server'; -import { mapValues, once } from 'lodash'; +import { isEmpty, mapValues, once } from 'lodash'; import { TECHNICAL_COMPONENT_TEMPLATE_NAME } from '../../rule_registry/common/assets'; import { mappingFromFieldMap } from '../../rule_registry/common/mapping_from_field_map'; -import { APMConfig, APMXPackConfig } from '.'; +import { APMConfig, APMXPackConfig, APM_SERVER_FEATURE_ID } from '.'; import { mergeConfigs } from './index'; import { UI_SETTINGS } from '../../../../src/plugins/data/common'; import { APM_FEATURE, registerFeaturesUsage } from './feature'; @@ -104,21 +104,6 @@ export class APMPlugin }); } - plugins.home?.tutorials.registerTutorial( - tutorialProvider({ - isEnabled: this.currentConfig['xpack.apm.ui.enabled'], - indexPatternTitle: this.currentConfig['apm_oss.indexPattern'], - cloud: plugins.cloud, - indices: { - errorIndices: this.currentConfig['apm_oss.errorIndices'], - metricsIndices: this.currentConfig['apm_oss.metricsIndices'], - onboardingIndices: this.currentConfig['apm_oss.onboardingIndices'], - sourcemapIndices: this.currentConfig['apm_oss.sourcemapIndices'], - transactionIndices: this.currentConfig['apm_oss.transactionIndices'], - }, - }) - ); - plugins.features.registerKibanaFeature(APM_FEATURE); registerFeaturesUsage({ licensingPlugin: plugins.licensing }); @@ -188,6 +173,7 @@ export class APMPlugin ); const ruleDataClient = ruleDataService.getRuleDataClient( + APM_SERVER_FEATURE_ID, ruleDataService.getFullAssetName('observability-apm'), () => initializeRuleDataTemplatesPromise ); @@ -205,8 +191,24 @@ export class APMPlugin }; }) as APMRouteHandlerResources['plugins']; + plugins.home?.tutorials.registerTutorial( + tutorialProvider({ + isEnabled: this.currentConfig['xpack.apm.ui.enabled'], + indexPatternTitle: this.currentConfig['apm_oss.indexPattern'], + cloud: plugins.cloud, + isFleetPluginEnabled: !isEmpty(resourcePlugins.fleet), + indices: { + errorIndices: this.currentConfig['apm_oss.errorIndices'], + metricsIndices: this.currentConfig['apm_oss.metricsIndices'], + onboardingIndices: this.currentConfig['apm_oss.onboardingIndices'], + sourcemapIndices: this.currentConfig['apm_oss.sourcemapIndices'], + transactionIndices: this.currentConfig['apm_oss.transactionIndices'], + }, + }) + ); + const telemetryUsageCounter = resourcePlugins.usageCollection?.setup.createUsageCounter( - 'apm' + APM_SERVER_FEATURE_ID ); registerRoutes({ diff --git a/x-pack/plugins/apm/server/routes/fleet.ts b/x-pack/plugins/apm/server/routes/fleet.ts index 6208b42e844fa..b760014d6af89 100644 --- a/x-pack/plugins/apm/server/routes/fleet.ts +++ b/x-pack/plugins/apm/server/routes/fleet.ts @@ -25,6 +25,8 @@ import { createCloudApmPackgePolicy } from '../lib/fleet/create_cloud_apm_packag import { getUnsupportedApmServerSchema } from '../lib/fleet/get_unsupported_apm_server_schema'; import { isSuperuser } from '../lib/fleet/is_superuser'; import { getInternalSavedObjectsClient } from '../lib/helpers/get_internal_saved_objects_client'; +import { setupRequest } from '../lib/helpers/setup_request'; +import { createStaticIndexPattern } from '../lib/index_pattern/create_static_index_pattern'; const hasFleetDataRoute = createApmServerRoute({ endpoint: 'GET /api/apm/fleet/has_data', @@ -32,7 +34,7 @@ const hasFleetDataRoute = createApmServerRoute({ handler: async ({ core, plugins }) => { const fleetPluginStart = await plugins.fleet?.start(); if (!fleetPluginStart) { - throw Boom.internal(FLEET_REQUIRED_MESSAGE); + return { hasData: false }; } const packagePolicies = await getApmPackgePolicies({ core, @@ -56,7 +58,7 @@ const fleetAgentsRoute = createApmServerRoute({ const fleetPluginStart = await plugins.fleet?.start(); if (!fleetPluginStart) { - throw Boom.internal(FLEET_REQUIRED_MESSAGE); + return { cloudStandaloneSetup, fleetAgents: [], isFleetEnabled: false }; } // fetches package policies that contains APM integrations const packagePolicies = await getApmPackgePolicies({ @@ -75,6 +77,7 @@ const fleetAgentsRoute = createApmServerRoute({ return { cloudStandaloneSetup, + isFleetEnabled: true, fleetAgents: fleetAgents.map((agent) => { const packagePolicy = policiesGroupedById[agent.id]; const packagePolicyVars = packagePolicy.inputs[0]?.vars; @@ -153,7 +156,7 @@ const createCloudApmPackagePolicyRoute = createApmServerRoute({ endpoint: 'POST /api/apm/fleet/cloud_apm_package_policy', options: { tags: ['access:apm', 'access:apm_write'] }, handler: async (resources) => { - const { plugins, context, config, request, logger } = resources; + const { plugins, context, config, request, logger, core } = resources; const cloudApmMigrationEnabled = config['xpack.apm.agent.migrations.enabled']; if (!plugins.fleet || !plugins.security) { @@ -163,20 +166,41 @@ const createCloudApmPackagePolicyRoute = createApmServerRoute({ const coreStart = await resources.core.start(); const esClient = coreStart.elasticsearch.client.asScoped(resources.request) .asCurrentUser; + const cloudPluginSetup = plugins.cloud?.setup; const fleetPluginStart = await plugins.fleet.start(); const securityPluginStart = await plugins.security.start(); const hasRequiredRole = isSuperuser({ securityPluginStart, request }); if (!hasRequiredRole || !cloudApmMigrationEnabled) { throw Boom.forbidden(CLOUD_SUPERUSER_REQUIRED_MESSAGE); } - return { - cloud_apm_package_policy: await createCloudApmPackgePolicy({ - fleetPluginStart, - savedObjectsClient, - esClient, - logger, - }), - }; + + const cloudApmAackagePolicy = await createCloudApmPackgePolicy({ + cloudPluginSetup, + fleetPluginStart, + savedObjectsClient, + esClient, + logger, + }); + + const [setup, internalSavedObjectsClient] = await Promise.all([ + setupRequest(resources), + core + .start() + .then(({ savedObjects }) => savedObjects.createInternalRepository()), + ]); + + const spaceId = plugins.spaces?.setup.spacesService.getSpaceId(request); + + // force update the index pattern title with data streams + await createStaticIndexPattern( + setup, + config, + internalSavedObjectsClient, + spaceId, + true + ); + + return { cloud_apm_package_policy: cloudApmAackagePolicy }; }, }); @@ -188,11 +212,6 @@ export const apmFleetRouteRepository = createApmServerRouteRepository() .add(getMigrationCheckRoute) .add(createCloudApmPackagePolicyRoute); -const FLEET_REQUIRED_MESSAGE = i18n.translate( - 'xpack.apm.fleet_has_data.fleetRequired', - { defaultMessage: `Fleet plugin is required` } -); - const FLEET_SECURITY_REQUIRED_MESSAGE = i18n.translate( 'xpack.apm.api.fleet.fleetSecurityRequired', { defaultMessage: `Fleet and Security plugins are required` } diff --git a/x-pack/plugins/apm/server/routes/register_routes/index.test.ts b/x-pack/plugins/apm/server/routes/register_routes/index.test.ts index 158d7ee7e76a3..b9dece866fae5 100644 --- a/x-pack/plugins/apm/server/routes/register_routes/index.test.ts +++ b/x-pack/plugins/apm/server/routes/register_routes/index.test.ts @@ -109,6 +109,11 @@ const initApi = ( params: {}, query: {}, body: null, + events: { + aborted$: { + toPromise: () => new Promise(() => {}), + }, + }, ...request, }, responseMock @@ -202,7 +207,7 @@ describe('createApi', () => { describe('when validating', () => { describe('_inspect', () => { it('allows _inspect=true', async () => { - const handlerMock = jest.fn(); + const handlerMock = jest.fn().mockResolvedValue({}); const { simulateRequest, mocks: { response }, @@ -234,7 +239,7 @@ describe('createApi', () => { }); it('rejects _inspect=1', async () => { - const handlerMock = jest.fn(); + const handlerMock = jest.fn().mockResolvedValue({}); const { simulateRequest, @@ -267,7 +272,7 @@ describe('createApi', () => { }); it('allows omitting _inspect', async () => { - const handlerMock = jest.fn(); + const handlerMock = jest.fn().mockResolvedValue({}); const { simulateRequest, @@ -297,7 +302,11 @@ describe('createApi', () => { simulateRequest, mocks: { response }, } = initApi([ - { endpoint: 'GET /foo', options: { tags: [] }, handler: jest.fn() }, + { + endpoint: 'GET /foo', + options: { tags: [] }, + handler: jest.fn().mockResolvedValue({}), + }, ]); await simulateRequest({ @@ -328,7 +337,7 @@ describe('createApi', () => { }); it('validates path parameters', async () => { - const handlerMock = jest.fn(); + const handlerMock = jest.fn().mockResolvedValue({}); const { simulateRequest, mocks: { response }, @@ -402,7 +411,7 @@ describe('createApi', () => { }); it('validates body parameters', async () => { - const handlerMock = jest.fn(); + const handlerMock = jest.fn().mockResolvedValue({}); const { simulateRequest, mocks: { response }, @@ -448,7 +457,7 @@ describe('createApi', () => { }); it('validates query parameters', async () => { - const handlerMock = jest.fn(); + const handlerMock = jest.fn().mockResolvedValue({}); const { simulateRequest, mocks: { response }, diff --git a/x-pack/plugins/apm/server/routes/register_routes/index.ts b/x-pack/plugins/apm/server/routes/register_routes/index.ts index 136f3c73d8046..16e77f59f4d02 100644 --- a/x-pack/plugins/apm/server/routes/register_routes/index.ts +++ b/x-pack/plugins/apm/server/routes/register_routes/index.ts @@ -18,9 +18,12 @@ import { routeValidationObject, } from '@kbn/server-route-repository'; import { mergeRt, jsonRt } from '@kbn/io-ts-utils'; -import { UsageCollectionSetup } from '../../../../../../src/plugins/usage_collection/server'; import { pickKeys } from '../../../common/utils/pick_keys'; -import { APMRouteHandlerResources, InspectResponse } from '../typings'; +import { + APMRouteHandlerResources, + InspectResponse, + TelemetryUsageCounter, +} from '../typings'; import type { ApmPluginRequestHandlerContext } from '../typings'; const inspectRt = t.exact( @@ -29,6 +32,13 @@ const inspectRt = t.exact( }) ); +const CLIENT_CLOSED_REQUEST = { + statusCode: 499, + body: { + message: 'Client closed request', + }, +}; + export const inspectableEsQueriesMap = new WeakMap< KibanaRequest, InspectResponse @@ -49,9 +59,7 @@ export function registerRoutes({ repository: ServerRouteRepository; config: APMRouteHandlerResources['config']; ruleDataClient: APMRouteHandlerResources['ruleDataClient']; - telemetryUsageCounter?: ReturnType< - UsageCollectionSetup['createUsageCounter'] - >; + telemetryUsageCounter?: TelemetryUsageCounter; }) { const routes = repository.getRoutes(); @@ -89,23 +97,41 @@ export function registerRoutes({ runtimeType ); - const data: Record | undefined | null = (await handler({ - request, - context, - config, - logger, - core, - plugins, - params: merge( - { - query: { - _inspect: false, + const { aborted, data } = await Promise.race([ + handler({ + request, + context, + config, + logger, + core, + plugins, + telemetryUsageCounter, + params: merge( + { + query: { + _inspect: false, + }, }, - }, - validatedParams - ), - ruleDataClient, - })) as any; + validatedParams + ), + ruleDataClient, + }).then((value) => { + return { + aborted: false, + data: value as Record | undefined | null, + }; + }), + request.events.aborted$.toPromise().then(() => { + return { + aborted: true, + data: undefined, + }; + }), + ]); + + if (aborted) { + return response.custom(CLIENT_CLOSED_REQUEST); + } if (Array.isArray(data)) { throw new Error('Return type cannot be an array'); @@ -118,9 +144,6 @@ export function registerRoutes({ } : { ...data }; - // cleanup - inspectableEsQueriesMap.delete(request); - if (!options.disableTelemetry && telemetryUsageCounter) { telemetryUsageCounter.incrementCounter({ counterName: `${method.toUpperCase()} ${pathname}`, @@ -131,6 +154,7 @@ export function registerRoutes({ return response.ok({ body }); } catch (error) { logger.error(error); + if (!options.disableTelemetry && telemetryUsageCounter) { telemetryUsageCounter.incrementCounter({ counterName: `${method.toUpperCase()} ${pathname}`, @@ -147,16 +171,18 @@ export function registerRoutes({ }, }; - if (Boom.isBoom(error)) { - opts.statusCode = error.output.statusCode; + if (error instanceof RequestAbortedError) { + return response.custom(merge(opts, CLIENT_CLOSED_REQUEST)); } - if (error instanceof RequestAbortedError) { - opts.statusCode = 499; - opts.body.message = 'Client closed request'; + if (Boom.isBoom(error)) { + opts.statusCode = error.output.statusCode; } return response.custom(opts); + } finally { + // cleanup + inspectableEsQueriesMap.delete(request); } } ); diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts index 05eec47893793..f50770cb5ded7 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts @@ -79,7 +79,7 @@ const deleteAgentConfigurationRoute = createApmServerRoute({ }), handler: async (resources) => { const setup = await setupRequest(resources); - const { params, logger, core } = resources; + const { params, logger, core, telemetryUsageCounter } = resources; const { service } = params.body; @@ -106,6 +106,7 @@ const deleteAgentConfigurationRoute = createApmServerRoute({ core, fleetPluginStart: await resources.plugins.fleet.start(), setup, + telemetryUsageCounter, }); logger.info( `Updated Fleet integration policy for APM to remove the deleted agent configuration.` @@ -128,7 +129,7 @@ const createOrUpdateAgentConfigurationRoute = createApmServerRoute({ ]), handler: async (resources) => { const setup = await setupRequest(resources); - const { params, logger, core } = resources; + const { params, logger, core, telemetryUsageCounter } = resources; const { body, query } = params; // if the config already exists, it is fetched and updated @@ -162,6 +163,7 @@ const createOrUpdateAgentConfigurationRoute = createApmServerRoute({ core, fleetPluginStart: await resources.plugins.fleet.start(), setup, + telemetryUsageCounter, }); logger.info( `Saved latest agent settings to Fleet integration policy for APM.` diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts index 98c2ee47b5633..4279cfd84328c 100644 --- a/x-pack/plugins/apm/server/routes/typings.ts +++ b/x-pack/plugins/apm/server/routes/typings.ts @@ -14,13 +14,16 @@ import { } from 'src/core/server'; import { RuleDataClient } from '../../../rule_registry/server'; import { AlertingApiRequestHandlerContext } from '../../../alerting/server'; +import type { RacApiRequestHandlerContext } from '../../../rule_registry/server'; import { LicensingApiRequestHandlerContext } from '../../../licensing/server'; import { APMConfig } from '..'; import { APMPluginDependencies } from '../types'; +import { UsageCollectionSetup } from '../../../../../src/plugins/usage_collection/server'; export interface ApmPluginRequestHandlerContext extends RequestHandlerContext { licensing: LicensingApiRequestHandlerContext; alerting: AlertingApiRequestHandlerContext; + rac: RacApiRequestHandlerContext; } export type InspectResponse = Array<{ @@ -45,6 +48,10 @@ export interface APMRouteCreateOptions { }; } +export type TelemetryUsageCounter = ReturnType< + UsageCollectionSetup['createUsageCounter'] +>; + export interface APMRouteHandlerResources { request: KibanaRequest; context: ApmPluginRequestHandlerContext; @@ -66,4 +73,5 @@ export interface APMRouteHandlerResources { }; }; ruleDataClient: RuleDataClient; + telemetryUsageCounter?: TelemetryUsageCounter; } diff --git a/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts b/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts index 55adc756f31af..a595ae1dc8a8b 100644 --- a/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts +++ b/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts @@ -46,7 +46,8 @@ export function createElasticCloudInstructions( function getApmServerInstructionSet( cloudSetup?: CloudSetup ): InstructionSetSchema { - const cloudId = cloudSetup?.cloudId; + const deploymentId = cloudSetup?.deploymentId; + return { title: i18n.translate('xpack.apm.tutorial.apmServer.title', { defaultMessage: 'APM Server', @@ -59,8 +60,8 @@ function getApmServerInstructionSet( title: 'Enable the APM Server in the ESS console', textPre: i18n.translate('xpack.apm.tutorial.elasticCloud.textPre', { defaultMessage: - 'To enable the APM Server go to [the Elastic Cloud console](https://cloud.elastic.co/deployments?q={cloudId}) and enable APM in the deployment settings. Once enabled, refresh this page.', - values: { cloudId }, + 'To enable the APM Server go to [the Elastic Cloud console](https://cloud.elastic.co/deployments/{deploymentId}/edit) and enable APM in the deployment settings. Once enabled, refresh this page.', + values: { deploymentId }, }), }, ], diff --git a/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts b/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts index 882d45c4c21db..400da79e3d2d0 100644 --- a/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts +++ b/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts @@ -38,12 +38,14 @@ export function onPremInstructions({ metricsIndices, sourcemapIndices, onboardingIndices, + isFleetPluginEnabled, }: { errorIndices: string; transactionIndices: string; metricsIndices: string; sourcemapIndices: string; onboardingIndices: string; + isFleetPluginEnabled: boolean; }): InstructionsSchema { const EDIT_CONFIG = createEditConfig(); const START_SERVER_UNIX = createStartServerUnix(); @@ -69,12 +71,17 @@ export function onPremInstructions({ iconType: 'alert', }, instructionVariants: [ - { - id: INSTRUCTION_VARIANT.FLEET, - instructions: [ - { customComponentName: 'TutorialFleetInstructions' }, - ], - }, + // hides fleet section when plugin is disabled + ...(isFleetPluginEnabled + ? [ + { + id: INSTRUCTION_VARIANT.FLEET, + instructions: [ + { customComponentName: 'TutorialFleetInstructions' }, + ], + }, + ] + : []), { id: INSTRUCTION_VARIANT.OSX, instructions: [ diff --git a/x-pack/plugins/apm/server/tutorial/index.ts b/x-pack/plugins/apm/server/tutorial/index.ts index 9118c30b845d0..edf056a6d1be4 100644 --- a/x-pack/plugins/apm/server/tutorial/index.ts +++ b/x-pack/plugins/apm/server/tutorial/index.ts @@ -28,6 +28,7 @@ export const tutorialProvider = ({ indexPatternTitle, indices, cloud, + isFleetPluginEnabled, }: { isEnabled: boolean; indexPatternTitle: string; @@ -39,6 +40,7 @@ export const tutorialProvider = ({ sourcemapIndices: string; onboardingIndices: string; }; + isFleetPluginEnabled: boolean; }) => () => { const savedObjects = [ { @@ -104,7 +106,7 @@ It allows you to monitor the performance of thousands of applications in real ti euiIconType: 'apmApp', artifacts, customStatusCheckName: 'apm_fleet_server_status_check', - onPrem: onPremInstructions(indices), + onPrem: onPremInstructions({ ...indices, isFleetPluginEnabled }), elasticCloud: createElasticCloudInstructions(cloud), previewImagePath: '/plugins/apm/assets/apm.png', savedObjects, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/urlparam.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/urlparam.ts index c8ce7717cd2d2..9d56278e2c324 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/urlparam.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/urlparam.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { parse } from 'url'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp } from '../../../i18n'; @@ -43,8 +42,9 @@ export function urlparam(): ExpressionFunctionDefinition< }, }, fn: (input, args) => { - const query = parse(window.location.href, true).query; - return query[args.param] || args.default; + const url = new URL(window.location.href); + const query = url.searchParams; + return query.get(args.param) || args.default; }, }; } diff --git a/x-pack/plugins/canvas/public/components/function_reference_generator/function_reference_generator.tsx b/x-pack/plugins/canvas/public/components/function_reference_generator/function_reference_generator.tsx index 81532816d9c83..eb394801f549c 100644 --- a/x-pack/plugins/canvas/public/components/function_reference_generator/function_reference_generator.tsx +++ b/x-pack/plugins/canvas/public/components/function_reference_generator/function_reference_generator.tsx @@ -29,7 +29,7 @@ export const FunctionReferenceGenerator: FC = ({ functionRegistry }) => { }; return ( - + Generate function reference ); diff --git a/x-pack/plugins/canvas/public/components/help_menu/help_menu.tsx b/x-pack/plugins/canvas/public/components/help_menu/help_menu.tsx index 2877ccf41056d..af1850beb5290 100644 --- a/x-pack/plugins/canvas/public/components/help_menu/help_menu.tsx +++ b/x-pack/plugins/canvas/public/components/help_menu/help_menu.tsx @@ -46,13 +46,13 @@ export const HelpMenu: FC = ({ functionRegistry }) => { return ( <> - + {strings.getKeyboardShortcutsLinkLabel()} {FunctionReferenceGenerator ? ( - + ) : null} diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__stories__/__snapshots__/edit_menu.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__stories__/__snapshots__/edit_menu.stories.storyshot index cc33ae3526c0c..f2bc9c57cbcc6 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__stories__/__snapshots__/edit_menu.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__stories__/__snapshots__/edit_menu.stories.storyshot @@ -9,7 +9,7 @@ exports[`Storyshots components/WorkpadHeader/EditMenu 2 elements selected 1`] = >
); diff --git a/x-pack/plugins/cases/public/components/user_action_tree/index.test.tsx b/x-pack/plugins/cases/public/components/user_action_tree/index.test.tsx index 610399c31928b..be1516843184d 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/index.test.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/index.test.tsx @@ -13,7 +13,14 @@ import routeData from 'react-router'; import { getFormMock, useFormMock, useFormDataMock } from '../__mock__/form'; import { useUpdateComment } from '../../containers/use_update_comment'; -import { basicCase, basicPush, getUserAction } from '../../containers/mock'; +import { + basicCase, + basicPush, + getUserAction, + getHostIsolationUserAction, + hostIsolationComment, + hostReleaseComment, +} from '../../containers/mock'; import { UserActionTree } from '.'; import { TestProviders } from '../../common/mock'; import { Ecs } from '../../../common'; @@ -368,4 +375,82 @@ describe(`UserActionTree`, () => { ).toEqual(true); }); }); + describe('Host isolation action', () => { + it('renders in the cases details view', async () => { + const isolateAction = [getHostIsolationUserAction()]; + const props = { + ...defaultProps, + caseUserActions: isolateAction, + data: { ...defaultProps.data, comments: [...basicCase.comments, hostIsolationComment()] }, + }; + + const wrapper = mount( + + + + ); + await waitFor(() => { + expect(wrapper.find(`[data-test-subj="endpoint-action"]`).exists()).toBe(true); + }); + }); + + it('shows the correct username', async () => { + const isolateAction = [getHostIsolationUserAction()]; + const props = { + ...defaultProps, + caseUserActions: isolateAction, + data: { ...defaultProps.data, comments: [hostIsolationComment()] }, + }; + + const wrapper = mount( + + + + ); + await waitFor(() => { + expect(wrapper.find(`[data-test-subj="user-action-avatar"]`).first().prop('name')).toEqual( + defaultProps.data.createdBy.fullName + ); + }); + }); + + it('shows a lock icon if the action is isolate', async () => { + const isolateAction = [getHostIsolationUserAction()]; + const props = { + ...defaultProps, + caseUserActions: isolateAction, + data: { ...defaultProps.data, comments: [hostIsolationComment()] }, + }; + + const wrapper = mount( + + + + ); + await waitFor(() => { + expect( + wrapper.find(`[data-test-subj="endpoint-action"]`).first().prop('timelineIcon') + ).toBe('lock'); + }); + }); + it('shows a lockOpen icon if the action is unisolate/release', async () => { + const isolateAction = [getHostIsolationUserAction()]; + const props = { + ...defaultProps, + caseUserActions: isolateAction, + data: { ...defaultProps.data, comments: [hostReleaseComment()] }, + }; + + const wrapper = mount( + + + + ); + await waitFor(() => { + expect( + wrapper.find(`[data-test-subj="endpoint-action"]`).first().prop('timelineIcon') + ).toBe('lockOpen'); + }); + }); + }); }); diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_host_isolation_comment_event.test.tsx b/x-pack/plugins/cases/public/components/user_action_tree/user_action_host_isolation_comment_event.test.tsx new file mode 100644 index 0000000000000..636cd7e40aac1 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_action_tree/user_action_host_isolation_comment_event.test.tsx @@ -0,0 +1,42 @@ +/* + * 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 { mount } from 'enzyme'; +import { HostIsolationCommentEvent } from './user_action_host_isolation_comment_event'; + +const defaultProps = () => { + return { + type: 'isolate', + endpoints: [{ endpointId: 'e1', hostname: 'host1' }], + href: jest.fn(), + onClick: jest.fn(), + }; +}; + +describe('UserActionHostIsolationCommentEvent', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders with the correct action and hostname', async () => { + const wrapper = mount(); + expect(wrapper.find(`[data-test-subj="actions-link-e1"]`).first().exists()).toBeTruthy(); + expect(wrapper.text()).toBe('isolated host host1'); + }); + + it('navigates to app on link click', async () => { + const onActionsLinkClick = jest.fn(); + + const wrapper = mount( + + ); + + wrapper.find(`[data-test-subj="actions-link-e1"]`).first().simulate('click'); + expect(onActionsLinkClick).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_host_isolation_comment_event.tsx b/x-pack/plugins/cases/public/components/user_action_tree/user_action_host_isolation_comment_event.tsx index d363e874a4e0d..2381d31b3ada8 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_host_isolation_comment_event.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/user_action_host_isolation_comment_event.tsx @@ -44,7 +44,7 @@ const HostIsolationCommentEventComponent: React.FC = ({ {endpoints[0].hostname} diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts index a900010235c9f..c955bb34240e2 100644 --- a/x-pack/plugins/cases/public/containers/mock.ts +++ b/x-pack/plugins/cases/public/containers/mock.ts @@ -76,6 +76,58 @@ export const alertComment: Comment = { version: 'WzQ3LDFc', }; +export const hostIsolationComment: () => Comment = () => { + return { + type: CommentType.actions, + comment: 'I just isolated the host!', + id: 'isolate-comment-id', + actions: { + targets: [ + { + hostname: 'host1', + endpointId: '001', + }, + ], + type: 'isolate', + }, + associationType: AssociationType.case, + createdAt: basicCreatedAt, + createdBy: elasticUser, + owner: SECURITY_SOLUTION_OWNER, + pushedAt: null, + pushedBy: null, + updatedAt: null, + updatedBy: null, + version: 'WzQ3LDFc', + }; +}; + +export const hostReleaseComment: () => Comment = () => { + return { + type: CommentType.actions, + comment: 'I just released the host!', + id: 'isolate-comment-id', + actions: { + targets: [ + { + hostname: 'host1', + endpointId: '001', + }, + ], + type: 'unisolate', + }, + associationType: AssociationType.case, + createdAt: basicCreatedAt, + createdBy: elasticUser, + owner: SECURITY_SOLUTION_OWNER, + pushedAt: null, + pushedBy: null, + updatedAt: null, + updatedBy: null, + version: 'WzQ3LDFc', + }; +}; + export const basicCase: Case = { type: CaseType.individual, owner: SECURITY_SOLUTION_OWNER, @@ -374,6 +426,15 @@ export const getAlertUserAction = () => ({ newValue: '{"type":"alert","alertId":"alert-id-1","index":"index-id-1"}', }); +export const getHostIsolationUserAction = () => ({ + ...basicAction, + actionId: 'isolate-action-id', + actionField: ['comment'] as UserActionField, + action: 'create' as UserAction, + commentId: 'isolate-comment-id', + newValue: 'some value', +}); + export const caseUserActions: CaseUserActions[] = [ getUserAction(['description'], 'create'), getUserAction(['comment'], 'create'), diff --git a/x-pack/plugins/cases/server/client/cases/mock.ts b/x-pack/plugins/cases/server/client/cases/mock.ts index 23db57c6d3097..313d6cd12a6db 100644 --- a/x-pack/plugins/cases/server/client/cases/mock.ts +++ b/x-pack/plugins/cases/server/client/cases/mock.ts @@ -52,6 +52,106 @@ export const comment: CommentResponse = { version: 'WzEsMV0=', }; +export const isolateCommentActions: CommentResponse = { + associationType: AssociationType.case, + id: 'mock-action-comment-1', + comment: 'Isolating this for investigation', + type: CommentType.actions as const, + created_at: '2019-11-25T21:55:00.177Z', + actions: { + targets: [ + { + endpointId: '123', + hostname: 'windows-host-1', + }, + ], + type: 'isolate', + }, + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + owner: SECURITY_SOLUTION_OWNER, + pushed_at: null, + pushed_by: null, + updated_at: '2019-11-25T21:55:00.177Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + version: 'WzEsMV0=', +}; + +export const releaseCommentActions: CommentResponse = { + associationType: AssociationType.case, + id: 'mock-action-comment-1', + comment: 'Releasing this for investigation', + type: CommentType.actions as const, + created_at: '2019-11-25T21:55:00.177Z', + actions: { + targets: [ + { + endpointId: '123', + hostname: 'windows-host-1', + }, + ], + type: 'unisolate', + }, + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + owner: SECURITY_SOLUTION_OWNER, + pushed_at: null, + pushed_by: null, + updated_at: '2019-11-25T21:55:00.177Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + version: 'WzEsMV0=', +}; + +export const isolateCommentActionsMultipleTargets: CommentResponse = { + associationType: AssociationType.case, + id: 'mock-action-comment-1', + comment: 'Isolating this for investigation', + type: CommentType.actions as const, + created_at: '2019-11-25T21:55:00.177Z', + actions: { + targets: [ + { + endpointId: '123', + hostname: 'windows-host-1', + }, + { + endpointId: '456', + hostname: 'windows-host-2', + }, + ], + type: 'isolate', + }, + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + owner: SECURITY_SOLUTION_OWNER, + pushed_at: null, + pushed_by: null, + updated_at: '2019-11-25T21:55:00.177Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + version: 'WzEsMV0=', +}; + export const commentAlert: CommentResponse = { associationType: AssociationType.case, id: 'mock-comment-1', diff --git a/x-pack/plugins/cases/server/client/cases/utils.test.ts b/x-pack/plugins/cases/server/client/cases/utils.test.ts index bfd5d1279420b..d7c45d3e1e9ae 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.test.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.test.ts @@ -18,6 +18,9 @@ import { commentAlert, commentAlertMultipleIds, commentGeneratedAlert, + isolateCommentActions, + releaseCommentActions, + isolateCommentActionsMultipleTargets, } from './mock'; import { @@ -37,6 +40,52 @@ const formatComment = { comment: 'Wow, good luck catching that bad meanie!', }; +const formatIsolateActionComment = { + commentId: isolateCommentActions.id, + comment: 'Isolating this for investigation', + actions: { + targets: [ + { + hostname: 'windows-host-1', + endpointId: '123', + }, + ], + type: 'isolate', + }, +}; + +const formatReleaseActionComment = { + commentId: releaseCommentActions.id, + comment: 'Releasing this for investigation', + actions: { + targets: [ + { + hostname: 'windows-host-1', + endpointId: '123', + }, + ], + type: 'unisolate', + }, +}; + +const formatIsolateCommentActionsMultipleTargets = { + commentId: isolateCommentActionsMultipleTargets.id, + comment: 'Isolating this for investigation', + actions: { + targets: [ + { + hostname: 'windows-host-1', + endpointId: '123', + }, + { + hostname: 'windows-host-2', + endpointId: '456', + }, + ], + type: 'isolate', + }, +}; + const params = { ...basicParams }; describe('utils', () => { @@ -289,6 +338,42 @@ describe('utils', () => { }, ]); }); + + test('transform isolate action comment', () => { + const comments = [isolateCommentActions]; + const res = transformComments(comments, ['informationCreated']); + const actionText = `Isolated host ${formatIsolateActionComment.actions.targets[0].hostname} with comment: ${formatIsolateActionComment.comment}`; + expect(res).toEqual([ + { + commentId: formatIsolateActionComment.commentId, + comment: `${actionText} (created at ${comments[0].created_at} by ${comments[0].created_by.full_name})`, + }, + ]); + }); + + test('transform release action comment', () => { + const comments = [releaseCommentActions]; + const res = transformComments(comments, ['informationCreated']); + const actionText = `Released host ${formatReleaseActionComment.actions.targets[0].hostname} with comment: ${formatReleaseActionComment.comment}`; + expect(res).toEqual([ + { + commentId: formatReleaseActionComment.commentId, + comment: `${actionText} (created at ${comments[0].created_at} by ${comments[0].created_by.full_name})`, + }, + ]); + }); + + test('transform isolate action comment with multiple hosts', () => { + const comments = [isolateCommentActionsMultipleTargets]; + const res = transformComments(comments, ['informationCreated']); + const actionText = `Isolated host ${formatIsolateCommentActionsMultipleTargets.actions.targets[0].hostname} and 1 more with comment: ${formatIsolateCommentActionsMultipleTargets.comment}`; + expect(res).toEqual([ + { + commentId: formatIsolateCommentActionsMultipleTargets.commentId, + comment: `${actionText} (created at ${comments[0].created_at} by ${comments[0].created_by.full_name})`, + }, + ]); + }); }); describe('transformers', () => { @@ -523,8 +608,7 @@ describe('utils', () => { }, ], }, - // Remove second push - userActions: userActions.filter((item, index) => index !== 4), + userActions, connector, mappings: [ ...mappings, @@ -551,7 +635,7 @@ describe('utils', () => { ]); }); - it('it removes alerts correctly', async () => { + it('it filters out the alerts from the comments correctly', async () => { const res = await createIncident({ actionsClient: actionsMock, theCase: { @@ -582,6 +666,32 @@ describe('utils', () => { ]); }); + it('does not add the alerts count comment if all alerts have been pushed', async () => { + const res = await createIncident({ + actionsClient: actionsMock, + theCase: { + ...theCase, + comments: [ + { ...commentObj, id: 'comment-user-1', pushed_at: '2019-11-25T21:55:00.177Z' }, + { ...commentGeneratedAlert, pushed_at: '2019-11-25T21:55:00.177Z' }, + ], + }, + userActions, + connector, + mappings, + alerts: [], + casesConnectors, + }); + + expect(res.comments).toEqual([ + { + comment: + 'Wow, good luck catching that bad meanie! (added at 2019-11-25T21:55:00.177Z by elastic)', + commentId: 'comment-user-1', + }, + ]); + }); + it('updates an existing incident', async () => { const existingIncidentData = { priority: null, @@ -644,22 +754,6 @@ describe('utils', () => { }); }); - it('throws error if connector is not supported', async () => { - expect.assertions(2); - createIncident({ - actionsClient: actionsMock, - theCase, - userActions, - connector: { ...connector, actionTypeId: 'not-supported' }, - mappings, - alerts: [], - casesConnectors, - }).catch((e) => { - expect(e).not.toBeNull(); - expect(e).toEqual(new Error('Invalid external service')); - }); - }); - describe('getLatestPushInfo', () => { it('it returns the latest push information correctly', async () => { const res = getLatestPushInfo('456', userActions); diff --git a/x-pack/plugins/cases/server/client/cases/utils.ts b/x-pack/plugins/cases/server/client/cases/utils.ts index f5a10d705e095..617191462c556 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.ts @@ -19,6 +19,7 @@ import { CommentAttributes, CommentRequestUserType, CommentRequestAlertType, + CommentRequestActionsType, } from '../../../common'; import { ActionsClient } from '../../../../actions/server'; import { CasesClientGetAlertsResponse } from '../../client/alerts/types'; @@ -76,18 +77,69 @@ const getCommentContent = (comment: CommentResponse): string => { } else if (comment.type === CommentType.alert || comment.type === CommentType.generatedAlert) { const ids = getAlertIds(comment); return `Alert with ids ${ids.join(', ')} added to case`; + } else if ( + comment.type === CommentType.actions && + (comment.actions.type === 'isolate' || comment.actions.type === 'unisolate') + ) { + const firstHostname = + comment.actions.targets?.length > 0 ? comment.actions.targets[0].hostname : 'unknown'; + const totalHosts = comment.actions.targets.length; + const actionText = comment.actions.type === 'isolate' ? 'Isolated' : 'Released'; + const additionalHostsText = totalHosts - 1 > 0 ? `and ${totalHosts - 1} more ` : ``; + + return `${actionText} host ${firstHostname} ${additionalHostsText}with comment: ${comment.comment}`; } return ''; }; -const countAlerts = (comments: CaseResponse['comments']): number => - comments?.reduce((total, comment) => { - if (comment.type === CommentType.alert || comment.type === CommentType.generatedAlert) { - return total + (Array.isArray(comment.alertId) ? comment.alertId.length : 1); - } - return total; - }, 0) ?? 0; +interface CountAlertsInfo { + totalComments: number; + pushed: number; + totalAlerts: number; +} + +const getAlertsInfo = ( + comments: CaseResponse['comments'] +): { totalAlerts: number; hasUnpushedAlertComments: boolean } => { + const countingInfo = { totalComments: 0, pushed: 0, totalAlerts: 0 }; + + const res = + comments?.reduce(({ totalComments, pushed, totalAlerts }, comment) => { + if (comment.type === CommentType.alert || comment.type === CommentType.generatedAlert) { + return { + totalComments: totalComments + 1, + pushed: comment.pushed_at != null ? pushed + 1 : pushed, + totalAlerts: totalAlerts + (Array.isArray(comment.alertId) ? comment.alertId.length : 1), + }; + } + return { totalComments, pushed, totalAlerts }; + }, countingInfo) ?? countingInfo; + + return { + totalAlerts: res.totalAlerts, + hasUnpushedAlertComments: res.totalComments > res.pushed, + }; +}; + +const addAlertMessage = ( + caseId: string, + caseComments: CaseResponse['comments'], + comments: ExternalServiceComment[] +): ExternalServiceComment[] => { + const { totalAlerts, hasUnpushedAlertComments } = getAlertsInfo(caseComments); + + const newComments = [...comments]; + + if (hasUnpushedAlertComments) { + newComments.push({ + comment: `Elastic Alerts attached to the case: ${totalAlerts}`, + commentId: `${caseId}-total-alerts`, + }); + } + + return newComments; +}; export const createIncident = async ({ actionsClient, @@ -161,11 +213,10 @@ export const createIncident = async ({ const commentsToBeUpdated = caseComments?.filter( (comment) => // We push only user's comments - comment.type === CommentType.user && commentsIdsToBeUpdated.has(comment.id) + (comment.type === CommentType.user || comment.type === CommentType.actions) && + commentsIdsToBeUpdated.has(comment.id) ); - const totalAlerts = countAlerts(caseComments); - let comments: ExternalServiceComment[] = []; if (commentsToBeUpdated && Array.isArray(commentsToBeUpdated) && commentsToBeUpdated.length > 0) { @@ -175,12 +226,7 @@ export const createIncident = async ({ } } - if (totalAlerts > 0) { - comments.push({ - comment: `Elastic Alerts attached to the case: ${totalAlerts}`, - commentId: `${theCase.id}-total-alerts`, - }); - } + comments = addAlertMessage(theCase.id, caseComments, comments); return { incident, comments }; }; @@ -322,7 +368,7 @@ export const isCommentAlertType = ( export const getCommentContextFromAttributes = ( attributes: CommentAttributes -): CommentRequestUserType | CommentRequestAlertType => { +): CommentRequestUserType | CommentRequestAlertType | CommentRequestActionsType => { const owner = attributes.owner; switch (attributes.type) { case CommentType.user: @@ -340,6 +386,16 @@ export const getCommentContextFromAttributes = ( rule: attributes.rule, owner, }; + case CommentType.actions: + return { + type: attributes.type, + comment: attributes.comment, + actions: { + targets: attributes.actions.targets, + type: attributes.actions.type, + }, + owner, + }; default: return { type: CommentType.user, diff --git a/x-pack/plugins/console_extensions/README.md b/x-pack/plugins/console_extensions/README.md deleted file mode 100644 index 49d83d2888d6b..0000000000000 --- a/x-pack/plugins/console_extensions/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Console extensions - -This plugin provides autocomplete definitions of licensed APIs to the OSS Console plugin. \ No newline at end of file diff --git a/x-pack/plugins/console_extensions/kibana.json b/x-pack/plugins/console_extensions/kibana.json deleted file mode 100644 index 9411523d3f6dd..0000000000000 --- a/x-pack/plugins/console_extensions/kibana.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "consoleExtensions", - "version": "1.0.0", - "kibanaVersion": "kibana", - "requiredPlugins": ["console"], - "server": true, - "ui": false -} diff --git a/x-pack/plugins/console_extensions/server/index.ts b/x-pack/plugins/console_extensions/server/index.ts deleted file mode 100644 index a03111a487090..0000000000000 --- a/x-pack/plugins/console_extensions/server/index.ts +++ /dev/null @@ -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 { PluginInitializerContext, PluginConfigDescriptor } from 'kibana/server'; - -import { config as configSchema, ConfigType } from './config'; -import { ConsoleExtensionsServerPlugin } from './plugin'; - -export const plugin = (ctx: PluginInitializerContext) => new ConsoleExtensionsServerPlugin(ctx); - -export const config: PluginConfigDescriptor = { - schema: configSchema, -}; diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/js/ingest.ts b/x-pack/plugins/console_extensions/server/lib/spec_definitions/js/ingest.ts deleted file mode 100644 index 36ebfa589b823..0000000000000 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/js/ingest.ts +++ /dev/null @@ -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. - */ - -// NOTE: This is copy-pasted from es_6_0/ingest.js in OSS Console. -const commonPipelineParams = { - on_failure: [], - ignore_failure: { - __one_of: [false, true], - }, - if: '', - tag: '', -}; - -// Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/enrich-processor.html -const enrichProcessorDefinition = { - enrich: { - __template: { - policy_name: '', - field: '', - target_field: '', - }, - policy_name: '', - field: '', - target_field: '', - ignore_missing: { - __one_of: [false, true], - }, - override: { - __one_of: [true, false], - }, - max_matches: 1, - shape_relation: 'INTERSECTS', - ...commonPipelineParams, - }, -}; - -// Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/inference-processor.html -const inferenceProcessorDefinition = { - inference: { - __template: { - model_id: '', - inference_config: {}, - field_mappings: {}, - }, - target_field: '', - model_id: '', - field_mappings: { - __template: {}, - }, - inference_config: { - regression: { - __template: {}, - results_field: '', - }, - classification: { - __template: {}, - results_field: '', - num_top_classes: 2, - top_classes_results_field: '', - }, - }, - ...commonPipelineParams, - }, -}; - -export const processors = [enrichProcessorDefinition, inferenceProcessorDefinition]; diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_expired_data.json b/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_expired_data.json deleted file mode 100644 index 4afa9e323b030..0000000000000 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.delete_expired_data.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "ml.delete_expired_data": { - "methods": [ - "DELETE" - ], - "patterns": [ - "_ml/_delete_expired_data" - ] - } -} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.info.json b/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.info.json deleted file mode 100644 index 51b571776ead9..0000000000000 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.info.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "ml.info": { - "methods": [ - "GET" - ], - "patterns": [ - "_ml/info" - ] - } -} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.put_filter.json b/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.put_filter.json deleted file mode 100644 index 6d57c433d71f4..0000000000000 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.put_filter.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "ml.put_filter": { - "methods": [ - "PUT" - ], - "patterns": [ - "_ml/filters/{filter_id}" - ] - } -} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.put_trained_model.json b/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.put_trained_model.json deleted file mode 100644 index 27d0393be6086..0000000000000 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/ml.put_trained_model.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "ml.put_trained_model": { - "methods": [ - "PUT" - ], - "patterns": [ - "_ml/inference/{model_id}" - ], - "documentation": "TODO" - } -} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/rollup.delete_job.json b/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/rollup.delete_job.json deleted file mode 100644 index 8ecf617751a51..0000000000000 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/rollup.delete_job.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "rollup.delete_job": { - "methods": [ - "DELETE" - ], - "patterns": [ - "_rollup/job/{id}" - ] - } -} diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/rollup.put_job.json b/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/rollup.put_job.json deleted file mode 100644 index 7734fd54a1ab1..0000000000000 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/generated/rollup.put_job.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "rollup.put_job": { - "methods": [ - "PUT" - ], - "patterns": [ - "_rollup/job/{id}" - ] - } -} diff --git a/x-pack/plugins/console_extensions/server/plugin.ts b/x-pack/plugins/console_extensions/server/plugin.ts deleted file mode 100644 index 9ea3f314296ee..0000000000000 --- a/x-pack/plugins/console_extensions/server/plugin.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { join } from 'path'; -import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; - -import { ConsoleSetup, ConsoleStart } from '../../../../src/plugins/console/server'; - -import { processors } from './lib/spec_definitions/js'; - -interface SetupDependencies { - console: ConsoleSetup; -} - -interface StartDependencies { - console: ConsoleStart; -} - -const CONSOLE_XPACK_JSON_SPEC_PATH = join(__dirname, 'lib/spec_definitions/json'); - -export class ConsoleExtensionsServerPlugin implements Plugin { - log: Logger; - constructor(private readonly ctx: PluginInitializerContext) { - this.log = this.ctx.logger.get(); - } - - setup(core: CoreSetup, { console: { addExtensionSpecFilePath } }: SetupDependencies) { - addExtensionSpecFilePath(CONSOLE_XPACK_JSON_SPEC_PATH); - this.log.debug(`Added extension path to ${CONSOLE_XPACK_JSON_SPEC_PATH}...`); - } - - start(core: CoreStart, { console: { addProcessorDefinition } }: StartDependencies) { - processors.forEach((processor) => addProcessorDefinition(processor)); - this.log.debug('Added processor definition extensions.'); - } -} diff --git a/x-pack/plugins/console_extensions/tsconfig.json b/x-pack/plugins/console_extensions/tsconfig.json deleted file mode 100644 index 5ad28f230a0bb..0000000000000 --- a/x-pack/plugins/console_extensions/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "composite": true, - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": [ - "server/**/*" - ], - "references": [ - { "path": "../../../src/core/tsconfig.json" }, - { "path": "../../../src/plugins/console/tsconfig.json" } - ] -} diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_non_persiseted_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/check_non_persiseted_sessions.ts index 8c75ce91cac6a..2115ce85eeb27 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/check_non_persiseted_sessions.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/check_non_persiseted_sessions.ts @@ -59,7 +59,11 @@ function checkNonPersistedSessionsPage( `${SEARCH_SESSIONS_CLEANUP_TASK_TYPE} Found ${nonPersistedSearchSessions.total} sessions, processing ${nonPersistedSearchSessions.saved_objects.length}` ); - const updatedSessions = await getAllSessionsStatusUpdates(deps, nonPersistedSearchSessions); + const updatedSessions = await getAllSessionsStatusUpdates( + deps, + config, + nonPersistedSearchSessions + ); const deletedSessionIds: string[] = []; await Promise.all( diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts index 0d51e97952275..3e89383c16d5e 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts @@ -36,7 +36,11 @@ function checkPersistedSessionsPage( `${SEARCH_SESSIONS_TASK_TYPE} Found ${persistedSearchSessions.total} sessions, processing ${persistedSearchSessions.saved_objects.length}` ); - const updatedSessions = await getAllSessionsStatusUpdates(deps, persistedSearchSessions); + const updatedSessions = await getAllSessionsStatusUpdates( + deps, + config, + persistedSearchSessions + ); await bulkUpdateSessions(deps, updatedSessions); return persistedSearchSessions; diff --git a/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts index e261c324f440f..61d1635dabe1b 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts @@ -36,7 +36,7 @@ function checkSessionExpirationPage( `${SEARCH_SESSIONS_EXPIRE_TASK_TYPE} Found ${searchSessions.total} sessions, processing ${searchSessions.saved_objects.length}` ); - const updatedSessions = await getAllSessionsStatusUpdates(deps, searchSessions); + const updatedSessions = await getAllSessionsStatusUpdates(deps, config, searchSessions); await bulkUpdateSessions(deps, updatedSessions); return searchSessions; diff --git a/x-pack/plugins/data_enhanced/server/search/session/get_session_status.test.ts b/x-pack/plugins/data_enhanced/server/search/session/get_session_status.test.ts index fc86e75297393..c3946e5af16fa 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/get_session_status.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/get_session_status.test.ts @@ -5,16 +5,21 @@ * 2.0. */ -import { SearchStatus } from './types'; +import { SearchSessionsConfig, SearchStatus } from './types'; import { getSessionStatus } from './get_session_status'; import { SearchSessionStatus } from '../../../../../../src/plugins/data/common'; +import moment from 'moment'; describe('getSessionStatus', () => { + const mockConfig = ({ + notTouchedInProgressTimeout: moment.duration(1, 'm'), + } as unknown) as SearchSessionsConfig; test("returns an in_progress status if there's nothing inside the session", () => { const session: any = { idMapping: {}, + touched: moment(), }; - expect(getSessionStatus(session)).toBe(SearchSessionStatus.IN_PROGRESS); + expect(getSessionStatus(session, mockConfig)).toBe(SearchSessionStatus.IN_PROGRESS); }); test("returns an error status if there's at least one error", () => { @@ -25,7 +30,25 @@ describe('getSessionStatus', () => { c: { status: SearchStatus.COMPLETE }, }, }; - expect(getSessionStatus(session)).toBe(SearchSessionStatus.ERROR); + expect(getSessionStatus(session, mockConfig)).toBe(SearchSessionStatus.ERROR); + }); + + test('expires a empty session after a minute', () => { + const session: any = { + idMapping: {}, + touched: moment().subtract(2, 'm'), + }; + expect(getSessionStatus(session, mockConfig)).toBe(SearchSessionStatus.EXPIRED); + }); + + test('doesnt expire a full session after a minute', () => { + const session: any = { + idMapping: { + a: { status: SearchStatus.IN_PROGRESS }, + }, + touched: moment().subtract(2, 'm'), + }; + expect(getSessionStatus(session, mockConfig)).toBe(SearchSessionStatus.IN_PROGRESS); }); test('returns a complete status if all are complete', () => { @@ -36,7 +59,7 @@ describe('getSessionStatus', () => { c: { status: SearchStatus.COMPLETE }, }, }; - expect(getSessionStatus(session)).toBe(SearchSessionStatus.COMPLETE); + expect(getSessionStatus(session, mockConfig)).toBe(SearchSessionStatus.COMPLETE); }); test('returns a running status if some are still running', () => { @@ -47,6 +70,6 @@ describe('getSessionStatus', () => { c: { status: SearchStatus.IN_PROGRESS }, }, }; - expect(getSessionStatus(session)).toBe(SearchSessionStatus.IN_PROGRESS); + expect(getSessionStatus(session, mockConfig)).toBe(SearchSessionStatus.IN_PROGRESS); }); }); diff --git a/x-pack/plugins/data_enhanced/server/search/session/get_session_status.ts b/x-pack/plugins/data_enhanced/server/search/session/get_session_status.ts index 23e02eedc0004..e7ae52b6c88ae 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/get_session_status.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/get_session_status.ts @@ -5,16 +5,28 @@ * 2.0. */ +import moment from 'moment'; import { SearchSessionSavedObjectAttributes, SearchSessionStatus, } from '../../../../../../src/plugins/data/common/'; -import { SearchStatus } from './types'; +import { SearchSessionsConfig, SearchStatus } from './types'; -export function getSessionStatus(session: SearchSessionSavedObjectAttributes): SearchSessionStatus { +export function getSessionStatus( + session: SearchSessionSavedObjectAttributes, + config: SearchSessionsConfig +): SearchSessionStatus { const searchStatuses = Object.values(session.idMapping); + const curTime = moment(); if (searchStatuses.some((item) => item.status === SearchStatus.ERROR)) { return SearchSessionStatus.ERROR; + } else if ( + searchStatuses.length === 0 && + curTime.diff(moment(session.touched), 'ms') > + moment.duration(config.notTouchedInProgressTimeout).asMilliseconds() + ) { + // Expire empty sessions that weren't touched for a minute + return SearchSessionStatus.EXPIRED; } else if ( searchStatuses.length > 0 && searchStatuses.every((item) => item.status === SearchStatus.COMPLETE) diff --git a/x-pack/plugins/data_enhanced/server/search/session/update_session_status.test.ts b/x-pack/plugins/data_enhanced/server/search/session/update_session_status.test.ts index 485a30fd54951..d9e3fa6f8cab3 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/update_session_status.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/update_session_status.test.ts @@ -21,6 +21,7 @@ import { describe('bulkUpdateSessions', () => { let mockClient: any; + const mockConfig: any = {}; let savedObjectsClient: jest.Mocked; const mockLogger: any = { debug: jest.fn(), @@ -66,6 +67,7 @@ describe('bulkUpdateSessions', () => { client: mockClient, logger: mockLogger, }, + mockConfig, so ); @@ -105,6 +107,7 @@ describe('bulkUpdateSessions', () => { client: mockClient, logger: mockLogger, }, + mockConfig, so ); @@ -139,6 +142,7 @@ describe('bulkUpdateSessions', () => { client: mockClient, logger: mockLogger, }, + mockConfig, so ); @@ -176,6 +180,7 @@ describe('bulkUpdateSessions', () => { client: mockClient, logger: mockLogger, }, + mockConfig, so ); @@ -219,6 +224,7 @@ describe('bulkUpdateSessions', () => { client: mockClient, logger: mockLogger, }, + mockConfig, so ); diff --git a/x-pack/plugins/data_enhanced/server/search/session/update_session_status.ts b/x-pack/plugins/data_enhanced/server/search/session/update_session_status.ts index 1c484467bef63..4758e7cb22684 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/update_session_status.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/update_session_status.ts @@ -13,11 +13,17 @@ import { } from '../../../../../../src/plugins/data/common'; import { getSearchStatus } from './get_search_status'; import { getSessionStatus } from './get_session_status'; -import { CheckSearchSessionsDeps, SearchSessionsResponse, SearchStatus } from './types'; +import { + CheckSearchSessionsDeps, + SearchSessionsConfig, + SearchSessionsResponse, + SearchStatus, +} from './types'; import { isSearchSessionExpired } from './utils'; export async function updateSessionStatus( { logger, client }: CheckSearchSessionsDeps, + config: SearchSessionsConfig, session: SavedObjectsFindResult ) { let sessionUpdated = false; @@ -61,7 +67,7 @@ export async function updateSessionStatus( // And only then derive the session's status const sessionStatus = isExpired ? SearchSessionStatus.EXPIRED - : getSessionStatus(session.attributes); + : getSessionStatus(session.attributes, config); if (sessionStatus !== session.attributes.status) { const now = new Date().toISOString(); session.attributes.status = sessionStatus; @@ -79,13 +85,14 @@ export async function updateSessionStatus( export async function getAllSessionsStatusUpdates( deps: CheckSearchSessionsDeps, + config: SearchSessionsConfig, searchSessions: SearchSessionsResponse ) { const updatedSessions = new Array>(); await Promise.all( searchSessions.saved_objects.map(async (session) => { - const updated = await updateSessionStatus(deps, session); + const updated = await updateSessionStatus(deps, config, session); if (updated) { updatedSessions.push(session); diff --git a/x-pack/plugins/data_visualizer/common/constants.ts b/x-pack/plugins/data_visualizer/common/constants.ts index 7e0fe65632ae3..55ebdf9a196d6 100644 --- a/x-pack/plugins/data_visualizer/common/constants.ts +++ b/x-pack/plugins/data_visualizer/common/constants.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { KBN_FIELD_TYPES } from '../../../../src/plugins/data/common'; + export const UI_SETTING_MAX_FILE_SIZE = 'fileUpload:maxFileSize'; export const MB = Math.pow(2, 20); @@ -27,7 +29,26 @@ export const JOB_FIELD_TYPES = { KEYWORD: 'keyword', NUMBER: 'number', TEXT: 'text', + HISTOGRAM: 'histogram', UNKNOWN: 'unknown', } as const; +export const JOB_FIELD_TYPES_OPTIONS = { + [JOB_FIELD_TYPES.BOOLEAN]: { name: 'Boolean', icon: 'tokenBoolean' }, + [JOB_FIELD_TYPES.DATE]: { name: 'Date', icon: 'tokenDate' }, + [JOB_FIELD_TYPES.GEO_POINT]: { name: 'Geo point', icon: 'tokenGeo' }, + [JOB_FIELD_TYPES.GEO_SHAPE]: { name: 'Geo shape', icon: 'tokenGeo' }, + [JOB_FIELD_TYPES.IP]: { name: 'IP address', icon: 'tokenIP' }, + [JOB_FIELD_TYPES.KEYWORD]: { name: 'Keyword', icon: 'tokenKeyword' }, + [JOB_FIELD_TYPES.NUMBER]: { name: 'Number', icon: 'tokenNumber' }, + [JOB_FIELD_TYPES.TEXT]: { name: 'Text', icon: 'tokenString' }, + [JOB_FIELD_TYPES.HISTOGRAM]: { name: 'Histogram', icon: 'tokenNumber' }, + [JOB_FIELD_TYPES.UNKNOWN]: { name: 'Unknown' }, +}; + export const OMIT_FIELDS: string[] = ['_source', '_type', '_index', '_id', '_version', '_score']; + +export const NON_AGGREGATABLE_FIELD_TYPES = new Set([ + KBN_FIELD_TYPES.GEO_SHAPE, + KBN_FIELD_TYPES.HISTOGRAM, +]); diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx index 50823006db3b6..ee4b4f8171d7d 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx @@ -72,6 +72,9 @@ export const FieldTypeIcon: FC = ({ iconType = 'tokenNumber'; color = fieldName !== undefined ? 'euiColorVis1' : 'euiColorVis2'; break; + case JOB_FIELD_TYPES.HISTOGRAM: + iconType = 'tokenHistogram'; + color = 'euiColorVis7'; case JOB_FIELD_TYPES.UNKNOWN: // Use defaults break; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx index 152926ad84ba7..511a068f305f9 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_types_filter/field_types_filter.tsx @@ -14,19 +14,7 @@ import type { FileBasedUnknownFieldVisConfig, } from '../stats_table/types/field_vis_config'; import { FieldTypeIcon } from '../field_type_icon'; -import { JOB_FIELD_TYPES } from '../../../../../common'; - -const JOB_FIELD_TYPES_OPTIONS = { - [JOB_FIELD_TYPES.BOOLEAN]: { name: 'Boolean', icon: 'tokenBoolean' }, - [JOB_FIELD_TYPES.DATE]: { name: 'Date', icon: 'tokenDate' }, - [JOB_FIELD_TYPES.GEO_POINT]: { name: 'Geo point', icon: 'tokenGeo' }, - [JOB_FIELD_TYPES.GEO_SHAPE]: { name: 'Geo shape', icon: 'tokenGeo' }, - [JOB_FIELD_TYPES.IP]: { name: 'IP address', icon: 'tokenIP' }, - [JOB_FIELD_TYPES.KEYWORD]: { name: 'Keyword', icon: 'tokenKeyword' }, - [JOB_FIELD_TYPES.NUMBER]: { name: 'Number', icon: 'tokenNumber' }, - [JOB_FIELD_TYPES.TEXT]: { name: 'Text', icon: 'tokenString' }, - [JOB_FIELD_TYPES.UNKNOWN]: { name: 'Unknown' }, -}; +import { JOB_FIELD_TYPES_OPTIONS } from '../../../../../common'; interface Props { fields: Array; diff --git a/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts b/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts index e0a6c8ebf85c9..98d43059e5ee3 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/util/field_types_utils.ts @@ -78,6 +78,9 @@ export function kbnTypeToJobType(field: IndexPatternField) { case KBN_FIELD_TYPES.GEO_SHAPE: type = JOB_FIELD_TYPES.GEO_SHAPE; break; + case KBN_FIELD_TYPES.HISTOGRAM: + type = JOB_FIELD_TYPES.HISTOGRAM; + break; default: break; diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_error_callouts.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_error_callouts.tsx index 62d860c1513e8..12e92f2936fff 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_error_callouts.tsx +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_error_callouts.tsx @@ -107,7 +107,7 @@ export const FileCouldNotBeRead: FC = ({ defaultMessage="If you know something about this data, such as the file format or timestamp format, adding initial overrides may help us to infer the rest of the structure." />
- + = (dataVi } }, [currentIndexPattern, toasts]); - // Obtain the list of non metric field types which appear in the index pattern. - let indexedFieldTypes: JobFieldType[] = []; const indexPatternFields: IndexPatternField[] = currentIndexPattern.fields; - indexPatternFields.forEach((field) => { - if (field.scripted !== true) { - const dataVisualizerType: JobFieldType | undefined = kbnTypeToJobType(field); - if (dataVisualizerType !== undefined && !indexedFieldTypes.includes(dataVisualizerType)) { - indexedFieldTypes.push(dataVisualizerType); + + const fieldTypes = useMemo(() => { + // Obtain the list of non metric field types which appear in the index pattern. + const indexedFieldTypes: JobFieldType[] = []; + indexPatternFields.forEach((field) => { + if (!OMIT_FIELDS.includes(field.name) && field.scripted !== true) { + const dataVisualizerType: JobFieldType | undefined = kbnTypeToJobType(field); + if (dataVisualizerType !== undefined && !indexedFieldTypes.includes(dataVisualizerType)) { + indexedFieldTypes.push(dataVisualizerType); + } } - } - }); - indexedFieldTypes = indexedFieldTypes.sort(); + }); + return indexedFieldTypes.sort(); + }, [indexPatternFields]); const defaults = getDefaultPageState(); @@ -368,9 +372,16 @@ export const IndexDataVisualizerView: FC = (dataVi earliest, latest ); + // Because load overall stats perform queries in batches + // there could be multiple errors + if (Array.isArray(allStats.errors) && allStats.errors.length > 0) { + allStats.errors.forEach((err: any) => { + dataLoader.displayError(extractErrorProperties(err)); + }); + } setOverallStats(allStats); } catch (err) { - dataLoader.displayError(err); + dataLoader.displayError(err.body ?? err); } } @@ -859,7 +870,7 @@ export const IndexDataVisualizerView: FC = (dataVi samplerShardSize={samplerShardSize} setSamplerShardSize={setSamplerShardSize} overallStats={overallStats} - indexedFieldTypes={indexedFieldTypes} + indexedFieldTypes={fieldTypes} setVisibleFieldTypes={setVisibleFieldTypes} visibleFieldTypes={visibleFieldTypes} visibleFieldNames={visibleFieldNames} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx index 4f9de09dc670e..a4286bc4e09d1 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/field_type_filter.tsx @@ -8,22 +8,10 @@ import React, { FC, useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { JOB_FIELD_TYPES, JobFieldType } from '../../../../../common'; +import { JOB_FIELD_TYPES_OPTIONS, JobFieldType } from '../../../../../common'; import { FieldTypeIcon } from '../../../common/components/field_type_icon'; import { MultiSelectPicker, Option } from '../../../common/components/multi_select_picker'; -const ML_JOB_FIELD_TYPES_OPTIONS = { - [JOB_FIELD_TYPES.BOOLEAN]: { name: 'Boolean', icon: 'tokenBoolean' }, - [JOB_FIELD_TYPES.DATE]: { name: 'Date', icon: 'tokenDate' }, - [JOB_FIELD_TYPES.GEO_POINT]: { name: 'Geo point', icon: 'tokenGeo' }, - [JOB_FIELD_TYPES.GEO_SHAPE]: { name: 'Geo shape', icon: 'tokenGeo' }, - [JOB_FIELD_TYPES.IP]: { name: 'IP address', icon: 'tokenIP' }, - [JOB_FIELD_TYPES.KEYWORD]: { name: 'Keyword', icon: 'tokenKeyword' }, - [JOB_FIELD_TYPES.NUMBER]: { name: 'Number', icon: 'tokenNumber' }, - [JOB_FIELD_TYPES.TEXT]: { name: 'Text', icon: 'tokenString' }, - [JOB_FIELD_TYPES.UNKNOWN]: { name: 'Unknown' }, -}; - export const DatavisualizerFieldTypeFilter: FC<{ indexedFieldTypes: JobFieldType[]; setVisibleFieldTypes(q: string[]): void; @@ -31,7 +19,7 @@ export const DatavisualizerFieldTypeFilter: FC<{ }> = ({ indexedFieldTypes, setVisibleFieldTypes, visibleFieldTypes }) => { const options: Option[] = useMemo(() => { return indexedFieldTypes.map((indexedFieldName) => { - const item = ML_JOB_FIELD_TYPES_OPTIONS[indexedFieldName]; + const item = JOB_FIELD_TYPES_OPTIONS[indexedFieldName]; return { value: indexedFieldName, diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts index 468bd3a2bd7ee..1b92eaddd1343 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts @@ -10,8 +10,7 @@ import { CoreSetup } from 'kibana/public'; import { estypes } from '@elastic/elasticsearch'; import { i18n } from '@kbn/i18n'; import { IndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; -import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/common'; -import { OMIT_FIELDS } from '../../../../common/constants'; +import { NON_AGGREGATABLE_FIELD_TYPES, OMIT_FIELDS } from '../../../../common/constants'; import { FieldRequestConfig } from '../../../../common/types'; import { getVisualizerFieldStats, getVisualizerOverallStats } from '../services/visualizer_stats'; @@ -49,7 +48,7 @@ export class DataLoader { this._indexPattern.fields.forEach((field) => { const fieldName = field.displayName !== undefined ? field.displayName : field.name; if (this.isDisplayField(fieldName) === true) { - if (field.aggregatable === true && field.type !== KBN_FIELD_TYPES.GEO_SHAPE) { + if (field.aggregatable === true && !NON_AGGREGATABLE_FIELD_TYPES.has(field.type)) { aggregatableFields.push(field.name); } else { nonAggregatableFields.push(field.name); @@ -110,17 +109,17 @@ export class DataLoader { 'The request may have timed out. Try using a smaller sample size or narrowing the time range.', values: { index: this._indexPattern.title, - message: err.message, + message: err.error ?? err.message, }, }), }); } else { this._toastNotifications.addError(err, { - title: i18n.translate('xpack.dataVisualizer.index.errorLoadingDataMessage.', { - defaultMessage: 'Error loading data in index {index}. {message}', + title: i18n.translate('xpack.dataVisualizer.index.errorLoadingDataMessage', { + defaultMessage: 'Error loading data in index {index}. {message}.', values: { index: this._indexPattern.title, - message: err.message, + message: err.error ?? err.message, }, }), }); diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/error_utils.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/error_utils.ts new file mode 100644 index 0000000000000..9bb36496a149e --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/error_utils.ts @@ -0,0 +1,184 @@ +/* + * 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 { HttpFetchError } from 'kibana/public'; +import Boom from '@hapi/boom'; +import { isPopulatedObject } from '../../../../common/utils/object_utils'; + +export interface WrappedError { + body: { + attributes: { + body: EsErrorBody; + }; + message: Boom.Boom; + }; + statusCode: number; +} + +export interface EsErrorRootCause { + type: string; + reason: string; + caused_by?: EsErrorRootCause; + script?: string; +} + +export interface EsErrorBody { + error: { + root_cause?: EsErrorRootCause[]; + caused_by?: EsErrorRootCause; + type: string; + reason: string; + }; + status: number; +} + +export interface DVResponseError { + statusCode: number; + error: string; + message: string; + attributes?: { + body: EsErrorBody; + }; +} + +export interface ErrorMessage { + message: string; +} + +export interface DVErrorObject { + causedBy?: string; + message: string; + statusCode?: number; + fullError?: EsErrorBody; +} + +export interface DVHttpFetchError extends HttpFetchError { + body: T; +} + +export type ErrorType = + | WrappedError + | DVHttpFetchError + | EsErrorBody + | Boom.Boom + | string + | undefined; + +export function isEsErrorBody(error: any): error is EsErrorBody { + return error && error.error?.reason !== undefined; +} + +export function isErrorString(error: any): error is string { + return typeof error === 'string'; +} + +export function isErrorMessage(error: any): error is ErrorMessage { + return error && error.message !== undefined && typeof error.message === 'string'; +} + +export function isDVResponseError(error: any): error is DVResponseError { + return typeof error.body === 'object' && 'message' in error.body; +} + +export function isBoomError(error: any): error is Boom.Boom { + return error.isBoom === true; +} + +export function isWrappedError(error: any): error is WrappedError { + return error && isBoomError(error.body?.message) === true; +} + +export const extractErrorProperties = (error: ErrorType): DVErrorObject => { + // extract properties of the error object from within the response error + // coming from Kibana, Elasticsearch, and our own DV messages + + // some responses contain raw es errors as part of a bulk response + // e.g. if some jobs fail the action in a bulk request + + if (isEsErrorBody(error)) { + return { + message: error.error.reason, + statusCode: error.status, + fullError: error, + }; + } + + if (isErrorString(error)) { + return { + message: error, + }; + } + if (isWrappedError(error)) { + return error.body.message?.output?.payload; + } + + if (isBoomError(error)) { + return { + message: error.output.payload.message, + statusCode: error.output.payload.statusCode, + }; + } + + if (error?.body === undefined && !error?.message) { + return { + message: '', + }; + } + + if (typeof error.body === 'string') { + return { + message: error.body, + }; + } + + if (isDVResponseError(error)) { + if ( + typeof error.body.attributes === 'object' && + typeof error.body.attributes.body?.error?.reason === 'string' + ) { + const errObj: DVErrorObject = { + message: error.body.attributes.body.error.reason, + statusCode: error.body.statusCode, + fullError: error.body.attributes.body, + }; + if ( + typeof error.body.attributes.body.error.caused_by === 'object' && + (typeof error.body.attributes.body.error.caused_by?.reason === 'string' || + typeof error.body.attributes.body.error.caused_by?.caused_by?.reason === 'string') + ) { + errObj.causedBy = + error.body.attributes.body.error.caused_by?.caused_by?.reason || + error.body.attributes.body.error.caused_by?.reason; + } + if ( + Array.isArray(error.body.attributes.body.error.root_cause) && + typeof error.body.attributes.body.error.root_cause[0] === 'object' && + isPopulatedObject(error.body.attributes.body.error.root_cause[0], ['script']) + ) { + errObj.causedBy = error.body.attributes.body.error.root_cause[0].script; + errObj.message += `: '${error.body.attributes.body.error.root_cause[0].script}'`; + } + return errObj; + } else { + return { + message: error.body.message, + statusCode: error.body.statusCode, + }; + } + } + + if (isErrorMessage(error)) { + return { + message: error.message, + }; + } + + // If all else fail return an empty message instead of JSON.stringify + return { + message: '', + }; +}; diff --git a/x-pack/plugins/data_visualizer/server/models/data_visualizer/data_visualizer.ts b/x-pack/plugins/data_visualizer/server/models/data_visualizer/data_visualizer.ts index 27c09c889deb7..155cf09ebb8db 100644 --- a/x-pack/plugins/data_visualizer/server/models/data_visualizer/data_visualizer.ts +++ b/x-pack/plugins/data_visualizer/server/models/data_visualizer/data_visualizer.ts @@ -31,6 +31,7 @@ import { getNumericFieldsStats, getStringFieldsStats, } from './get_fields_stats'; +import { wrapError } from '../../utils/error_wrapper'; export class DataVisualizer { private _client: IScopedClusterClient; @@ -60,6 +61,7 @@ export class DataVisualizer { aggregatableNotExistsFields: [] as FieldData[], nonAggregatableExistsFields: [] as FieldData[], nonAggregatableNotExistsFields: [] as FieldData[], + errors: [] as any[], }; // To avoid checking for the existence of too many aggregatable fields in one request, @@ -76,49 +78,61 @@ export class DataVisualizer { await Promise.all( batches.map(async (fields) => { - const batchStats = await this.checkAggregatableFieldsExist( - indexPatternTitle, - query, - fields, - samplerShardSize, - timeFieldName, - earliestMs, - latestMs, - undefined, - runtimeMappings - ); + try { + const batchStats = await this.checkAggregatableFieldsExist( + indexPatternTitle, + query, + fields, + samplerShardSize, + timeFieldName, + earliestMs, + latestMs, + undefined, + runtimeMappings + ); - // Total count will be returned with each batch of fields. Just overwrite. - stats.totalCount = batchStats.totalCount; + // Total count will be returned with each batch of fields. Just overwrite. + stats.totalCount = batchStats.totalCount; - // Add to the lists of fields which do and do not exist. - stats.aggregatableExistsFields.push(...batchStats.aggregatableExistsFields); - stats.aggregatableNotExistsFields.push(...batchStats.aggregatableNotExistsFields); + // Add to the lists of fields which do and do not exist. + stats.aggregatableExistsFields.push(...batchStats.aggregatableExistsFields); + stats.aggregatableNotExistsFields.push(...batchStats.aggregatableNotExistsFields); + } catch (e) { + // If index not found, no need to proceed with other batches + if (e.statusCode === 404) { + throw e; + } + stats.errors.push(wrapError(e)); + } }) ); await Promise.all( nonAggregatableFields.map(async (field) => { - const existsInDocs = await this.checkNonAggregatableFieldExists( - indexPatternTitle, - query, - field, - timeFieldName, - earliestMs, - latestMs, - runtimeMappings - ); + try { + const existsInDocs = await this.checkNonAggregatableFieldExists( + indexPatternTitle, + query, + field, + timeFieldName, + earliestMs, + latestMs, + runtimeMappings + ); - const fieldData: FieldData = { - fieldName: field, - existsInDocs, - stats: {}, - }; + const fieldData: FieldData = { + fieldName: field, + existsInDocs, + stats: {}, + }; - if (existsInDocs === true) { - stats.nonAggregatableExistsFields.push(fieldData); - } else { - stats.nonAggregatableNotExistsFields.push(fieldData); + if (existsInDocs === true) { + stats.nonAggregatableExistsFields.push(fieldData); + } else { + stats.nonAggregatableNotExistsFields.push(fieldData); + } + } catch (e) { + stats.errors.push(wrapError(e)); } }) ); diff --git a/x-pack/plugins/data_visualizer/server/plugin.ts b/x-pack/plugins/data_visualizer/server/plugin.ts index 4ae695b05b81f..9db580959b116 100644 --- a/x-pack/plugins/data_visualizer/server/plugin.ts +++ b/x-pack/plugins/data_visualizer/server/plugin.ts @@ -12,7 +12,7 @@ import { dataVisualizerRoutes } from './routes'; export class DataVisualizerPlugin implements Plugin { constructor() {} - async setup(coreSetup: CoreSetup, plugins: SetupDeps) { + setup(coreSetup: CoreSetup, plugins: SetupDeps) { dataVisualizerRoutes(coreSetup); } diff --git a/x-pack/plugins/enterprise_search/README.md b/x-pack/plugins/enterprise_search/README.md index 0b067e25e32e8..96b0391bbc8da 100644 --- a/x-pack/plugins/enterprise_search/README.md +++ b/x-pack/plugins/enterprise_search/README.md @@ -2,16 +2,30 @@ ## Overview -This plugin's goal is to provide a Kibana user interface to the Enterprise Search solution's products (App Search and Workplace Search). In it's current MVP state, the plugin provides the following with the goal of gathering user feedback and raising product awareness: +This plugin provides beta Kibana user interfaces for managing the Enterprise Search solution and its products, App Search and Workplace Search. -- **App Search:** A basic engines overview with links into the product. -- **Workplace Search:** A simple app overview with basic statistics, links to the sources, users (if standard auth), and product settings. +> :warning: The Kibana interface for Enterprise Search is a beta feature. It is subject to change and is not covered by the same level of support as generally available features. This interface will become the sole management panel for Enterprise Search with the 8.0 release. Until then, the standalone Enterprise Search UI remains available and supported. + +### App Search + + + +Add rich, relevant search to your apps and websites. https://www.elastic.co/app-search/ + +### Workplace Search + + + +Unify all your team's content into a personalized search experience. https://www.elastic.co/workplace-search/ ## Development 1. When developing locally, Enterprise Search should be running locally alongside Kibana on `localhost:3002`. 2. Update `config/kibana.dev.yml` with `enterpriseSearch.host: 'http://localhost:3002'` -3. For faster QA/development, run Enterprise Search on [elasticsearch-native auth](https://www.elastic.co/guide/en/app-search/current/security-and-users.html#app-search-self-managed-security-and-user-management-elasticsearch-native-realm) and log in as the `elastic` superuser on Kibana. + +Problems? If you're an Elastic Enterprise Search engineer, please reach out to @elastic/enterprise-search-frontend for questions or our in-depth Getting Started developer guide. + +Don't forget to read Kibana's [contributing documentation](https://github.com/elastic/kibana/#building-and-running-kibana-andor-contributing-code) and developer guides for more general info on the Kibana ecosystem. ### Kea diff --git a/x-pack/plugins/enterprise_search/kibana.json b/x-pack/plugins/enterprise_search/kibana.json index f8b4261114a22..723b24f951434 100644 --- a/x-pack/plugins/enterprise_search/kibana.json +++ b/x-pack/plugins/enterprise_search/kibana.json @@ -12,5 +12,5 @@ "name": "Enterprise Search", "githubTeam": "enterprise-search-frontend" }, - "description": "Adds dashboards for discovering and managing Enterprise Search products" + "description": "Adds dashboards for discovering and managing Enterprise Search products." } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_filters.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_filters.tsx index 0c8455e986ae1..dd99d368a0105 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_filters.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_filters.tsx @@ -80,6 +80,7 @@ export const AnalyticsFilters: React.FC = () => { 'xpack.enterpriseSearch.appSearch.engine.analytics.filters.startDateAriaLabel', { defaultMessage: 'Filter by start date' } )} + locale={i18n.getLocale()} /> } endDateControl={ @@ -93,6 +94,7 @@ export const AnalyticsFilters: React.FC = () => { 'xpack.enterpriseSearch.appSearch.engine.analytics.filters.endDateAriaLabel', { defaultMessage: 'Filter by end date' } )} + locale={i18n.getLocale()} /> } fullWidth diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/multi_input_rows/multi_input_rows.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/multi_input_rows/multi_input_rows.test.tsx index 3b8e1c96ff504..63952bc2a6de7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/multi_input_rows/multi_input_rows.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/multi_input_rows/multi_input_rows.test.tsx @@ -10,7 +10,7 @@ import '../../../__mocks__/shallow_useeffect.mock'; import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; import { rerender } from '../../../test_helpers'; @@ -162,10 +162,18 @@ describe('MultiInputRows', () => { }); describe('onChange', () => { + let wrapper: ShallowWrapper; const onChange = jest.fn(); + beforeEach(() => { + wrapper = shallow(); + }); + + it('does not call on change on mount', () => { + expect(onChange).not.toHaveBeenCalled(); + }); + it('returns the current values dynamically on change', () => { - const wrapper = shallow(); setMockValues({ ...values, values: ['updated'] }); rerender(wrapper); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/multi_input_rows/multi_input_rows.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/multi_input_rows/multi_input_rows.tsx index ac61e69eb44c4..257f4b637f3e0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/multi_input_rows/multi_input_rows.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/multi_input_rows/multi_input_rows.tsx @@ -5,9 +5,10 @@ * 2.0. */ -import React, { useEffect } from 'react'; +import React from 'react'; import { useValues, useActions } from 'kea'; +import useUpdateEffect from 'react-use/lib/useUpdateEffect'; import { EuiForm, EuiButton, EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; @@ -49,7 +50,7 @@ export const MultiInputRows: React.FC = ({ const { values, addedNewRow, hasEmptyValues, hasOnlyOneValue } = useValues(logic); const { addValue, editValue, deleteValue } = useActions(logic); - useEffect(() => { + useUpdateEffect(() => { if (onChange) { onChange(filterEmptyValues(values)); } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts index 4233a7b300d15..e2493b6404f7d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts @@ -229,7 +229,7 @@ describe('RelevanceTuningLogic', () => { }); describe('updatePrecision', () => { - it('should set precision inside search settings', () => { + it('should set precision inside search settings and set unsavedChanges to true', () => { mount(); RelevanceTuningLogic.actions.updatePrecision(9); @@ -239,6 +239,7 @@ describe('RelevanceTuningLogic', () => { ...DEFAULT_VALUES.searchSettings, precision: 9, }, + unsavedChanges: true, }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts index 743bb1aa1502b..02903b4588ed4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts @@ -191,6 +191,7 @@ export const RelevanceTuningLogic = kea< unsavedChanges: [ false, { + updatePrecision: () => true, setSearchSettings: () => true, setSearchSettingsResponse: () => false, }, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/field_number.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/field_number.test.tsx index 3ac50d906e9c4..c012167f67818 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/field_number.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/field_number.test.tsx @@ -54,7 +54,7 @@ describe('FieldNumber', () => { }} /> ); - expect(wrapper.find(EuiFieldNumber).prop('value')).toEqual(''); + expect(wrapper.find(EuiFieldNumber).prop('value')).toEqual(' '); }); it('is disabled if the [fieldEnabledProperty] in fieldSettings is false', () => { @@ -90,10 +90,10 @@ describe('FieldNumber', () => { expect(props.updateAction).toHaveBeenCalledWith('foo', 21); }); - it('will call updateAction on blur using the minimum possible value if the current value is something other than a number', () => { + it('will call clearAction on blur if the current value is something other than a number', () => { const wrapper = shallow(); wrapper.simulate('blur', { target: { value: '' } }); - expect(props.updateAction).toHaveBeenCalledWith('foo', 20); + expect(props.clearAction).toHaveBeenCalledWith('foo'); }); it('will call updateAction on blur using the minimum possible value if the value is something lower than the minimum', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/field_number.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/field_number.tsx index cd7bab3c6f594..f16bab5234ab1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/field_number.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/field_number.tsx @@ -43,11 +43,10 @@ const handleFieldNumberBlur = ( clearAction: (fieldName: string) => void ) => { return (e: FocusEvent) => { - const value = parseInt(e.target.value, 10); - const fieldValue = Math.min( - SIZE_FIELD_MAXIMUM, - Math.max(SIZE_FIELD_MINIMUM, isNaN(value) ? 0 : value) - ); + let fieldValue = parseInt(e.target.value, 10); + if (!isNaN(fieldValue)) { + fieldValue = Math.min(SIZE_FIELD_MAXIMUM, Math.max(SIZE_FIELD_MINIMUM, fieldValue)); + } updateOrClearSizeForField(fieldName, fieldValue, updateAction, clearAction); }; }; @@ -74,7 +73,7 @@ export const FieldNumber: React.FC = ({ value={ typeof fieldSettings[fieldSizeProperty] === 'number' ? (fieldSettings[fieldSizeProperty] as number) - : '' + : ' ' // Without the space, invalid non-numbers don't get cleared for some reason } placeholder={i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.resultSettings.numberFieldPlaceholder', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_body.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_body.tsx index 3a2eb20fecdf0..c3b46f5852724 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_body.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_body.tsx @@ -56,7 +56,7 @@ export const TextFieldsBody: React.FC = () => { }} /> - + { }} /> - + { { defaultMessage: 'Raw' } )} - + {i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.resultSettings.table.column.maxSizeTitle', { defaultMessage: 'Max size' } @@ -48,7 +48,7 @@ export const TextFieldsHeader: React.FC = () => { { defaultMessage: 'Fallback' } )} - + {i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.resultSettings.table.column.maxSizeTitle', { defaultMessage: 'Max size' } diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx index a7eb2424e797a..0dd2b0988b3f4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx @@ -19,6 +19,7 @@ import { EuiFlexItem, EuiSpacer, EuiTitle, + EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -59,14 +60,15 @@ export const ProductSelector: React.FC = ({ access }) => { -

+

{i18n.translate('xpack.enterpriseSearch.overview.heading', { defaultMessage: 'Welcome to Elastic Enterprise Search', })}

- -

+ + +

{config.host ? i18n.translate('xpack.enterpriseSearch.overview.subheading', { defaultMessage: 'Select a product to get started.', @@ -75,7 +77,7 @@ export const ProductSelector: React.FC = ({ access }) => { defaultMessage: 'Choose a product to set up and get started.', })}

-
+
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.scss b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.scss index 45bf37def1121..4be8d7322b4c8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.scss +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.scss @@ -21,21 +21,7 @@ &__header { text-align: center; margin: auto; - } - - &__heading { - @include euiBreakpoint('xs', 's') { - font-size: $euiFontSizeXL; - line-height: map-get(map-get($euiTitles, 'm'), 'line-height'); - } - } - - &__subheading { - color: $euiColorMediumShade; - font-size: $euiFontSize; - @include euiBreakpoint('m', 'l', 'xl') { - font-size: $euiFontSizeL; margin-bottom: $euiSizeL; } } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts index 748dc6a7cbcf8..c599a13cc3119 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts @@ -303,6 +303,7 @@ export const exampleResult = { titleField: 'otherTitle', subtitleField: 'otherSubtitle', urlField: 'myLink', + urlFieldIsLinkable: true, color: '#e3e3e3', descriptionField: 'about', typeField: 'otherType', @@ -314,14 +315,18 @@ export const exampleResult = { { fieldName: 'dogs', label: 'Canines' }, ], }, - titleFieldHover: false, - urlFieldHover: false, exampleDocuments: [ { myLink: 'http://foo', otherTitle: 'foo', + content_source_id: '60e85e7ea2564c265a88a4f0', + external_id: 'doc-60e85eb7a2564c937a88a4f3', + last_updated: '2021-07-09T14:35:35+00:00', + updated_at: '2021-07-09T14:35:35+00:00', + source: 'custom', }, ], + schemaFields: {}, }; export const mostRecentIndexJob = { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/status_item/status_item.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/status_item/status_item.tsx index 79455ccc1d90d..35ac8f1b85c05 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/status_item/status_item.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/status_item/status_item.tsx @@ -44,7 +44,7 @@ export const StatusItem: React.FC = ({ details }) => { const infoPopover = ( { expect(getAsLocalDateTimeString(date)).toEqual(new Date(Date.parse(date)).toLocaleString()); }); + it('returns null if passed value is not a string', () => { + const date = ['1', '2']; + + expect(getAsLocalDateTimeString(date)).toEqual(null); + }); + it('returns null if string cannot be parsed as date', () => { const date = 'foo'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/get_as_local_datetime_string.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/get_as_local_datetime_string.ts index d5ceb50d4c9af..6350c4e4a4099 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/get_as_local_datetime_string.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/get_as_local_datetime_string.ts @@ -5,7 +5,11 @@ * 2.0. */ -export const getAsLocalDateTimeString = (str: string) => { - const dateValue = Date.parse(str); +import { CustomAPIFieldValue } from '../types'; + +export const getAsLocalDateTimeString = (maybeDate: CustomAPIFieldValue) => { + if (typeof maybeDate !== 'string') return null; + + const dateValue = Date.parse(maybeDate); return dateValue ? new Date(dateValue).toLocaleString() : null; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/mime_types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/mime_types.ts index f7664c90d461c..7a5020be5986e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/mime_types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/mime_types.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { CustomAPIFieldValue } from '../types'; + const mimeTypes = { 'application/iwork-keynote-sffkey': 'Keynote', 'application/x-iwork-keynote-sffkey': 'Keynote', @@ -51,4 +53,5 @@ const mimeTypes = { 'video/quicktime': 'MOV', } as { [key: string]: string }; -export const mimeType = (type: string) => mimeTypes[type.toLowerCase()] || type; +export const mimeType = (type: CustomAPIFieldValue) => + mimeTypes[type.toString().toLowerCase()] || type; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx index b30511f0a6d80..af94707aa3612 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx @@ -58,6 +58,13 @@ describe('AddSourceList', () => { expect(wrapper.find(AvailableSourcesList)).toHaveLength(1); }); + it('does not render header when loading', () => { + setMockValues({ ...mockValues, dataLoading: true }); + const wrapper = shallow(); + + expect(wrapper.prop('pageHeader')).toBe(undefined); + }); + describe('layout', () => { it('renders the default workplace search layout when on an organization view', () => { setMockValues({ ...mockValues, isOrganization: true }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx index a7a64194cb42f..165586dcc3903 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx @@ -104,7 +104,9 @@ export const AddSourceList: React.FC = () => { {!isOrganization && ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx index eef508b2e618f..8b0a72ac23e39 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx @@ -62,7 +62,7 @@ export const ExampleResultDetailCard: React.FC = () => {
{detailFields.length > 0 ? ( detailFields.map(({ fieldName, label }, index) => { - const value = result[fieldName] as string; + const value = result[fieldName]; const dateValue = getAsLocalDateTimeString(value); return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx index 549faf1676a54..3ca5b619c0366 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx @@ -117,7 +117,7 @@ export const ExampleSearchResultGroup: React.FC = () => { data-test-subj="MediaTypeField" > - {mimeType(result[mediaTypeField] as string)} + {mimeType(result[mediaTypeField])}
)} @@ -135,8 +135,7 @@ export const ExampleSearchResultGroup: React.FC = () => { by {result[updatedByField]}  )} - {getAsLocalDateTimeString(result.last_updated as string) || - result.last_updated} + {getAsLocalDateTimeString(result.last_updated) || result.last_updated}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx index 46b8de6789467..b3ba4c6d50973 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx @@ -109,7 +109,7 @@ export const ExampleStandoutResult: React.FC = () => { data-test-subj="MediaTypeField" > - {mimeType(result[mediaTypeField] as string)} + {mimeType(result[mediaTypeField])}
)} @@ -127,7 +127,7 @@ export const ExampleStandoutResult: React.FC = () => { by {result[updatedByField]}  )} - {getAsLocalDateTimeString(result.last_updated as string) || result.last_updated} + {getAsLocalDateTimeString(result.last_updated) || result.last_updated}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.test.tsx index 76c28ae3d4060..7506c499dff31 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.test.tsx @@ -5,6 +5,8 @@ * 2.0. */ +import { exampleResult } from '../../../../__mocks__/content_sources.mock'; + import React from 'react'; import { shallow } from 'enzyme'; @@ -12,7 +14,11 @@ import { shallow } from 'enzyme'; import { SubtitleField } from './subtitle_field'; describe('SubtitleField', () => { - const result = { foo: 'bar' }; + const result = { + ...exampleResult.exampleDocuments[0], + foo: 'bar', + }; + it('renders', () => { const props = { result, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.test.tsx index 2ed4aa0b0fad1..e5681bc7e8619 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.test.tsx @@ -5,6 +5,8 @@ * 2.0. */ +import { exampleResult } from '../../../../__mocks__/content_sources.mock'; + import React from 'react'; import { shallow } from 'enzyme'; @@ -12,7 +14,10 @@ import { shallow } from 'enzyme'; import { TitleField } from './title_field'; describe('TitleField', () => { - const result = { foo: 'bar' }; + const result = { + ...exampleResult.exampleDocuments[0], + foo: 'bar', + }; it('renders', () => { const props = { result, @@ -26,7 +31,10 @@ describe('TitleField', () => { it('handles title when array', () => { const props = { - result: { foo: ['baz', 'bar'] }, + result: { + ...exampleResult.exampleDocuments[0], + foo: ['baz', 'bar'], + }, titleField: 'foo', titleFieldHover: false, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx index a0e3c28f20eb0..a97cc85cb822a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx @@ -137,7 +137,7 @@ export const SourceContent: React.FC = () => { )} {urlFieldIsLinkable && ( - + )} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.test.tsx index 74f32cc22c2a8..da4346d54727c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.test.tsx @@ -125,5 +125,15 @@ describe('SourceSettings', () => { '/api/workplace_search/account/sources/123/download_diagnostics' ); }); + + it('renders with the correct download file name', () => { + jest.spyOn(global.Date, 'now').mockImplementationOnce(() => new Date('1970-01-01').valueOf()); + + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="DownloadDiagnosticsButton"]').prop('download')).toEqual( + '123_custom_0_diagnostics.json' + ); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx index 667e7fd4dbfb4..e4f52d94ad9e7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx @@ -6,14 +6,12 @@ */ import React, { useEffect, useState, ChangeEvent, FormEvent } from 'react'; -import { Link } from 'react-router-dom'; import { useActions, useValues } from 'kea'; import { isEmpty } from 'lodash'; import { EuiButton, - EuiButtonEmpty, EuiConfirmModal, EuiFieldText, EuiFlexGroup, @@ -22,6 +20,8 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { HttpLogic } from '../../../../shared/http'; +import { EuiButtonEmptyTo } from '../../../../shared/react_router_helpers'; import { AppLogic } from '../../../app_logic'; import { ContentSection } from '../../../components/shared/content_section'; import { SourceConfigFields } from '../../../components/shared/source_config_fields'; @@ -57,6 +57,8 @@ import { SourceLogic } from '../source_logic'; import { SourceLayout } from './source_layout'; export const SourceSettings: React.FC = () => { + const { http } = useValues(HttpLogic); + const { updateContentSource, removeContentSource } = useActions(SourceLogic); const { getSourceConfigData } = useActions(AddSourceLogic); @@ -90,8 +92,8 @@ export const SourceSettings: React.FC = () => { const { clientId, clientSecret, publicKey, consumerKey, baseUrl } = configuredFields || {}; const diagnosticsPath = isOrganization - ? `/api/workplace_search/org/sources/${id}/download_diagnostics` - : `/api/workplace_search/account/sources/${id}/download_diagnostics`; + ? http.basePath.prepend(`/api/workplace_search/org/sources/${id}/download_diagnostics`) + : http.basePath.prepend(`/api/workplace_search/account/sources/${id}/download_diagnostics`); const handleNameChange = (e: ChangeEvent) => setValue(e.target.value); @@ -172,9 +174,9 @@ export const SourceSettings: React.FC = () => { baseUrl={baseUrl} /> - - {SOURCE_CONFIG_LINK} - + + {SOURCE_CONFIG_LINK} + )} @@ -184,7 +186,7 @@ export const SourceSettings: React.FC = () => { href={diagnosticsPath} isLoading={buttonLoading} data-test-subj="DownloadDiagnosticsButton" - download + download={`${id}_${serviceType}_${Date.now()}_diagnostics.json`} > {SYNC_DIAGNOSTICS_BUTTON} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx index e7dfd6ddf1389..5714cc965827e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx @@ -33,24 +33,23 @@ import { NAV, CANCEL_BUTTON } from '../../../constants'; import { USERS_AND_ROLES_PATH } from '../../../routes'; import { GroupLogic, MAX_NAME_LENGTH } from '../group_logic'; -import { GroupUsersTable } from './group_users_table'; - export const EMPTY_SOURCES_DESCRIPTION = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.overview.emptySourcesDescription', { defaultMessage: 'No content sources are shared with this group.', } ); -const GROUP_USERS_DESCRIPTION = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.groups.overview.groupUsersDescription', +const USERS_SECTION_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.groups.overview.usersSectionTitle', { - defaultMessage: 'Members will be able to search over the group’s sources.', + defaultMessage: 'Group users', } ); -export const EMPTY_USERS_DESCRIPTION = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.groups.overview.emptyUsersDescription', +const GROUP_USERS_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.groups.overview.groupUsersDescription', { - defaultMessage: 'There are no users in this group.', + defaultMessage: + "Users assigned to this group gain access to the sources' data and content defined above. User assignments for this group can be managed in the Users and Roles area.", } ); const MANAGE_SOURCES_BUTTON_TEXT = i18n.translate( @@ -118,7 +117,7 @@ export const GroupOverview: React.FC = () => { onGroupNameInputChange, } = useActions(GroupLogic); const { - group: { name, contentSources, users, canDeleteGroup }, + group: { name, contentSources, canDeleteGroup }, groupNameInputValue, dataLoading, confirmDeleteModalVisible, @@ -158,7 +157,6 @@ export const GroupOverview: React.FC = () => { ); const hasContentSources = contentSources?.length > 0; - const hasUsers = users?.length > 0; const manageSourcesButton = ( @@ -199,12 +197,11 @@ export const GroupOverview: React.FC = () => { const usersSection = ( - {hasUsers && } + {manageUsersButton} ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.test.tsx index f98b873aed5bb..770bf8a51efd3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.test.tsx @@ -14,8 +14,7 @@ import moment from 'moment'; import { EuiTableRow } from '@elastic/eui'; -import { GroupRow, NO_USERS_MESSAGE, NO_SOURCES_MESSAGE } from './group_row'; -import { GroupUsers } from './group_users'; +import { GroupRow, NO_SOURCES_MESSAGE } from './group_row'; describe('GroupRow', () => { it('renders', () => { @@ -24,12 +23,6 @@ describe('GroupRow', () => { expect(wrapper.find(EuiTableRow)).toHaveLength(1); }); - it('renders group users', () => { - const wrapper = shallow(); - - expect(wrapper.find(GroupUsers)).toHaveLength(1); - }); - it('renders fromNow date string when in range', () => { const wrapper = shallow( @@ -44,12 +37,6 @@ describe('GroupRow', () => { expect(wrapper.find('small').text()).toEqual('Last updated January 1, 2020.'); }); - it('renders empty users message when no users present', () => { - const wrapper = shallow(); - - expect(wrapper.find('.user-group__accounts').text()).toEqual(NO_USERS_MESSAGE); - }); - it('renders empty sources message when no sources present', () => { const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx index 94d44fde57aed..d079eb34fbf89 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx @@ -19,7 +19,6 @@ import { Group } from '../../../types'; import { MAX_NAME_LENGTH } from '../group_logic'; import { GroupSources } from './group_sources'; -import { GroupUsers } from './group_users'; const DAYS_CUTOFF = 8; export const NO_SOURCES_MESSAGE = i18n.translate( @@ -40,14 +39,7 @@ const dateDisplay = (date: string) => ? moment(date).fromNow() : moment(date).format('MMMM D, YYYY'); -export const GroupRow: React.FC = ({ - id, - name, - updatedAt, - contentSources, - users, - usersCount, -}) => { +export const GroupRow: React.FC = ({ id, name, updatedAt, contentSources }) => { const GROUP_UPDATED_TEXT = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.groupUpdatedText', { @@ -76,15 +68,6 @@ export const GroupRow: React.FC = ({ )}
- -
- {usersCount > 0 ? ( - - ) : ( - NO_USERS_MESSAGE - )} -
-
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx index cfb3ed8044235..45175e489f94a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx @@ -36,12 +36,6 @@ const SOURCES_TABLE_HEADER = i18n.translate( defaultMessage: 'Content sources', } ); -const USERS_TABLE_HEADER = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.usersTableHeader', - { - defaultMessage: 'Users', - } -); export const GroupsTable: React.FC<{}> = () => { const { setActivePage } = useActions(GroupsLogic); @@ -77,7 +71,6 @@ export const GroupsTable: React.FC<{}> = () => { {GROUP_TABLE_HEADER} {SOURCES_TABLE_HEADER} - {USERS_TABLE_HEADER} diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts index e6524151b0a6c..f9756119b336c 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts @@ -209,17 +209,23 @@ describe('EnterpriseSearchRequestHandler', () => { headers: mockExpectedResponseHeaders, }); }); - }); - it('works if response contains no json data', async () => { - EnterpriseSearchAPI.mockReturn(); + it('passes back the response body as-is if hasJsonResponse is false', async () => { + const mockFile = new File(['mockFile'], 'mockFile.json'); + EnterpriseSearchAPI.mockReturn(mockFile); - const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/prep' }); - await makeAPICall(requestHandler); + const requestHandler = enterpriseSearchRequestHandler.createRequest({ + path: '/api/file', + hasJsonResponse: false, + }); + await makeAPICall(requestHandler); - expect(responseMock.custom).toHaveBeenCalledWith({ - statusCode: 200, - headers: mockExpectedResponseHeaders, + EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/file'); + expect(responseMock.custom).toHaveBeenCalledWith({ + body: expect.any(Buffer), // Unfortunately Response() buffers the body so we can't actually inspect/equality assert on it + statusCode: 200, + headers: mockExpectedResponseHeaders, + }); }); }); }); @@ -397,7 +403,7 @@ describe('EnterpriseSearchRequestHandler', () => { expect(responseMock.customError).toHaveBeenCalledWith({ statusCode: 502, body: 'Cannot authenticate Enterprise Search user', - headers: mockExpectedResponseHeaders, + headers: { ...mockExpectedResponseHeaders, [ERROR_CONNECTING_HEADER]: 'true' }, }); expect(mockLogger.error).toHaveBeenCalled(); }); diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts index b4768c1a9ee15..57b91c2b30c73 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts @@ -34,6 +34,7 @@ interface ConstructorDependencies { interface RequestParams { path: string; params?: object; + hasJsonResponse?: boolean; hasValidData?: Function; } interface ErrorResponse { @@ -64,7 +65,12 @@ export class EnterpriseSearchRequestHandler { this.enterpriseSearchUrl = config.host as string; } - createRequest({ path, params = {}, hasValidData = () => true }: RequestParams) { + createRequest({ + path, + params = {}, + hasJsonResponse = true, + hasValidData = () => true, + }: RequestParams) { return async ( _context: RequestHandlerContext, request: KibanaRequest, @@ -119,7 +125,7 @@ export class EnterpriseSearchRequestHandler { // Check returned data let responseBody; - try { + if (hasJsonResponse) { const json = await apiResponse.json(); if (!hasValidData(json)) { @@ -134,8 +140,8 @@ export class EnterpriseSearchRequestHandler { } else { responseBody = json; } - } catch (e) { - responseBody = undefined; + } else { + responseBody = apiResponse.body; } // Pass successful responses back to the front-end @@ -277,12 +283,11 @@ export class EnterpriseSearchRequestHandler { handleConnectionError(response: KibanaResponseFactory, e: Error) { const errorMessage = `Error connecting to Enterprise Search: ${e?.message || e.toString()}`; + const headers = { ...this.headers, [ERROR_CONNECTING_HEADER]: 'true' }; this.log.error(errorMessage); if (e instanceof Error) this.log.debug(e.stack as string); - const headers = { ...this.headers, [ERROR_CONNECTING_HEADER]: 'true' }; - return response.customError({ statusCode: 502, headers, body: errorMessage }); } @@ -292,9 +297,10 @@ export class EnterpriseSearchRequestHandler { */ handleAuthenticationError(response: KibanaResponseFactory) { const errorMessage = 'Cannot authenticate Enterprise Search user'; + const headers = { ...this.headers, [ERROR_CONNECTING_HEADER]: 'true' }; this.log.error(errorMessage); - return response.customError({ statusCode: 502, headers: this.headers, body: errorMessage }); + return response.customError({ statusCode: 502, headers, body: errorMessage }); } /** diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/onboarding.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/onboarding.test.ts index c26f8dbaf5213..47b480f61341a 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/onboarding.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/onboarding.test.ts @@ -30,6 +30,7 @@ describe('engine routes', () => { mockRouter.callRoute({ body: {} }); expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ path: '/as/onboarding/complete', + hasJsonResponse: false, }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/onboarding.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/onboarding.ts index 9a46c75555969..147f935a56aed 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/onboarding.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/onboarding.ts @@ -24,6 +24,7 @@ export function registerOnboardingRoutes({ }, enterpriseSearchRequestHandler.createRequest({ path: '/as/onboarding/complete', + hasJsonResponse: false, }) ); } diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts index 4043f9daddaa7..a68a7716933f8 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts @@ -559,6 +559,7 @@ describe('sources routes', () => { it('creates a request handler', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ path: '/ws/sources/:sourceId/download_diagnostics', + hasJsonResponse: false, }); }); }); @@ -1057,6 +1058,7 @@ describe('sources routes', () => { it('creates a request handler', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ path: '/ws/org/sources/:sourceId/download_diagnostics', + hasJsonResponse: false, }); }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts index b393ab9d1f26a..5de4387f2c0d9 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts @@ -400,6 +400,7 @@ export function registerAccountSourceDownloadDiagnosticsRoute({ }, enterpriseSearchRequestHandler.createRequest({ path: '/ws/sources/:sourceId/download_diagnostics', + hasJsonResponse: false, }) ); } @@ -748,6 +749,7 @@ export function registerOrgSourceDownloadDiagnosticsRoute({ }, enterpriseSearchRequestHandler.createRequest({ path: '/ws/org/sources/:sourceId/download_diagnostics', + hasJsonResponse: false, }) ); } diff --git a/x-pack/plugins/file_upload/server/plugin.ts b/x-pack/plugins/file_upload/server/plugin.ts index c729afec92f94..bd5eebe372a75 100644 --- a/x-pack/plugins/file_upload/server/plugin.ts +++ b/x-pack/plugins/file_upload/server/plugin.ts @@ -21,7 +21,7 @@ export class FileUploadPlugin implements Plugin { this._logger = initializerContext.logger.get(); } - async setup(coreSetup: CoreSetup, plugins: SetupDeps) { + setup(coreSetup: CoreSetup, plugins: SetupDeps) { fileUploadRoutes(coreSetup, this._logger); setupCapabilities(coreSetup); diff --git a/x-pack/plugins/fleet/common/services/agent_status.ts b/x-pack/plugins/fleet/common/services/agent_status.ts index b8a59e6447723..e4b227b79536c 100644 --- a/x-pack/plugins/fleet/common/services/agent_status.ts +++ b/x-pack/plugins/fleet/common/services/agent_status.ts @@ -54,7 +54,7 @@ export function buildKueryForOnlineAgents() { } export function buildKueryForErrorAgents() { - return 'last_checkin_status:error or last_checkin_status:degraded'; + return `(last_checkin_status:error or last_checkin_status:degraded) AND not (${buildKueryForUpdatingAgents()})`; } export function buildKueryForOfflineAgents() { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index b07d76dc6bd8e..ee529b6865e56 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -430,7 +430,9 @@ export const EditPackagePolicyForm = memo<{ /> )} {configurePackage} - + {/* Extra space to accomodate the EuiBottomBar height */} + + diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_dataset.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_dataset.tsx index 47174561230ba..18f6a8b565ab9 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_dataset.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_dataset.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { memo, useState, useEffect } from 'react'; +import React, { memo, useState, useEffect, useCallback } from 'react'; import { EuiPopover, EuiFilterButton, EuiFilterSelectItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -22,6 +22,9 @@ export const DatasetFilter: React.FunctionComponent<{ const [isLoading, setIsLoading] = useState(false); const [datasetValues, setDatasetValues] = useState([AGENT_DATASET]); + const togglePopover = useCallback(() => setIsOpen((prevIsOpen) => !prevIsOpen), [setIsOpen]); + const closePopover = useCallback(() => setIsOpen(false), [setIsOpen]); + useEffect(() => { const fetchValues = async () => { setIsLoading(true); @@ -48,7 +51,7 @@ export const DatasetFilter: React.FunctionComponent<{ button={ setIsOpen(true)} + onClick={togglePopover} isSelected={isOpen} isLoading={isLoading} numFilters={datasetValues.length} @@ -61,7 +64,7 @@ export const DatasetFilter: React.FunctionComponent<{ } isOpen={isOpen} - closePopover={() => setIsOpen(false)} + closePopover={closePopover} panelPaddingSize="none" > {datasetValues.map((dataset) => ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_log_level.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_log_level.tsx index 120f21fe68207..b423f3a8a57b3 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_log_level.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_log_level.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { memo, useState, useEffect } from 'react'; +import React, { memo, useState, useEffect, useCallback } from 'react'; import { EuiPopover, EuiFilterButton, EuiFilterSelectItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -33,6 +33,9 @@ export const LogLevelFilter: React.FunctionComponent<{ const [isLoading, setIsLoading] = useState(false); const [levelValues, setLevelValues] = useState([]); + const togglePopover = useCallback(() => setIsOpen((prevIsOpen) => !prevIsOpen), []); + const closePopover = useCallback(() => setIsOpen(false), []); + useEffect(() => { const fetchValues = async () => { setIsLoading(true); @@ -59,7 +62,7 @@ export const LogLevelFilter: React.FunctionComponent<{ button={ setIsOpen(true)} + onClick={togglePopover} isSelected={isOpen} isLoading={isLoading} numFilters={levelValues.length} @@ -72,7 +75,7 @@ export const LogLevelFilter: React.FunctionComponent<{ } isOpen={isOpen} - closePopover={() => setIsOpen(false)} + closePopover={closePopover} panelPaddingSize="none" > {levelValues.map((level) => ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx index 48ff51f1a25e8..0fc3821d2e3f7 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx @@ -452,6 +452,8 @@ export const AddFleetServerHostStepContent = ({ await addFleetServerHost(fleetServerHost); setCalloutHost(fleetServerHost); setFleetServerHost(''); + } else { + setCalloutHost(''); } } finally { setIsLoading(false); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_unenroll_modal/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_unenroll_modal/index.tsx index 0b13fcc9c72be..fae88fcda4bc8 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_unenroll_modal/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_unenroll_modal/index.tsx @@ -137,7 +137,7 @@ export const AgentUnenrollAgentModal: React.FunctionComponent = ({

diff --git a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_package_install.tsx b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_package_install.tsx index 342d6b54c2613..edbe06f33b18e 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_package_install.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_package_install.tsx @@ -118,7 +118,12 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar ); const uninstallPackage = useCallback( - async ({ name, version, title }: Pick) => { + async ({ + name, + version, + title, + redirectToVersion, + }: Pick & { redirectToVersion: string }) => { setPackageInstallStatus({ name, status: InstallStatus.uninstalling, version }); const pkgkey = `${name}-${version}`; @@ -160,9 +165,15 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar /> ), }); + if (redirectToVersion !== version) { + const settingsPath = getPath('integration_details_settings', { + pkgkey: `${name}-${redirectToVersion}`, + }); + history.push(settingsPath); + } } }, - [notifications.toasts, setPackageInstallStatus] + [notifications.toasts, setPackageInstallStatus, getPath, history] ); return { diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx index 96e4071e9b464..63372e435cfa1 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx @@ -151,7 +151,10 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps /> - + {i18n.translate('xpack.fleet.epm.agentEnrollment.viewDataAssetsLabel', { defaultMessage: 'View assets', })} diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/installation_button.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/installation_button.tsx index 8cf8466e6d9b0..eab28a051f061 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/installation_button.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/installation_button.tsx @@ -24,9 +24,10 @@ import { ConfirmPackageInstall } from './confirm_package_install'; type InstallationButtonProps = Pick & { disabled?: boolean; isUpdate?: boolean; + latestVersion?: string; }; export function InstallationButton(props: InstallationButtonProps) { - const { assets, name, title, version, disabled = true, isUpdate = false } = props; + const { assets, name, title, version, disabled = true, isUpdate = false, latestVersion } = props; const hasWriteCapabilites = useCapabilities().write; const installPackage = useInstallPackage(); const uninstallPackage = useUninstallPackage(); @@ -52,9 +53,9 @@ export function InstallationButton(props: InstallationButtonProps) { }, [installPackage, name, title, version]); const handleClickUninstall = useCallback(() => { - uninstallPackage({ name, version, title }); + uninstallPackage({ name, version, title, redirectToVersion: latestVersion ?? version }); toggleModal(); - }, [uninstallPackage, name, title, toggleModal, version]); + }, [uninstallPackage, name, title, toggleModal, version, latestVersion]); // counts the number of assets in the package const numOfAssets = useMemo( diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx index 9e8d200344b01..14f378bc379a6 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx @@ -10,11 +10,11 @@ import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; import semverLt from 'semver/functions/lt'; -import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer } from '@elastic/eui'; +import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer, EuiLink } from '@elastic/eui'; import type { PackageInfo } from '../../../../../types'; import { InstallStatus } from '../../../../../types'; -import { useGetPackagePolicies, useGetPackageInstallStatus } from '../../../../../hooks'; +import { useGetPackagePolicies, useGetPackageInstallStatus, useLink } from '../../../../../hooks'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants'; import { UpdateIcon } from '../components'; @@ -47,6 +47,21 @@ const UpdatesAvailableMsg = () => ( ); +const LatestVersionLink = ({ name, version }: { name: string; version: string }) => { + const { getPath } = useLink(); + const settingsPath = getPath('integration_details_settings', { + pkgkey: `${name}-${version}`, + }); + return ( + + + + ); +}; + interface Props { packageInfo: PackageInfo; } @@ -72,6 +87,7 @@ export const SettingsPage: React.FC = memo(({ packageInfo }: Props) => { (installationStatus === InstallStatus.installed && installedVersion !== version); const isUpdating = installationStatus === InstallStatus.installing && installedVersion; + return ( @@ -206,6 +222,7 @@ export const SettingsPage: React.FC = memo(({ packageInfo }: Props) => {

@@ -244,6 +261,37 @@ export const SettingsPage: React.FC = memo(({ packageInfo }: Props) => { )}
)} + {hideInstallOptions && isViewingOldPackage && !isUpdating && ( +
+ +
+ +

+ +

+
+ +

+ + , + }} + /> + +

+
+
+ )} diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts index d2e7c4089e88b..5c292187982dc 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.mocks.ts @@ -11,6 +11,7 @@ jest.mock('../../hooks/use_request', () => { ...module, useGetSettings: jest.fn(), sendGetFleetStatus: jest.fn(), + sendGetOneAgentPolicy: jest.fn(), }; }); diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx index f68b1b878c51c..18296134ee1a7 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_enrollment_flyout.test.tsx @@ -16,7 +16,7 @@ import { coreMock } from 'src/core/public/mocks'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import type { AgentPolicy } from '../../../common'; -import { useGetSettings, sendGetFleetStatus } from '../../hooks/use_request'; +import { useGetSettings, sendGetFleetStatus, sendGetOneAgentPolicy } from '../../hooks/use_request'; import { FleetStatusProvider, ConfigContext } from '../../hooks'; import { useFleetServerInstructions } from '../../applications/fleet/sections/agents/agent_requirements_page/components'; @@ -79,6 +79,10 @@ describe('', () => { data: { isReady: true }, }); + (sendGetOneAgentPolicy as jest.Mock).mockResolvedValue({ + data: { item: { package_policies: [] } }, + }); + (useFleetServerInstructions as jest.Mock).mockReturnValue({ serviceToken: 'test', getServiceToken: jest.fn(), diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx index 9b82b2a80b5e1..87911e5d6c2c7 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx @@ -22,7 +22,9 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useGetSettings, useUrlModal } from '../../hooks'; +import { useGetSettings, useUrlModal, sendGetOneAgentPolicy, useFleetStatus } from '../../hooks'; +import { FLEET_SERVER_PACKAGE } from '../../constants'; +import type { PackagePolicy } from '../../types'; import { ManagedInstructions } from './managed_instructions'; import { StandaloneInstructions } from './standalone_instructions'; @@ -63,6 +65,30 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ } }, [modal, lastModal, settings]); + const fleetStatus = useFleetStatus(); + const [policyId, setSelectedPolicyId] = useState(agentPolicy?.id); + const [isFleetServerPolicySelected, setIsFleetServerPolicySelected] = useState(false); + + useEffect(() => { + async function checkPolicyIsFleetServer() { + if (policyId && setIsFleetServerPolicySelected) { + const agentPolicyRequest = await sendGetOneAgentPolicy(policyId); + if ( + agentPolicyRequest.data?.item && + (agentPolicyRequest.data.item.package_policies as PackagePolicy[]).some( + (packagePolicy) => packagePolicy.package?.name === FLEET_SERVER_PACKAGE + ) + ) { + setIsFleetServerPolicySelected(true); + } else { + setIsFleetServerPolicySelected(false); + } + } + } + + checkPolicyIsFleetServer(); + }, [policyId]); + const isLoadingInitialRequest = settings.isLoading && settings.isInitialRequest; return ( @@ -110,16 +136,23 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ ) : undefined } > - {fleetServerHosts.length === 0 && mode === 'managed' ? null : mode === 'managed' ? ( + {mode === 'managed' ? ( ) : ( diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx index 61f86335cd7f9..8054c48fbbaa8 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx @@ -11,7 +11,7 @@ import type { EuiContainedStepProps } from '@elastic/eui/src/components/steps/st import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useGetOneEnrollmentAPIKey, useGetSettings, useLink, useFleetStatus } from '../../hooks'; +import { useGetOneEnrollmentAPIKey, useLink, useFleetStatus } from '../../hooks'; import { ManualInstructions } from '../../components/enrollment_instructions'; import { @@ -56,14 +56,19 @@ const FleetServerMissingRequirements = () => { }; export const ManagedInstructions = React.memo( - ({ agentPolicy, agentPolicies, viewDataStep }) => { + ({ + agentPolicy, + agentPolicies, + viewDataStep, + setSelectedPolicyId, + isFleetServerPolicySelected, + settings, + }) => { const fleetStatus = useFleetStatus(); const [selectedApiKeyId, setSelectedAPIKeyId] = useState(); - const [isFleetServerPolicySelected, setIsFleetServerPolicySelected] = useState(false); const apiKey = useGetOneEnrollmentAPIKey(selectedApiKeyId); - const settings = useGetSettings(); const fleetServerInstructions = useFleetServerInstructions(apiKey?.data?.item?.policy_id); const fleetServerSteps = useMemo(() => { @@ -88,7 +93,7 @@ export const ManagedInstructions = React.memo( }, [fleetServerInstructions]); const steps = useMemo(() => { - const fleetServerHosts = settings.data?.item?.fleet_server_hosts || []; + const fleetServerHosts = settings?.fleet_server_hosts || []; const baseSteps: EuiContainedStepProps[] = [ DownloadStep(), !agentPolicy @@ -96,7 +101,7 @@ export const ManagedInstructions = React.memo( agentPolicies, selectedApiKeyId, setSelectedAPIKeyId, - setIsFleetServerPolicySelected, + setSelectedPolicyId, }) : AgentEnrollmentKeySelectionStep({ agentPolicy, selectedApiKeyId, setSelectedAPIKeyId }), ]; @@ -121,30 +126,39 @@ export const ManagedInstructions = React.memo( }, [ agentPolicy, selectedApiKeyId, + setSelectedPolicyId, setSelectedAPIKeyId, agentPolicies, apiKey.data, fleetServerSteps, isFleetServerPolicySelected, - settings.data?.item?.fleet_server_hosts, + settings?.fleet_server_hosts, viewDataStep, ]); + if (fleetStatus.isReady && settings?.fleet_server_hosts.length === 0) { + return null; + } + + if (fleetStatus.isReady) { + return ( + <> + + + + + + + ); + } + return ( <> - {fleetStatus.isReady ? ( - <> - - - - - - - ) : fleetStatus.missingRequirements?.length === 1 && - fleetStatus.missingRequirements[0] === 'fleet_server' ? ( + {fleetStatus.missingRequirements?.length === 1 && + fleetStatus.missingRequirements[0] === 'fleet_server' ? ( ) : ( diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx index 6cffa39628d92..1cfdc45fb7dba 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx @@ -11,9 +11,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import semver from 'semver'; -import type { AgentPolicy, PackagePolicy } from '../../types'; -import { sendGetOneAgentPolicy, useKibanaVersion } from '../../hooks'; -import { FLEET_SERVER_PACKAGE } from '../../constants'; +import type { AgentPolicy } from '../../types'; +import { useKibanaVersion } from '../../hooks'; import { EnrollmentStepAgentPolicy } from './agent_policy_selection'; import { AdvancedAgentAuthenticationSettings } from './advanced_agent_authentication_settings'; @@ -69,13 +68,11 @@ export const AgentPolicySelectionStep = ({ selectedApiKeyId, setSelectedAPIKeyId, excludeFleetServer, - setIsFleetServerPolicySelected, }: { agentPolicies?: AgentPolicy[]; setSelectedPolicyId?: (policyId?: string) => void; selectedApiKeyId?: string; setSelectedAPIKeyId?: (key?: string) => void; - setIsFleetServerPolicySelected?: (selected: boolean) => void; excludeFleetServer?: boolean; }) => { const regularAgentPolicies = useMemo(() => { @@ -92,21 +89,8 @@ export const AgentPolicySelectionStep = ({ if (setSelectedPolicyId) { setSelectedPolicyId(policyId); } - if (policyId && setIsFleetServerPolicySelected) { - const agentPolicyRequest = await sendGetOneAgentPolicy(policyId); - if ( - agentPolicyRequest.data?.item && - (agentPolicyRequest.data.item.package_policies as PackagePolicy[]).some( - (packagePolicy) => packagePolicy.package?.name === FLEET_SERVER_PACKAGE - ) - ) { - setIsFleetServerPolicySelected(true); - } else { - setIsFleetServerPolicySelected(false); - } - } }, - [setIsFleetServerPolicySelected, setSelectedPolicyId] + [setSelectedPolicyId] ); return { diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts index 9ee514c634655..282a5b243caed 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/types.ts @@ -7,7 +7,7 @@ import type { EuiStepProps } from '@elastic/eui'; -import type { AgentPolicy } from '../../types'; +import type { AgentPolicy, Settings } from '../../types'; export interface BaseProps { /** @@ -27,4 +27,10 @@ export interface BaseProps { * in some way. This is an area for consumers to render a button and text explaining how data can be viewed. */ viewDataStep?: EuiStepProps; + + settings?: Settings; + + setSelectedPolicyId?: (policyId?: string) => void; + + isFleetServerPolicySelected?: boolean; } diff --git a/x-pack/plugins/fleet/public/types/index.ts b/x-pack/plugins/fleet/public/types/index.ts index f21552d68e77b..c91ec42d3e527 100644 --- a/x-pack/plugins/fleet/public/types/index.ts +++ b/x-pack/plugins/fleet/public/types/index.ts @@ -27,6 +27,7 @@ export { PackagePolicyPackage, Output, DataStream, + Settings, // API schema - misc setup, status GetFleetStatusResponse, // API schemas - Agent policy diff --git a/x-pack/plugins/graph/public/plugin.ts b/x-pack/plugins/graph/public/plugin.ts index 4525b42b3feb4..ec19e639b91c9 100644 --- a/x-pack/plugins/graph/public/plugin.ts +++ b/x-pack/plugins/graph/public/plugin.ts @@ -19,10 +19,7 @@ import { } from '../../../../src/core/public'; import { Storage } from '../../../../src/plugins/kibana_utils/public'; -import { - initAngularBootstrap, - KibanaLegacyStart, -} from '../../../../src/plugins/kibana_legacy/public'; +import { KibanaLegacyStart } from '../../../../src/plugins/kibana_legacy/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../src/plugins/navigation/public'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; @@ -77,7 +74,6 @@ export class GraphPlugin const config = this.initializerContext.config.get(); - initAngularBootstrap(); core.application.register({ id: 'graph', title: 'Graph', @@ -88,6 +84,7 @@ export class GraphPlugin updater$: this.appUpdater$, mount: async (params: AppMountParameters) => { const [coreStart, pluginsStart] = await core.getStartServices(); + await pluginsStart.kibanaLegacy.loadAngularBootstrap(); coreStart.chrome.docTitle.change( i18n.translate('xpack.graph.pageTitle', { defaultMessage: 'Graph' }) ); diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/js/index.ts b/x-pack/plugins/infra/common/alerting/logs/log_threshold/index.ts similarity index 82% rename from x-pack/plugins/console_extensions/server/lib/spec_definitions/js/index.ts rename to x-pack/plugins/infra/common/alerting/logs/log_threshold/index.ts index ef5a474df32d5..3f4cbc82c405c 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/js/index.ts +++ b/x-pack/plugins/infra/common/alerting/logs/log_threshold/index.ts @@ -5,4 +5,5 @@ * 2.0. */ -export { processors } from './ingest'; +export * from './rule_data'; +export * from './types'; diff --git a/x-pack/plugins/infra/common/alerting/logs/log_threshold/rule_data.ts b/x-pack/plugins/infra/common/alerting/logs/log_threshold/rule_data.ts new file mode 100644 index 0000000000000..dd60739289756 --- /dev/null +++ b/x-pack/plugins/infra/common/alerting/logs/log_threshold/rule_data.ts @@ -0,0 +1,19 @@ +/* + * 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 { jsonRt } from '@kbn/io-ts-utils/target/json_rt'; +import * as rt from 'io-ts'; +import { alertParamsRT as logThresholdAlertParamsRT } from './types'; + +export const serializedParamsKey = 'serialized_params'; + +export const logThresholdRuleDataNamespace = 'log_threshold_rule'; +export const logThresholdRuleDataSerializedParamsKey = `${logThresholdRuleDataNamespace}.${serializedParamsKey}` as const; + +export const logThresholdRuleDataRT = rt.type({ + [logThresholdRuleDataSerializedParamsKey]: rt.array(jsonRt.pipe(logThresholdAlertParamsRT)), +}); diff --git a/x-pack/plugins/infra/common/constants.ts b/x-pack/plugins/infra/common/constants.ts index 9362293fce82f..1c3aa550f2f62 100644 --- a/x-pack/plugins/infra/common/constants.ts +++ b/x-pack/plugins/infra/common/constants.ts @@ -11,3 +11,8 @@ export const LOGS_INDEX_PATTERN = 'logs-*,filebeat-*,kibana_sample_data_logs*'; export const TIMESTAMP_FIELD = '@timestamp'; export const METRICS_APP = 'metrics'; export const LOGS_APP = 'logs'; + +export const METRICS_FEATURE_ID = 'infrastructure'; +export const LOGS_FEATURE_ID = 'logs'; + +export type InfraFeatureId = typeof METRICS_FEATURE_ID | typeof LOGS_FEATURE_ID; diff --git a/x-pack/plugins/infra/kibana.json b/x-pack/plugins/infra/kibana.json index ec1b11c90f7a3..981036114282e 100644 --- a/x-pack/plugins/infra/kibana.json +++ b/x-pack/plugins/infra/kibana.json @@ -12,7 +12,8 @@ "visTypeTimeseries", "alerting", "triggersActionsUi", - "observability" + "observability", + "ruleRegistry" ], "optionalPlugins": ["ml", "home", "embeddable"], "server": true, diff --git a/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx index cf84ea40d64cc..1f2998db4b43f 100644 --- a/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/alerting/common/components/metrics_alert_dropdown.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import React, { useState, useCallback, useMemo } from 'react'; import { EuiPopover, - EuiButtonEmpty, + EuiHeaderLink, EuiContextMenu, EuiContextMenuPanelDescriptor, } from '@elastic/eui'; @@ -134,8 +134,7 @@ export const MetricsAlertDropdown = () => { panelPaddingSize="none" anchorPosition="downLeft" button={ - { id="xpack.infra.alerting.alertsButton" defaultMessage="Alerts and rules" /> - + } isOpen={popoverOpen} closePopover={closePopover} diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx index f3481cab73360..302de15db9f5a 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx @@ -7,7 +7,7 @@ import React, { useState, useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; +import { EuiPopover, EuiContextMenuItem, EuiContextMenuPanel, EuiHeaderLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { AlertFlyout } from './alert_flyout'; import { useLinkProps } from '../../../hooks/use_link_props'; @@ -83,8 +83,7 @@ export const AlertDropdown = () => { { id="xpack.infra.alerting.logs.alertsButton" defaultMessage="Alerts and rules" /> - + } isOpen={popoverOpen} closePopover={closePopover} diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/index.ts b/x-pack/plugins/infra/public/alerting/log_threshold/index.ts index 5bd64de2f3ac2..0f2746b446927 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/index.ts +++ b/x-pack/plugins/infra/public/alerting/log_threshold/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export { getAlertType } from './log_threshold_alert_type'; +export * from './log_threshold_alert_type'; export { AlertDropdown } from './components/alert_dropdown'; diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts b/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts index 2c8a6a7ea286a..44097fd005cc7 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts +++ b/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts @@ -7,14 +7,15 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; -import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; +import { ObservabilityRuleTypeModel } from '../../../../observability/public'; import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, PartialAlertParams, } from '../../../common/alerting/logs/log_threshold/types'; +import { formatReason } from './rule_data_formatters'; import { validateExpression } from './validation'; -export function getAlertType(): AlertTypeModel { +export function createLogThresholdAlertType(): ObservabilityRuleTypeModel { return { id: LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, description: i18n.translate('xpack.infra.logs.alertFlyout.alertDescription', { @@ -33,5 +34,6 @@ export function getAlertType(): AlertTypeModel { } ), requiresAppContext: false, + format: formatReason, }; } diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/rule_data_formatters.ts b/x-pack/plugins/infra/public/alerting/log_threshold/rule_data_formatters.ts new file mode 100644 index 0000000000000..6ca081ffbc5ef --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/log_threshold/rule_data_formatters.ts @@ -0,0 +1,87 @@ +/* + * 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 { + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, + ALERT_ID, + ALERT_START, +} from '@kbn/rule-data-utils'; +import { modifyUrl } from '@kbn/std'; +import { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/function'; +import { ObservabilityRuleTypeFormatter } from '../../../../observability/public'; +import { + ComparatorToi18nMap, + logThresholdRuleDataRT, + logThresholdRuleDataSerializedParamsKey, + ratioAlertParamsRT, +} from '../../../common/alerting/logs/log_threshold'; + +export const formatReason: ObservabilityRuleTypeFormatter = ({ fields }) => { + const reason = pipe( + logThresholdRuleDataRT.decode(fields), + fold( + () => + i18n.translate('xpack.infra.logs.alerting.threshold.unknownReasonDescription', { + defaultMessage: 'unknown reason', + }), + (logThresholdRuleData) => { + const params = logThresholdRuleData[logThresholdRuleDataSerializedParamsKey][0]; + + const actualCount = fields[ALERT_EVALUATION_VALUE]; + const groupName = fields[ALERT_ID]; + const isGrouped = (params.groupBy?.length ?? 0) > 0; + const isRatio = ratioAlertParamsRT.is(params); + const thresholdCount = fields[ALERT_EVALUATION_THRESHOLD]; + const translatedComparator = ComparatorToi18nMap[params.count.comparator]; + + if (isRatio) { + return i18n.translate('xpack.infra.logs.alerting.threshold.ratioAlertReasonDescription', { + defaultMessage: + '{isGrouped, select, true{{groupName}: } false{}}The log entries ratio is {actualCount} ({translatedComparator} {thresholdCount}).', + values: { + actualCount, + translatedComparator, + groupName, + isGrouped, + thresholdCount, + }, + }); + } else { + return i18n.translate('xpack.infra.logs.alerting.threshold.countAlertReasonDescription', { + defaultMessage: + '{isGrouped, select, true{{groupName}: } false{}}{actualCount, plural, one {{actualCount} log entry} other {{actualCount} log entries} } ({translatedComparator} {thresholdCount}) match the configured conditions.', + values: { + actualCount, + translatedComparator, + groupName, + isGrouped, + thresholdCount, + }, + }); + } + } + ) + ); + + const alertStartDate = fields[ALERT_START]; + const timestamp = alertStartDate != null ? new Date(alertStartDate).valueOf() : null; + const link = modifyUrl('/app/logs/link-to/default/logs', ({ query, ...otherUrlParts }) => ({ + ...otherUrlParts, + query: { + ...query, + ...(timestamp != null ? { time: `${timestamp}` } : {}), + }, + })); + + return { + reason, + link, // TODO: refactor to URL generators + }; +}; diff --git a/x-pack/plugins/infra/public/pages/logs/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/page_content.tsx index 8175a95f6a064..d8b5667e60d04 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_content.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; +import { EuiHeaderLinks, EuiHeaderLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useContext } from 'react'; import { Route, Switch } from 'react-router-dom'; @@ -78,28 +78,19 @@ export const LogsPageContent: React.FunctionComponent = () => { {setHeaderActionMenu && ( - - - - {settingsTabTitle} - - - - - - - - {ADD_DATA_LABEL} - - - + + + {settingsTabTitle} + + + + {ADD_DATA_LABEL} + + )} diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index 045fcb57ae943..d4845a4dd9e44 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import React, { useContext } from 'react'; import { Route, RouteComponentProps, Switch } from 'react-router-dom'; -import { EuiErrorBoundary, EuiFlexItem, EuiFlexGroup, EuiButtonEmpty } from '@elastic/eui'; +import { EuiErrorBoundary, EuiHeaderLinks, EuiHeaderLink } from '@elastic/eui'; import { IIndexPattern } from 'src/plugins/data/common'; import { MetricsSourceConfigurationProperties } from '../../../common/metrics_sources'; import { DocumentTitle } from '../../components/document_title'; @@ -86,31 +86,22 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => { {setHeaderActionMenu && ( - - - - {settingsTabTitle} - - - - - - - - - - - {ADD_DATA_LABEL} - - - + + + {settingsTabTitle} + + + + + {ADD_DATA_LABEL} + + )} diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout.tsx index d2cd4f87a5342..4e28fb4202bdc 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout.tsx @@ -6,7 +6,7 @@ */ import React, { useState, useCallback } from 'react'; -import { EuiButtonEmpty, EuiFlyout } from '@elastic/eui'; +import { EuiHeaderLink, EuiFlyout } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { FlyoutHome } from './flyout_home'; import { JobSetupScreen } from './job_setup_screen'; @@ -50,8 +50,7 @@ export const AnomalyDetectionFlyout = () => { return ( <> - { id="xpack.infra.ml.anomalyDetectionButton" defaultMessage="Anomaly detection" /> - + {showFlyout && ( ; -type LogThresholdAlertServices = AlertServices< - AlertInstanceState, - AlertInstanceContext, - LogThresholdActionGroups ->; -type LogThresholdAlertExecutorOptions = AlertExecutorOptions< - AlertTypeParams, - AlertTypeState, - AlertInstanceState, - AlertInstanceContext, +export type LogThresholdActionGroups = ActionGroupIdsOf; +export type LogThresholdAlertTypeParams = AlertParams; +export type LogThresholdAlertTypeState = AlertTypeState; // no specific state used +export type LogThresholdAlertInstanceState = AlertInstanceState; // no specific state used +export type LogThresholdAlertInstanceContext = AlertInstanceContext; // no specific instance context used + +type LogThresholdAlertInstance = AlertInstance< + LogThresholdAlertInstanceState, + LogThresholdAlertInstanceContext, LogThresholdActionGroups >; +type LogThresholdAlertInstanceFactory = ( + id: string, + threshold: number, + value: number +) => LogThresholdAlertInstance; const COMPOSITE_GROUP_SIZE = 2000; @@ -75,9 +80,26 @@ const checkValueAgainstComparatorMap: { // With forks for group_by vs ungrouped, and ratio vs non-ratio. export const createLogThresholdExecutor = (libs: InfraBackendLibs) => - async function ({ services, params }: LogThresholdAlertExecutorOptions) { - const { alertInstanceFactory, savedObjectsClient, scopedClusterClient } = services; + libs.logsRules.createLifecycleRuleExecutor< + LogThresholdAlertTypeParams, + LogThresholdAlertTypeState, + LogThresholdAlertInstanceState, + LogThresholdAlertInstanceContext, + LogThresholdActionGroups + >(async ({ services, params }) => { + const { alertWithLifecycle, savedObjectsClient, scopedClusterClient } = services; const { sources } = libs; + const alertInstanceFactory: LogThresholdAlertInstanceFactory = (id, threshold, value) => + alertWithLifecycle({ + id, + fields: { + [ALERT_EVALUATION_THRESHOLD]: threshold, + [ALERT_EVALUATION_VALUE]: value, + ...logThresholdRuleDataRT.encode({ + [logThresholdRuleDataSerializedParamsKey]: [params], + }), + }, + }); const sourceConfiguration = await sources.getSourceConfiguration(savedObjectsClient, 'default'); const { indices, timestampField, runtimeMappings } = await resolveLogSourceConfiguration( @@ -113,7 +135,7 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) => } catch (e) { throw new Error(e); } - }; + }); async function executeAlert( alertParams: CountAlertParams, @@ -121,7 +143,7 @@ async function executeAlert( indexPattern: string, runtimeMappings: estypes.MappingRuntimeFields, esClient: ElasticsearchClient, - alertInstanceFactory: LogThresholdAlertServices['alertInstanceFactory'] + alertInstanceFactory: LogThresholdAlertInstanceFactory ) { const query = getESQuery(alertParams, timestampField, indexPattern, runtimeMappings); @@ -152,7 +174,7 @@ async function executeRatioAlert( indexPattern: string, runtimeMappings: estypes.MappingRuntimeFields, esClient: ElasticsearchClient, - alertInstanceFactory: LogThresholdAlertServices['alertInstanceFactory'] + alertInstanceFactory: LogThresholdAlertInstanceFactory ) { // Ratio alert params are separated out into two standard sets of alert params const numeratorParams: AlertParams = { @@ -214,14 +236,14 @@ const getESQuery = ( export const processUngroupedResults = ( results: UngroupedSearchQueryResponse, params: CountAlertParams, - alertInstanceFactory: LogThresholdAlertExecutorOptions['services']['alertInstanceFactory'], + alertInstanceFactory: LogThresholdAlertInstanceFactory, alertInstaceUpdater: AlertInstanceUpdater ) => { const { count, criteria } = params; const documentCount = results.hits.total.value; if (checkValueAgainstComparatorMap[count.comparator](documentCount, count.value)) { - const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY); + const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY, count.value, documentCount); alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ { actionGroup: FIRED_ACTIONS.id, @@ -240,7 +262,7 @@ export const processUngroupedRatioResults = ( numeratorResults: UngroupedSearchQueryResponse, denominatorResults: UngroupedSearchQueryResponse, params: RatioAlertParams, - alertInstanceFactory: LogThresholdAlertExecutorOptions['services']['alertInstanceFactory'], + alertInstanceFactory: LogThresholdAlertInstanceFactory, alertInstaceUpdater: AlertInstanceUpdater ) => { const { count, criteria } = params; @@ -250,7 +272,7 @@ export const processUngroupedRatioResults = ( const ratio = getRatio(numeratorCount, denominatorCount); if (ratio !== undefined && checkValueAgainstComparatorMap[count.comparator](ratio, count.value)) { - const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY); + const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY, count.value, ratio); alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ { actionGroup: FIRED_ACTIONS.id, @@ -308,7 +330,7 @@ const getReducedGroupByResults = ( export const processGroupByResults = ( results: GroupedSearchQueryResponse['aggregations']['groups']['buckets'], params: CountAlertParams, - alertInstanceFactory: LogThresholdAlertExecutorOptions['services']['alertInstanceFactory'], + alertInstanceFactory: LogThresholdAlertInstanceFactory, alertInstaceUpdater: AlertInstanceUpdater ) => { const { count, criteria } = params; @@ -319,7 +341,7 @@ export const processGroupByResults = ( const documentCount = group.documentCount; if (checkValueAgainstComparatorMap[count.comparator](documentCount, count.value)) { - const alertInstance = alertInstanceFactory(group.name); + const alertInstance = alertInstanceFactory(group.name, count.value, documentCount); alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ { actionGroup: FIRED_ACTIONS.id, @@ -339,7 +361,7 @@ export const processGroupByRatioResults = ( numeratorResults: GroupedSearchQueryResponse['aggregations']['groups']['buckets'], denominatorResults: GroupedSearchQueryResponse['aggregations']['groups']['buckets'], params: RatioAlertParams, - alertInstanceFactory: LogThresholdAlertExecutorOptions['services']['alertInstanceFactory'], + alertInstanceFactory: LogThresholdAlertInstanceFactory, alertInstaceUpdater: AlertInstanceUpdater ) => { const { count, criteria } = params; @@ -360,7 +382,7 @@ export const processGroupByRatioResults = ( ratio !== undefined && checkValueAgainstComparatorMap[count.comparator](ratio, count.value) ) { - const alertInstance = alertInstanceFactory(numeratorGroup.name); + const alertInstance = alertInstanceFactory(numeratorGroup.name, count.value, ratio); alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ { actionGroup: FIRED_ACTIONS.id, diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts index 62d92d0487ff7..3d0bac3dd2bf5 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts @@ -6,14 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { - PluginSetupContract, - AlertTypeParams, - AlertTypeState, - AlertInstanceContext, - AlertInstanceState, - ActionGroupIdsOf, -} from '../../../../../alerting/server'; +import { PluginSetupContract } from '../../../../../alerting/server'; import { createLogThresholdExecutor, FIRED_ACTIONS } from './log_threshold_executor'; import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, @@ -88,13 +81,7 @@ export async function registerLogThresholdAlertType( ); } - alertingPlugin.registerType< - AlertTypeParams, - AlertTypeState, - AlertInstanceState, - AlertInstanceContext, - ActionGroupIdsOf - >({ + alertingPlugin.registerType({ id: LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, name: i18n.translate('xpack.infra.logs.alertName', { defaultMessage: 'Log threshold', diff --git a/x-pack/plugins/infra/server/lib/infra_types.ts b/x-pack/plugins/infra/server/lib/infra_types.ts index 0c57ff2e05847..332a2e499977b 100644 --- a/x-pack/plugins/infra/server/lib/infra_types.ts +++ b/x-pack/plugins/infra/server/lib/infra_types.ts @@ -5,15 +5,16 @@ * 2.0. */ +import { handleEsError } from '../../../../../src/plugins/es_ui_shared/server'; +import { InfraConfig } from '../plugin'; +import { GetLogQueryFields } from '../services/log_queries/get_log_query_fields'; +import { RulesServiceSetup } from '../services/rules'; +import { KibanaFramework } from './adapters/framework/kibana_framework_adapter'; import { InfraFieldsDomain } from './domains/fields_domain'; import { InfraLogEntriesDomain } from './domains/log_entries_domain'; import { InfraMetricsDomain } from './domains/metrics_domain'; import { InfraSources } from './sources'; import { InfraSourceStatus } from './source_status'; -import { InfraConfig } from '../plugin'; -import { KibanaFramework } from './adapters/framework/kibana_framework_adapter'; -import { GetLogQueryFields } from '../services/log_queries/get_log_query_fields'; -import { handleEsError } from '../../../../../src/plugins/es_ui_shared/server'; export interface InfraDomainLibs { fields: InfraFieldsDomain; @@ -28,4 +29,6 @@ export interface InfraBackendLibs extends InfraDomainLibs { sourceStatus: InfraSourceStatus; getLogQueryFields: GetLogQueryFields; handleEsError: typeof handleEsError; + logsRules: RulesServiceSetup; + metricsRules: RulesServiceSetup; } diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index 7c5666049bd60..de445affc178e 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -8,7 +8,9 @@ import { Server } from '@hapi/hapi'; import { schema, TypeOf } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; +import { Logger } from '@kbn/logging'; import { CoreSetup, PluginInitializerContext, Plugin } from 'src/core/server'; +import { LOGS_FEATURE_ID, METRICS_FEATURE_ID } from '../common/constants'; import { InfraStaticSourceConfiguration } from '../common/source_configuration/source_configuration'; import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view'; import { metricsExplorerViewSavedObjectType } from '../common/saved_objects/metrics_explorer_view'; @@ -32,6 +34,7 @@ import { InfraPluginRequestHandlerContext } from './types'; import { UsageCollector } from './usage/usage_collector'; import { createGetLogQueryFields } from './services/log_queries/get_log_query_fields'; import { handleEsError } from '../../../../src/plugins/es_ui_shared/server'; +import { RulesService } from './services/rules'; export const config = { schema: schema.object({ @@ -82,9 +85,25 @@ export interface InfraPluginSetup { export class InfraServerPlugin implements Plugin { public config: InfraConfig; public libs: InfraBackendLibs | undefined; + public logger: Logger; + + private logsRules: RulesService; + private metricsRules: RulesService; constructor(context: PluginInitializerContext) { this.config = context.config.get(); + this.logger = context.logger.get(); + + this.logsRules = new RulesService( + LOGS_FEATURE_ID, + 'observability.logs', + this.logger.get('logsRules') + ); + this.metricsRules = new RulesService( + METRICS_FEATURE_ID, + 'observability.metrics', + this.logger.get('metricsRules') + ); } setup(core: CoreSetup, plugins: InfraServerPluginSetupDeps) { @@ -126,6 +145,8 @@ export class InfraServerPlugin implements Plugin { ...domainLibs, getLogQueryFields: createGetLogQueryFields(sources, framework), handleEsError, + logsRules: this.logsRules.setup(core, plugins), + metricsRules: this.metricsRules.setup(core, plugins), }; plugins.features.registerKibanaFeature(METRICS_FEATURE); diff --git a/x-pack/plugins/infra/server/services/rules/index.ts b/x-pack/plugins/infra/server/services/rules/index.ts new file mode 100644 index 0000000000000..eaa3d0da493e5 --- /dev/null +++ b/x-pack/plugins/infra/server/services/rules/index.ts @@ -0,0 +1,9 @@ +/* + * 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 './rules_service'; +export * from './types'; diff --git a/x-pack/plugins/infra/server/services/rules/rule_data_client.ts b/x-pack/plugins/infra/server/services/rules/rule_data_client.ts new file mode 100644 index 0000000000000..d693be40f10d0 --- /dev/null +++ b/x-pack/plugins/infra/server/services/rules/rule_data_client.ts @@ -0,0 +1,87 @@ +/* + * 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 { once } from 'lodash'; +import { CoreSetup, Logger } from 'src/core/server'; +import { TECHNICAL_COMPONENT_TEMPLATE_NAME } from '../../../../rule_registry/common/assets'; +import { RuleRegistryPluginSetupContract } from '../../../../rule_registry/server'; +import { logThresholdRuleDataNamespace } from '../../../common/alerting/logs/log_threshold'; +import type { InfraFeatureId } from '../../../common/constants'; +import { RuleRegistrationContext, RulesServiceStartDeps } from './types'; + +export const createRuleDataClient = ({ + ownerFeatureId, + registrationContext, + getStartServices, + logger, + ruleDataService, +}: { + ownerFeatureId: InfraFeatureId; + registrationContext: RuleRegistrationContext; + getStartServices: CoreSetup['getStartServices']; + logger: Logger; + ruleDataService: RuleRegistryPluginSetupContract['ruleDataService']; +}) => { + const initializeRuleDataTemplates = once(async () => { + const componentTemplateName = ruleDataService.getFullAssetName( + `${registrationContext}-mappings` + ); + + const indexNamePattern = ruleDataService.getFullAssetName(`${registrationContext}*`); + + if (!ruleDataService.isWriteEnabled()) { + return; + } + + await ruleDataService.createOrUpdateComponentTemplate({ + name: componentTemplateName, + body: { + template: { + settings: { + number_of_shards: 1, + }, + mappings: { + properties: { + [logThresholdRuleDataNamespace]: { + properties: { + serialized_params: { + type: 'keyword', + index: false, + }, + }, + }, + }, + }, + }, + }, + }); + + await ruleDataService.createOrUpdateIndexTemplate({ + name: ruleDataService.getFullAssetName(registrationContext), + body: { + index_patterns: [indexNamePattern], + composed_of: [ + ruleDataService.getFullAssetName(TECHNICAL_COMPONENT_TEMPLATE_NAME), + componentTemplateName, + ], + }, + }); + + await ruleDataService.updateIndexMappingsMatchingPattern(indexNamePattern); + }); + + // initialize eagerly + const initializeRuleDataTemplatesPromise = initializeRuleDataTemplates().catch((err) => { + logger.error(err); + }); + + return ruleDataService.getRuleDataClient( + ownerFeatureId, + ruleDataService.getFullAssetName(registrationContext), + () => initializeRuleDataTemplatesPromise + ); +}; diff --git a/x-pack/plugins/infra/server/services/rules/rules_service.ts b/x-pack/plugins/infra/server/services/rules/rules_service.ts new file mode 100644 index 0000000000000..9341fc59d75b8 --- /dev/null +++ b/x-pack/plugins/infra/server/services/rules/rules_service.ts @@ -0,0 +1,50 @@ +/* + * 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 { CoreSetup, Logger } from 'src/core/server'; +import { createLifecycleExecutor } from '../../../../rule_registry/server'; +import { InfraFeatureId } from '../../../common/constants'; +import { createRuleDataClient } from './rule_data_client'; +import { + RuleRegistrationContext, + RulesServiceSetup, + RulesServiceSetupDeps, + RulesServiceStart, + RulesServiceStartDeps, +} from './types'; + +export class RulesService { + constructor( + public readonly ownerFeatureId: InfraFeatureId, + public readonly registrationContext: RuleRegistrationContext, + private readonly logger: Logger + ) {} + + public setup( + core: CoreSetup, + setupDeps: RulesServiceSetupDeps + ): RulesServiceSetup { + const ruleDataClient = createRuleDataClient({ + getStartServices: core.getStartServices, + logger: this.logger, + ownerFeatureId: this.ownerFeatureId, + registrationContext: this.registrationContext, + ruleDataService: setupDeps.ruleRegistry.ruleDataService, + }); + + const createLifecycleRuleExecutor = createLifecycleExecutor(this.logger, ruleDataClient); + + return { + createLifecycleRuleExecutor, + ruleDataClient, + }; + } + + public start(_startDeps: RulesServiceStartDeps): RulesServiceStart { + return {}; + } +} diff --git a/x-pack/plugins/infra/server/services/rules/types.ts b/x-pack/plugins/infra/server/services/rules/types.ts new file mode 100644 index 0000000000000..b67b79ee5d3c2 --- /dev/null +++ b/x-pack/plugins/infra/server/services/rules/types.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PluginSetupContract as AlertingPluginSetup } from '../../../../alerting/server'; +import { + createLifecycleExecutor, + RuleDataClient, + RuleRegistryPluginSetupContract, +} from '../../../../rule_registry/server'; + +type LifecycleRuleExecutorCreator = ReturnType; + +export interface RulesServiceSetupDeps { + alerting: AlertingPluginSetup; + ruleRegistry: RuleRegistryPluginSetupContract; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface RulesServiceStartDeps {} + +export interface RulesServiceSetup { + createLifecycleRuleExecutor: LifecycleRuleExecutorCreator; + ruleDataClient: RuleDataClient; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface RulesServiceStart {} + +export type RuleRegistrationContext = 'observability.logs' | 'observability.metrics'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/network_direction.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/network_direction.test.tsx new file mode 100644 index 0000000000000..7a4c55d6f5e02 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/network_direction.test.tsx @@ -0,0 +1,180 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { setup, SetupResult, getProcessorValue } from './processor.helpers'; + +// Default parameter values automatically added to the network direction processor when saved +const defaultNetworkDirectionParameters = { + if: undefined, + tag: undefined, + source_ip: undefined, + description: undefined, + target_field: undefined, + ignore_missing: undefined, + ignore_failure: undefined, + destination_ip: undefined, + internal_networks: undefined, + internal_networks_field: undefined, +}; + +const NETWORK_DIRECTION_TYPE = 'network_direction'; + +describe('Processor: Network Direction', () => { + let onUpdate: jest.Mock; + let testBed: SetupResult; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(async () => { + onUpdate = jest.fn(); + + await act(async () => { + testBed = await setup({ + value: { + processors: [], + }, + onFlyoutOpen: jest.fn(), + onUpdate, + }); + }); + + testBed.component.update(); + + // Open flyout to add new processor + testBed.actions.addProcessor(); + // Add type (the other fields are not visible until a type is selected) + await testBed.actions.addProcessorType(NETWORK_DIRECTION_TYPE); + }); + + test('prevents form submission if internal_network field is not provided', async () => { + const { + actions: { saveNewProcessor }, + form, + } = testBed; + + // Click submit button with only the type defined + await saveNewProcessor(); + + // Expect form error as "field" is required parameter + expect(form.getErrorsMessages()).toEqual(['A field value is required.']); + }); + + test('saves with default parameter values', async () => { + const { + actions: { saveNewProcessor }, + find, + component, + } = testBed; + + // Add "networkDirectionField" value (required) + await act(async () => { + find('networkDirectionField.input').simulate('change', [{ label: 'loopback' }]); + }); + component.update(); + + // Save the field + await saveNewProcessor(); + + const processors = getProcessorValue(onUpdate, NETWORK_DIRECTION_TYPE); + expect(processors[0][NETWORK_DIRECTION_TYPE]).toEqual({ + ...defaultNetworkDirectionParameters, + internal_networks: ['loopback'], + }); + }); + + test('allows to set internal_networks_field', async () => { + const { + actions: { saveNewProcessor }, + form, + find, + } = testBed; + + find('toggleCustomField').simulate('click'); + + form.setInputValue('networkDirectionField.input', 'internal_networks_field'); + + // Save the field with new changes + await saveNewProcessor(); + + const processors = getProcessorValue(onUpdate, NETWORK_DIRECTION_TYPE); + expect(processors[0][NETWORK_DIRECTION_TYPE]).toEqual({ + ...defaultNetworkDirectionParameters, + internal_networks_field: 'internal_networks_field', + }); + }); + + test('allows to set just internal_networks_field or internal_networks', async () => { + const { + actions: { saveNewProcessor }, + form, + find, + component, + } = testBed; + + // Set internal_networks field + await act(async () => { + find('networkDirectionField.input').simulate('change', [{ label: 'loopback' }]); + }); + component.update(); + + // Toggle to internal_networks_field and set a random value + find('toggleCustomField').simulate('click'); + form.setInputValue('networkDirectionField.input', 'internal_networks_field'); + + // Save the field with new changes + await saveNewProcessor(); + + const processors = getProcessorValue(onUpdate, NETWORK_DIRECTION_TYPE); + expect(processors[0][NETWORK_DIRECTION_TYPE]).toEqual({ + ...defaultNetworkDirectionParameters, + internal_networks_field: 'internal_networks_field', + }); + }); + + test('allows optional parameters to be set', async () => { + const { + actions: { saveNewProcessor }, + form, + find, + component, + } = testBed; + + // Add "networkDirectionField" value (required) + await act(async () => { + find('networkDirectionField.input').simulate('change', [{ label: 'loopback' }]); + }); + component.update(); + + // Set optional parameteres + form.toggleEuiSwitch('ignoreMissingSwitch.input'); + form.toggleEuiSwitch('ignoreFailureSwitch.input'); + form.setInputValue('sourceIpField.input', 'source.ip'); + form.setInputValue('targetField.input', 'target_field'); + form.setInputValue('destinationIpField.input', 'destination.ip'); + + // Save the field with new changes + await saveNewProcessor(); + + const processors = getProcessorValue(onUpdate, NETWORK_DIRECTION_TYPE); + expect(processors[0][NETWORK_DIRECTION_TYPE]).toEqual({ + ...defaultNetworkDirectionParameters, + ignore_failure: true, + ignore_missing: false, + source_ip: 'source.ip', + target_field: 'target_field', + destination_ip: 'destination.ip', + internal_networks: ['loopback'], + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx index 24e1ddce008ea..e4024e4ec67f4 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx @@ -171,6 +171,10 @@ type TestSubject = | 'regexFileField.input' | 'valueFieldInput' | 'mediaTypeSelectorField' + | 'networkDirectionField.input' + | 'sourceIpField.input' + | 'destinationIpField.input' + | 'toggleCustomField' | 'ignoreEmptyField.input' | 'overrideField.input' | 'fieldsValueField.input' diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/index.ts index 5e3e5f82478bd..f5eb1ab3ec59b 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/index.ts @@ -28,6 +28,7 @@ export { Join } from './join'; export { Json } from './json'; export { Kv } from './kv'; export { Lowercase } from './lowercase'; +export { NetworkDirection } from './network_direction'; export { Pipeline } from './pipeline'; export { RegisteredDomain } from './registered_domain'; export { Remove } from './remove'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/network_direction.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/network_direction.tsx new file mode 100644 index 0000000000000..2026a77bc6566 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/network_direction.tsx @@ -0,0 +1,242 @@ +/* + * 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, { FunctionComponent, useState, useCallback, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { isEmpty } from 'lodash'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiButtonEmpty, EuiCode } from '@elastic/eui'; + +import { + FIELD_TYPES, + UseField, + useFormContext, + Field, + FieldHook, + FieldConfig, + SerializerFunc, +} from '../../../../../../shared_imports'; +import { FieldsConfig, from, to } from './shared'; +import { TargetField } from './common_fields/target_field'; +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; + +interface InternalNetworkTypes { + internal_networks: string[]; + internal_networks_field: string; +} + +type InternalNetworkFields = { + [K in keyof InternalNetworkTypes]: FieldHook; +}; + +const internalNetworkValues: string[] = [ + 'loopback', + 'unicast', + 'global_unicast', + 'multicast', + 'interface_local_multicast', + 'link_local_unicast', + 'link_local_multicast', + 'link_local_multicast', + 'private', + 'public', + 'unspecified', +]; + +const fieldsConfig: FieldsConfig = { + /* Optional fields config */ + source_ip: { + type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.networkDirection.sourceIpLabel', { + defaultMessage: 'Source IP (optional)', + }), + helpText: ( + {'source.ip'}, + }} + /> + ), + }, + destination_ip: { + type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.networkDirection.destinationIpLabel', + { + defaultMessage: 'Destination IP (optional)', + } + ), + helpText: ( + {'destination.ip'}, + }} + /> + ), + }, +}; + +const getInternalNetworkConfig: ( + toggleCustom: () => void +) => Record< + keyof InternalNetworkFields, + { + path: string; + config?: FieldConfig; + euiFieldProps?: Record; + labelAppend: JSX.Element; + } +> = (toggleCustom: () => void) => ({ + internal_networks: { + path: 'fields.internal_networks', + euiFieldProps: { + noSuggestions: false, + options: internalNetworkValues.map((label) => ({ label })), + }, + config: { + type: FIELD_TYPES.COMBO_BOX, + deserializer: to.arrayOfStrings, + serializer: from.optionalArrayOfStrings, + fieldsToValidateOnChange: ['fields.internal_networks', 'fields.internal_networks_field'], + validations: [ + { + validator: ({ value, path, formData }) => { + if (isEmpty(value) && isEmpty(formData['fields.internal_networks_field'])) { + return { path, message: 'A field value is required.' }; + } + }, + }, + ], + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.networkDirection.internalNetworksLabel', + { + defaultMessage: 'Internal networks', + } + ), + helpText: ( + + ), + }, + labelAppend: ( + + {i18n.translate('xpack.ingestPipelines.pipelineEditor.internalNetworkCustomLabel', { + defaultMessage: 'Use custom field', + })} + + ), + key: 'preset', + }, + internal_networks_field: { + path: 'fields.internal_networks_field', + config: { + type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, + fieldsToValidateOnChange: ['fields.internal_networks', 'fields.internal_networks_field'], + validations: [ + { + validator: ({ value, path, formData }) => { + if (isEmpty(value) && isEmpty(formData['fields.internal_networks'])) { + return { path, message: 'A field value is required.' }; + } + }, + }, + ], + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.networkDirection.internalNetworksFieldLabel', + { + defaultMessage: 'Internal networks field', + } + ), + helpText: ( + {'internal_networks'}, + }} + /> + ), + }, + labelAppend: ( + + {i18n.translate('xpack.ingestPipelines.pipelineEditor.internalNetworkPredefinedLabel', { + defaultMessage: 'Use preset field', + })} + + ), + key: 'custom', + }, +}); + +export const NetworkDirection: FunctionComponent = () => { + const { getFieldDefaultValue } = useFormContext(); + const isInternalNetowrksFieldDefined = + getFieldDefaultValue('fields.internal_networks_field') !== undefined; + const [isCustom, setIsCustom] = useState(isInternalNetowrksFieldDefined); + + const toggleCustom = useCallback(() => { + setIsCustom((prev) => !prev); + }, []); + + const internalNetworkFieldProps = useMemo( + () => + isCustom + ? getInternalNetworkConfig(toggleCustom).internal_networks_field + : getInternalNetworkConfig(toggleCustom).internal_networks, + [isCustom, toggleCustom] + ); + + return ( + <> + + + + + {'network.direction'}, + }} + /> + } + /> + + + + } + /> + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx index 983fb0ea67bb0..e6ca465bf1a02 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx @@ -34,6 +34,7 @@ import { Json, Kv, Lowercase, + NetworkDirection, Pipeline, RegisteredDomain, Remove, @@ -517,6 +518,23 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { }, }), }, + network_direction: { + FieldsComponent: NetworkDirection, + docLinkPath: '/network-direction-processor.html', + label: i18n.translate('xpack.ingestPipelines.processors.label.networkDirection', { + defaultMessage: 'Network Direction', + }), + typeDescription: i18n.translate( + 'xpack.ingestPipelines.processors.description.networkDirection', + { + defaultMessage: 'Calculates the network direction given a source IP address.', + } + ), + getDefaultDescription: () => + i18n.translate('xpack.ingestPipelines.processors.defaultDescription.networkDirection', { + defaultMessage: 'Calculates the network direction given a source IP address.', + }), + }, pipeline: { FieldsComponent: Pipeline, docLinkPath: '/pipeline-processor.html', diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/context/processors_context.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/context/processors_context.tsx index 0c43297e811d3..ddf996de7805c 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/context/processors_context.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/context/processors_context.tsx @@ -151,7 +151,13 @@ export const PipelineProcessorsContextProvider: FunctionComponent = ({ break; case 'managingProcessor': // These are the option names we get back from our UI - const knownOptionNames = Object.keys(processorTypeAndOptions.options); + const knownOptionNames = [ + ...Object.keys(processorTypeAndOptions.options), + // We manually add fields that we **don't** want to be treated as "unknownOptions" + 'internal_networks', + 'internal_networks_field', + ]; + // The processor that we are updating may have options configured the UI does not know about const unknownOptions = omit(mode.arg.processor.options, knownOptionNames); // In order to keep the options we don't get back from our UI, we merge the known and unknown options diff --git a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts index 8ed57221a1395..29be11430bf64 100644 --- a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts +++ b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts @@ -43,6 +43,7 @@ export { ArrayItem, FormHook, useFormContext, + UseMultiFields, FormDataProvider, OnFormUpdateArg, FieldConfig, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx index 19d91c1006cf0..936f1e477057d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx @@ -35,7 +35,7 @@ const operationDefinitionMap: Record = { }), } as unknown) as GenericOperationDefinition, terms: { input: 'field' } as GenericOperationDefinition, - sum: { input: 'field' } as GenericOperationDefinition, + sum: { input: 'field', filterable: true } as GenericOperationDefinition, last_value: { input: 'field' } as GenericOperationDefinition, max: { input: 'field' } as GenericOperationDefinition, count: ({ @@ -928,6 +928,63 @@ invalid: " ).toEqual(['The operation average does not accept any parameter']); }); + it('returns an error if first argument type is passed multiple times', () => { + const formulas = [ + 'average(bytes, bytes)', + "sum(bytes, kql='category.keyword: *', bytes)", + 'moving_average(average(bytes), average(bytes))', + "moving_average(average(bytes), kql='category.keyword: *', average(bytes))", + 'moving_average(average(bytes, bytes), count())', + 'moving_average(moving_average(average(bytes, bytes), count(), count()))', + ]; + for (const formula of formulas) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual( + expect.arrayContaining([ + expect.stringMatching( + /The operation (moving_average|average|sum) in the Formula requires a single (field|metric), found:/ + ), + ]) + ); + } + }); + + it('returns an error if a function received an argument of the wrong argument type in any position', () => { + const formulas = [ + 'average(bytes, count())', + "sum(bytes, kql='category.keyword: *', count(), count())", + 'average(bytes, bytes + 1)', + 'average(count(), bytes)', + 'moving_average(average(bytes), bytes)', + 'moving_average(bytes, bytes)', + 'moving_average(average(bytes), window=7, bytes)', + 'moving_average(window=7, bytes)', + "moving_average(kql='category.keyword: *', bytes)", + ]; + for (const formula of formulas) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual( + expect.arrayContaining([ + expect.stringMatching( + /The operation (moving_average|average|sum) in the Formula does not support (metric|field) parameters, found:/ + ), + ]) + ); + } + }); + it('returns an error if the parameter passed to an operation is of the wrong type', () => { expect( formulaOperation.getErrorMessage!( @@ -1087,6 +1144,14 @@ invalid: " ) ).toEqual([`The first argument for ${fn} should be a field name. Found no field`]); } + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(`sum(kql='category.keyword: *')`), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual([`The first argument for sum should be a field name. Found category.keyword: *`]); }); it("returns a clear error when there's a missing function for a fullReference operation", () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts index 445df21a6067e..6d0a585db048f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts @@ -455,7 +455,7 @@ Example: Calculate area based on side length }, }; -export function isMathNode(node: TinymathAST) { +export function isMathNode(node: TinymathAST | string) { return isObject(node) && node.type === 'function' && tinymathFunctions[node.name]; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts index 5b7a9beaa4e32..d65ef5ada8b37 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts @@ -7,7 +7,7 @@ import { isObject, partition } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { parse, TinymathLocation } from '@kbn/tinymath'; +import { parse, TinymathLocation, TinymathVariable } from '@kbn/tinymath'; import type { TinymathAST, TinymathFunction, TinymathNamedArgument } from '@kbn/tinymath'; import { esKuery, esQuery } from '../../../../../../../../src/plugins/data/public'; import { @@ -63,6 +63,19 @@ interface ValidationErrors { message: string; type: {}; }; + tooManyFirstArguments: { + message: string; + type: { + operation: string; + type: string; + text: string; + supported?: number; + }; + }; + wrongArgument: { + message: string; + type: { operation: string; text: string; type: string }; + }; } type ErrorTypes = keyof ValidationErrors; @@ -276,6 +289,25 @@ function getMessageFromId({ defaultMessage: 'Use only one of kql= or lucene=, not both', }); break; + case 'tooManyFirstArguments': + message = i18n.translate('xpack.lens.indexPattern.formulaOperationTooManyFirstArguments', { + defaultMessage: + 'The operation {operation} in the Formula requires a {supported, plural, one {single} other {supported}} {type}, found: {text}', + values: { + operation: out.operation, + text: out.text, + type: out.type, + supported: out.supported || 1, + }, + }); + break; + case 'wrongArgument': + message = i18n.translate('xpack.lens.indexPattern.formulaOperationwrongArgument', { + defaultMessage: + 'The operation {operation} in the Formula does not support {type} parameters, found: {text}', + values: { operation: out.operation, text: out.text, type: out.type }, + }); + break; // case 'mathRequiresFunction': // message = i18n.translate('xpack.lens.indexPattern.formulaMathRequiresFunctionLabel', { // defaultMessage; 'The function {name} requires an Elasticsearch function', @@ -531,14 +563,16 @@ function runFullASTValidation( } else { if (nodeOperation.input === 'field') { if (shouldHaveFieldArgument(node)) { - if (!isFirstArgumentValidType(firstArg, 'variable')) { + if (!isArgumentValidType(firstArg, 'variable')) { if (isMathNode(firstArg)) { errors.push( getMessageFromId({ messageId: 'wrongFirstArgument', values: { operation: node.name, - type: 'field', + type: i18n.translate('xpack.lens.indexPattern.formulaFieldValue', { + defaultMessage: 'field', + }), argument: `math operation`, }, locations: node.location ? [node.location] : [], @@ -550,7 +584,9 @@ function runFullASTValidation( messageId: 'wrongFirstArgument', values: { operation: node.name, - type: 'field', + type: i18n.translate('xpack.lens.indexPattern.formulaFieldValue', { + defaultMessage: 'field', + }), argument: getValueOrName(firstArg) || i18n.translate('xpack.lens.indexPattern.formulaNoFieldForOperation', { @@ -561,6 +597,25 @@ function runFullASTValidation( }) ); } + } else { + // If the first argument is valid proceed with the other arguments validation + const fieldErrors = validateFieldArguments(node, variables, { + isFieldOperation: true, + firstArg, + }); + if (fieldErrors.length) { + errors.push(...fieldErrors); + } + } + const functionErrors = validateFunctionArguments(node, functions, 0, { + isFieldOperation: true, + type: i18n.translate('xpack.lens.indexPattern.formulaFieldValue', { + defaultMessage: 'field', + }), + firstArgValidation: false, + }); + if (functionErrors.length) { + errors.push(...functionErrors); } } else { // Named arguments only @@ -602,16 +657,20 @@ function runFullASTValidation( if (nodeOperation.input === 'fullReference') { // What about fn(7 + 1)? We may want to allow that // In general this should be handled down the Esaggs route rather than here - if ( - !isFirstArgumentValidType(firstArg, 'function') || - (isMathNode(firstArg) && validateMathNodes(firstArg, missingVariablesSet).length) - ) { + const isFirstArgumentNotValid = Boolean( + !isArgumentValidType(firstArg, 'function') || + (isMathNode(firstArg) && validateMathNodes(firstArg, missingVariablesSet).length) + ); + // First field has a special handling + if (isFirstArgumentNotValid) { errors.push( getMessageFromId({ messageId: 'wrongFirstArgument', values: { operation: node.name, - type: 'operation', + type: i18n.translate('xpack.lens.indexPattern.formulaOperationValue', { + defaultMessage: 'operation', + }), argument: getValueOrName(firstArg) || i18n.translate('xpack.lens.indexPattern.formulaNoOperation', { @@ -622,6 +681,21 @@ function runFullASTValidation( }) ); } + // Check for multiple function passed + const requiredFunctions = nodeOperation.requiredReferences + ? nodeOperation.requiredReferences.length + : 1; + const functionErrors = validateFunctionArguments(node, functions, requiredFunctions, { + isFieldOperation: false, + firstArgValidation: isFirstArgumentNotValid, + type: i18n.translate('xpack.lens.indexPattern.formulaMetricValue', { + defaultMessage: 'metric', + }), + }); + if (functionErrors.length) { + errors.push(...functionErrors); + } + if (!canHaveParams(nodeOperation) && namedArguments.length) { errors.push( getMessageFromId({ @@ -633,6 +707,14 @@ function runFullASTValidation( }) ); } else { + // check for fields passed at any position + const fieldErrors = validateFieldArguments(node, variables, { + isFieldOperation: false, + firstArg, + }); + if (fieldErrors.length) { + errors.push(...fieldErrors); + } const argumentsErrors = validateNameArguments( node, nodeOperation, @@ -736,7 +818,7 @@ export function hasFunctionFieldArgument(type: string) { return !['count'].includes(type); } -export function isFirstArgumentValidType(arg: TinymathAST, type: TinymathNodeTypes['type']) { +export function isArgumentValidType(arg: TinymathAST | string, type: TinymathNodeTypes['type']) { return isObject(arg) && arg.type === type; } @@ -812,3 +894,109 @@ export function validateMathNodes(root: TinymathAST, missingVariableSet: Set, + { isFieldOperation, firstArg }: { isFieldOperation: boolean; firstArg: TinymathAST } +) { + const fields = variables.filter( + (arg) => isArgumentValidType(arg, 'variable') && !isMathNode(arg) + ); + const errors = []; + if (isFieldOperation && (fields.length > 1 || (fields.length === 1 && fields[0] !== firstArg))) { + errors.push( + getMessageFromId({ + messageId: 'tooManyFirstArguments', + values: { + operation: node.name, + type: i18n.translate('xpack.lens.indexPattern.formulaFieldValue', { + defaultMessage: 'field', + }), + supported: 1, + text: (fields as TinymathVariable[]).map(({ text }) => text).join(', '), + }, + locations: node.location ? [node.location] : [], + }) + ); + } + if (!isFieldOperation && fields.length) { + errors.push( + getMessageFromId({ + messageId: 'wrongArgument', + values: { + operation: node.name, + text: (fields as TinymathVariable[]).map(({ text }) => text).join(', '), + type: i18n.translate('xpack.lens.indexPattern.formulaFieldValue', { + defaultMessage: 'field', + }), + }, + locations: node.location ? [node.location] : [], + }) + ); + } + return errors; +} + +function validateFunctionArguments( + node: TinymathFunction, + functions: TinymathFunction[], + requiredFunctions: number = 0, + { + isFieldOperation, + firstArgValidation, + type, + }: { isFieldOperation: boolean; firstArgValidation: boolean; type: string } +) { + const errors = []; + // For math operation let the native operation run its own validation + const [esOperations, mathOperations] = partition(functions, (arg) => !isMathNode(arg)); + if (esOperations.length > requiredFunctions) { + if (isFieldOperation) { + errors.push( + getMessageFromId({ + messageId: 'wrongArgument', + values: { + operation: node.name, + text: (esOperations as TinymathFunction[]).map(({ text }) => text).join(', '), + type: i18n.translate('xpack.lens.indexPattern.formulaMetricValue', { + defaultMessage: 'metric', + }), + }, + locations: node.location ? [node.location] : [], + }) + ); + } else { + errors.push( + getMessageFromId({ + messageId: 'tooManyFirstArguments', + values: { + operation: node.name, + type, + supported: requiredFunctions, + text: (esOperations as TinymathFunction[]).map(({ text }) => text).join(', '), + }, + locations: node.location ? [node.location] : [], + }) + ); + } + } + // full reference operation have another way to handle math operations + if ( + isFieldOperation && + ((!firstArgValidation && mathOperations.length) || mathOperations.length > 1) + ) { + errors.push( + getMessageFromId({ + messageId: 'wrongArgument', + values: { + operation: node.name, + type, + text: (mathOperations as TinymathFunction[]).map(({ text }) => text).join(', '), + }, + locations: node.location ? [node.location] : [], + }) + ); + } + return errors; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx index a1bc643c3bd93..8cfd25914f59c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx @@ -81,12 +81,6 @@ export const timeShiftOptions = [ }), value: '1y', }, - { - label: i18n.translate('xpack.lens.indexPattern.timeShift.previous', { - defaultMessage: 'Previous time range', - }), - value: 'previous', - }, ]; export const timeShiftOptionOrder = timeShiftOptions.reduce<{ [key: string]: number }>( diff --git a/x-pack/plugins/lens/server/routes/field_stats.ts b/x-pack/plugins/lens/server/routes/field_stats.ts index 12d3ef3f4a95e..7103e395eabdc 100644 --- a/x-pack/plugins/lens/server/routes/field_stats.ts +++ b/x-pack/plugins/lens/server/routes/field_stats.ts @@ -8,7 +8,7 @@ import { errors, estypes } from '@elastic/elasticsearch'; import DateMath from '@elastic/datemath'; import { schema } from '@kbn/config-schema'; import { CoreSetup } from 'src/core/server'; -import { IFieldType } from 'src/plugins/data/common'; +import type { IndexPatternField } from 'src/plugins/data/common'; import { SavedObjectNotFound } from '../../../../../src/plugins/kibana_utils/common'; import { ESSearchResponse } from '../../../../../src/core/types/elasticsearch'; import { FieldStatsResponse, BASE_API_URL } from '../../common'; @@ -79,6 +79,14 @@ export async function initFieldsRoute(setup: CoreSetup) { }, }; + const runtimeMappings = indexPattern.fields + .filter((f) => f.runtimeField) + .reduce((acc, f) => { + if (!f.runtimeField) return acc; + acc[f.name] = f.runtimeField; + return acc; + }, {} as Record); + const search = async (aggs: Record) => { const { body: result } = await requestClient.search({ index: indexPattern.title, @@ -86,7 +94,7 @@ export async function initFieldsRoute(setup: CoreSetup) { body: { query, aggs, - runtime_mappings: field.runtimeField ? { [fieldName]: field.runtimeField } : {}, + runtime_mappings: runtimeMappings, }, size: 0, }); @@ -138,7 +146,7 @@ export async function getNumberHistogram( aggSearchWithBody: ( aggs: Record ) => Promise, - field: IFieldType, + field: IndexPatternField, useTopHits = true ): Promise { const fieldRef = getFieldRef(field); @@ -247,7 +255,7 @@ export async function getNumberHistogram( export async function getStringSamples( aggSearchWithBody: (aggs: Record) => unknown, - field: IFieldType, + field: IndexPatternField, size = 10 ): Promise { const fieldRef = getFieldRef(field); @@ -287,7 +295,7 @@ export async function getStringSamples( // This one is not sampled so that it returns the full date range export async function getDateHistogram( aggSearchWithBody: (aggs: Record) => unknown, - field: IFieldType, + field: IndexPatternField, range: { fromDate: string; toDate: string } ): Promise { const fromDate = DateMath.parse(range.fromDate); @@ -329,7 +337,7 @@ export async function getDateHistogram( }; } -function getFieldRef(field: IFieldType) { +function getFieldRef(field: IndexPatternField) { return field.scripted ? { script: { diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx index 82347f6212442..e5a5e76f8cc5d 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx @@ -164,7 +164,6 @@ export const AutocompleteFieldMatchAnyComponent: React.FC diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.ts b/x-pack/plugins/lists/server/services/items/create_list_item.ts index b4203f000b7b9..ccdb8ab4779b6 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_item.ts @@ -15,9 +15,9 @@ import { SerializerOrUndefined, Type, } from '@kbn/securitysolution-io-ts-list-types'; +import { encodeHitVersion } from '@kbn/securitysolution-es-utils'; import { transformListItemToElasticQuery } from '../utils'; -import { encodeHitVersion } from '../utils/encode_hit_version'; import { IndexEsListItemSchema } from '../../schemas/elastic_query'; export interface CreateListItemOptions { diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.ts b/x-pack/plugins/lists/server/services/items/update_list_item.ts index c73149019f416..78651bb83d73b 100644 --- a/x-pack/plugins/lists/server/services/items/update_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/update_list_item.ts @@ -12,10 +12,9 @@ import type { MetaOrUndefined, _VersionOrUndefined, } from '@kbn/securitysolution-io-ts-list-types'; +import { decodeVersion, encodeHitVersion } from '@kbn/securitysolution-es-utils'; import { transformListItemToElasticQuery } from '../utils'; -import { decodeVersion } from '../utils/decode_version'; -import { encodeHitVersion } from '../utils/encode_hit_version'; import { UpdateEsListItemSchema } from '../../schemas/elastic_query'; import { getListItem } from './get_list_item'; diff --git a/x-pack/plugins/lists/server/services/lists/create_list.ts b/x-pack/plugins/lists/server/services/lists/create_list.ts index 6c7081d7c701e..521a38a51d6eb 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list.ts @@ -19,8 +19,8 @@ import type { Type, } from '@kbn/securitysolution-io-ts-list-types'; import type { Version } from '@kbn/securitysolution-io-ts-types'; +import { encodeHitVersion } from '@kbn/securitysolution-es-utils'; -import { encodeHitVersion } from '../utils/encode_hit_version'; import { IndexEsListSchema } from '../../schemas/elastic_query'; export interface CreateListOptions { diff --git a/x-pack/plugins/lists/server/services/lists/update_list.ts b/x-pack/plugins/lists/server/services/lists/update_list.ts index 22235341ca075..11868a6187bbf 100644 --- a/x-pack/plugins/lists/server/services/lists/update_list.ts +++ b/x-pack/plugins/lists/server/services/lists/update_list.ts @@ -15,9 +15,8 @@ import type { _VersionOrUndefined, } from '@kbn/securitysolution-io-ts-list-types'; import { VersionOrUndefined } from '@kbn/securitysolution-io-ts-types'; +import { decodeVersion, encodeHitVersion } from '@kbn/securitysolution-es-utils'; -import { decodeVersion } from '../utils/decode_version'; -import { encodeHitVersion } from '../utils/encode_hit_version'; import { UpdateEsListSchema } from '../../schemas/elastic_query'; import { getList } from '.'; diff --git a/x-pack/plugins/lists/server/services/utils/index.ts b/x-pack/plugins/lists/server/services/utils/index.ts index 0cd2720bd199b..64e7c50d0e7b0 100644 --- a/x-pack/plugins/lists/server/services/utils/index.ts +++ b/x-pack/plugins/lists/server/services/utils/index.ts @@ -6,9 +6,7 @@ */ export * from './calculate_scroll_math'; -export * from './decode_version'; export * from './encode_decode_cursor'; -export * from './encode_hit_version'; export * from './escape_query'; export * from './find_source_type'; export * from './find_source_value'; diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list.ts index 19177c1c2785f..5b0949d7b79b7 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list.ts @@ -7,11 +7,10 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ListArraySchema } from '@kbn/securitysolution-io-ts-list-types'; +import { encodeHitVersion } from '@kbn/securitysolution-es-utils'; import { SearchEsListSchema } from '../../schemas/elastic_response'; -import { encodeHitVersion } from './encode_hit_version'; - export interface TransformElasticToListOptions { response: estypes.SearchResponse; } diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts index 79db56f9a7fe9..65392f8c379d9 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts @@ -7,11 +7,11 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ListItemArraySchema, Type } from '@kbn/securitysolution-io-ts-list-types'; +import { encodeHitVersion } from '@kbn/securitysolution-es-utils'; import { ErrorWithStatusCode } from '../../error_with_status_code'; import { SearchEsListItemSchema } from '../../schemas/elastic_response'; -import { encodeHitVersion } from './encode_hit_version'; import { findSourceValue } from './find_source_value'; export interface TransformElasticToListItemOptions { diff --git a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx index 024c2308df6c6..87747d915af4a 100644 --- a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx +++ b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx @@ -106,7 +106,7 @@ export class ClientFileCreateSourceEditor extends Component { sourceDataRequest ); expect(areResultsTrimmed).toBe(true); - expect(tooltipContent).toBe('Results limited to first 1000 tracks of ~5000.'); + expect(tooltipContent).toBe('Results limited to first 1,000 tracks of ~5,000.'); }); it('Should show results trimmed icon and message when tracks are trimmed', () => { @@ -90,7 +90,7 @@ describe('getSourceTooltipContent', () => { ); expect(areResultsTrimmed).toBe(true); expect(tooltipContent).toBe( - 'Results limited to first 1000 tracks of ~5000. 10 of 1000 tracks are incomplete.' + 'Results limited to first 1,000 tracks of ~5,000. 10 of 1,000 tracks are incomplete.' ); }); }); diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx index 460c1228e50a8..82be83dad43f7 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx @@ -326,21 +326,21 @@ export class ESGeoLineSource extends AbstractESAggSource { ? i18n.translate('xpack.maps.esGeoLine.areEntitiesTrimmedMsg', { defaultMessage: `Results limited to first {entityCount} tracks of ~{totalEntities}.`, values: { - entityCount: meta.entityCount, - totalEntities: meta.totalEntities, + entityCount: meta.entityCount.toLocaleString(), + totalEntities: meta.totalEntities.toLocaleString(), }, }) : i18n.translate('xpack.maps.esGeoLine.tracksCountMsg', { defaultMessage: `Found {entityCount} tracks.`, - values: { entityCount: meta.entityCount }, + values: { entityCount: meta.entityCount.toLocaleString() }, }); const tracksTrimmedMsg = meta.numTrimmedTracks > 0 ? i18n.translate('xpack.maps.esGeoLine.tracksTrimmedMsg', { defaultMessage: `{numTrimmedTracks} of {entityCount} tracks are incomplete.`, values: { - entityCount: meta.entityCount, - numTrimmedTracks: meta.numTrimmedTracks, + entityCount: meta.entityCount.toLocaleString(), + numTrimmedTracks: meta.numTrimmedTracks.toLocaleString(), }, }) : undefined; diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index 343c366b548f6..55eed588b8840 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -620,17 +620,17 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye ? i18n.translate('xpack.maps.esSearch.topHitsResultsTrimmedMsg', { defaultMessage: `Results limited to first {entityCount} entities of ~{totalEntities}.`, values: { - entityCount: meta.entityCount, - totalEntities: meta.totalEntities, + entityCount: meta.entityCount?.toLocaleString(), + totalEntities: meta.totalEntities?.toLocaleString(), }, }) : i18n.translate('xpack.maps.esSearch.topHitsEntitiesCountMsg', { defaultMessage: `Found {entityCount} entities.`, - values: { entityCount: meta.entityCount }, + values: { entityCount: meta.entityCount?.toLocaleString() }, }); const docsPerEntityMsg = i18n.translate('xpack.maps.esSearch.topHitsSizeMsg', { defaultMessage: `Showing top {topHitsSize} documents per entity.`, - values: { topHitsSize: this._descriptor.topHitsSize }, + values: { topHitsSize: this._descriptor.topHitsSize?.toLocaleString() }, }); return { @@ -645,7 +645,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye return { tooltipContent: i18n.translate('xpack.maps.esSearch.resultsTrimmedMsg', { defaultMessage: `Results limited to first {count} documents.`, - values: { count: meta.resultsCount }, + values: { count: meta.resultsCount?.toLocaleString() }, }), areResultsTrimmed: true, }; @@ -654,7 +654,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye return { tooltipContent: i18n.translate('xpack.maps.esSearch.featureCountMsg', { defaultMessage: `Found {count} documents.`, - values: { count: meta.resultsCount }, + values: { count: meta.resultsCount?.toLocaleString() }, }), areResultsTrimmed: false, }; diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/util/__snapshots__/scaling_form.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/__snapshots__/scaling_form.test.tsx.snap index 03f2594f287ea..99ce13ce326d6 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/util/__snapshots__/scaling_form.test.tsx.snap +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/__snapshots__/scaling_form.test.tsx.snap @@ -28,7 +28,7 @@ exports[`scaling form should disable clusters option when clustering is not supp @@ -114,14 +114,14 @@ exports[`scaling form should render 1`] = ` { state = { - maxResultWindow: DEFAULT_MAX_RESULT_WINDOW, + maxResultWindow: DEFAULT_MAX_RESULT_WINDOW.toLocaleString(), }; _isMounted = false; @@ -61,7 +61,7 @@ export class ScalingForm extends Component { const indexPattern = await getIndexPatternService().get(this.props.indexPatternId); const { maxResultWindow } = await loadIndexSettings(indexPattern!.title); if (this._isMounted) { - this.setState({ maxResultWindow }); + this.setState({ maxResultWindow: maxResultWindow.toLocaleString() }); } } catch (err) { return; @@ -90,7 +90,7 @@ export class ScalingForm extends Component { { , + "name": "Edit layer settings", + "onClick": [Function], + "toolTipContent": null, + }, ], "title": "Layer actions", }, diff --git a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx index 83b4d2c2a756b..2a3186f00d7ce 100644 --- a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx +++ b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx @@ -158,6 +158,17 @@ export class TOCEntryActionsPopover extends Component { }, }, ]; + actionItems.push({ + disabled: this.props.isEditButtonDisabled, + name: EDIT_LAYER_SETTINGS_LABEL, + icon: , + 'data-test-subj': 'layerSettingsButton', + toolTipContent: null, + onClick: () => { + this._closePopover(); + this.props.openLayerSettings(); + }, + }); if (!this.props.isReadOnly) { if (this.state.supportsFeatureEditing) { @@ -186,17 +197,6 @@ export class TOCEntryActionsPopover extends Component { }, }); } - actionItems.push({ - disabled: this.props.isEditButtonDisabled, - name: EDIT_LAYER_SETTINGS_LABEL, - icon: , - 'data-test-subj': 'layerSettingsButton', - toolTipContent: null, - onClick: () => { - this._closePopover(); - this.props.openLayerSettings(); - }, - }); actionItems.push({ name: i18n.translate('xpack.maps.layerTocActions.cloneLayerTitle', { defaultMessage: 'Clone layer', diff --git a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_button/toc_entry_button.tsx b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_button/toc_entry_button.tsx index 41c2992c77d88..ffad34454bb61 100644 --- a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_button/toc_entry_button.tsx +++ b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_button/toc_entry_button.tsx @@ -116,7 +116,7 @@ export class TOCEntryButton extends Component { footnotes.push({ icon: , message: i18n.translate('xpack.maps.layer.isUsingSearchMsg', { - defaultMessage: 'Results narrowed by search bar', + defaultMessage: 'Results narrowed by query and filters', }), }); } diff --git a/x-pack/plugins/ml/common/constants/messages.test.ts b/x-pack/plugins/ml/common/constants/messages.test.ts index 1141eea2c176d..59fc50757b674 100644 --- a/x-pack/plugins/ml/common/constants/messages.test.ts +++ b/x-pack/plugins/ml/common/constants/messages.test.ts @@ -35,7 +35,7 @@ describe('Constants: Messages parseMessages()', () => { status: 'success', text: 'Presence of detector functions validated in all detectors.', url: - 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/create-jobs.html#detectors', + 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-ad-finding-anomalies.html#ml-ad-detectors', }, { bucketSpan: '15m', @@ -44,7 +44,7 @@ describe('Constants: Messages parseMessages()', () => { status: 'success', text: 'Format of "15m" is valid and passed validation checks.', url: - 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/create-jobs.html#bucket-span', + 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-ad-finding-anomalies.html#ml-ad-bucket-span', }, { heading: 'Time range', @@ -58,7 +58,7 @@ describe('Constants: Messages parseMessages()', () => { status: 'success', text: 'Valid and within the estimated model memory limit.', url: - 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/create-jobs.html#model-memory-limits', + 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-ad-finding-anomalies.html#ml-ad-model-memory-limits', }, ]); }); @@ -79,7 +79,7 @@ describe('Constants: Messages parseMessages()', () => { status: 'success', text: 'Presence of detector functions validated in all detectors.', url: - 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/create-jobs.html#detectors', + 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-ad-finding-anomalies.html#ml-ad-detectors', }, { bucketSpan: '15m', @@ -116,7 +116,7 @@ describe('Constants: Messages parseMessages()', () => { status: 'success', text: 'Presence of detector functions validated in all detectors.', url: - 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/create-jobs.html#detectors', + 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-ad-finding-anomalies.html#ml-ad-detectors', }, { id: 'cardinality_model_plot_high', @@ -131,7 +131,7 @@ describe('Constants: Messages parseMessages()', () => { text: 'Cardinality of partition_field "order_id" is above 1000 and might result in high memory usage.', url: - 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/create-jobs.html#cardinality', + 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-ad-finding-anomalies.html#ml-ad-cardinality', }, { heading: 'Bucket span', @@ -140,7 +140,7 @@ describe('Constants: Messages parseMessages()', () => { text: 'Bucket span is 1 day or more. Be aware that days are considered as UTC days, not local days.', url: - 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/create-jobs.html#bucket-span', + 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-ad-finding-anomalies.html#ml-ad-bucket-span', }, { bucketSpanCompareFactor: 25, @@ -156,7 +156,7 @@ describe('Constants: Messages parseMessages()', () => { status: 'success', text: 'Influencer configuration passed the validation checks.', url: - 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-influencers.html', + 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-ad-finding-anomalies.html#ml-ad-influencers', }, { id: 'half_estimated_mml_greater_than_mml', @@ -165,7 +165,7 @@ describe('Constants: Messages parseMessages()', () => { text: 'The specified model memory limit is less than half of the estimated model memory limit and will likely hit the hard limit.', url: - 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/create-jobs.html#model-memory-limits', + 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-ad-finding-anomalies.html#ml-ad-model-memory-limits', }, { id: 'missing_summary_count_field_name', diff --git a/x-pack/plugins/ml/common/types/anomalies.ts b/x-pack/plugins/ml/common/types/anomalies.ts index e84035aa50c8f..2bf717067f712 100644 --- a/x-pack/plugins/ml/common/types/anomalies.ts +++ b/x-pack/plugins/ml/common/types/anomalies.ts @@ -12,6 +12,8 @@ export interface Influencer { influencer_field_values: string[]; } +export type MLAnomalyDoc = AnomalyRecordDoc; + export interface AnomalyRecordDoc { [key: string]: any; job_id: string; diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index 7d461c4ec8572..39d4dd1a71dd9 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -18,13 +18,13 @@ import React, { Component, Fragment, useContext } from 'react'; import memoizeOne from 'memoize-one'; import { EuiBadge, - EuiButtonEmpty, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, EuiLink, EuiLoadingSpinner, + EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -52,6 +52,19 @@ import { timeFormatter } from '../../../../../common/util/date_utils'; import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context'; import { DatafeedChartFlyout } from '../../../jobs/jobs_list/components/datafeed_chart_flyout'; +const editAnnotationsText = ( + +); +const viewDataFeedText = ( + +); + const CURRENT_SERIES = 'current_series'; /** * Table component for rendering the lists of annotations for an ML job. @@ -128,6 +141,12 @@ class AnnotationsTableUI extends Component { jobId: undefined, }); }); + } else { + this.setState({ + annotations: [], + isLoading: false, + jobId: undefined, + }); } } @@ -457,82 +476,67 @@ class AnnotationsTableUI extends Component { const actions = []; actions.push({ - render: (annotation) => { - // find the original annotation because the table might not show everything + name: editAnnotationsText, + description: editAnnotationsText, + icon: 'pencil', + type: 'icon', + onClick: (annotation) => { const annotationId = annotation._id; const originalAnnotation = annotations.find((d) => d._id === annotationId); - const editAnnotationsText = ( - - ); - const editAnnotationsAriaLabelText = i18n.translate( - 'xpack.ml.annotationsTable.editAnnotationsTooltipAriaLabel', - { defaultMessage: 'Edit annotation' } - ); - return ( - annotationUpdatesService.setValue(originalAnnotation ?? annotation)} - > - {editAnnotationsText} - - ); + + annotationUpdatesService.setValue(originalAnnotation ?? annotation); }, }); if (this.state.jobId && this.props.jobs[0].analysis_config.bucket_span) { // add datafeed modal action actions.push({ - render: (annotation) => { - const viewDataFeedText = ( - - ); - const viewDataFeedTooltipAriaLabelText = i18n.translate( - 'xpack.ml.annotationsTable.datafeedChartTooltipAriaLabel', - { defaultMessage: 'Datafeed chart' } - ); - return ( - - this.setState({ - datafeedFlyoutVisible: true, - datafeedEnd: annotation.end_timestamp, - }) - } - > - {viewDataFeedText} - - ); + name: viewDataFeedText, + description: viewDataFeedText, + icon: 'visAreaStacked', + type: 'icon', + onClick: (annotation) => { + this.setState({ + datafeedFlyoutVisible: true, + datafeedEnd: annotation.end_timestamp, + }); }, }); } if (isSingleMetricViewerLinkVisible) { actions.push({ - render: (annotation) => { + name: (annotation) => { const isDrillDownAvailable = isTimeSeriesViewJob(this.getJob(annotation.job_id)); - const openInSingleMetricViewerTooltipText = isDrillDownAvailable ? ( - - ) : ( - + + if (isDrillDownAvailable) { + return ( + + ); + } + return ( + + } + > + + ); - const openInSingleMetricViewerAriaLabelText = isDrillDownAvailable + }, + description: (annotation) => { + const isDrillDownAvailable = isTimeSeriesViewJob(this.getJob(annotation.job_id)); + + return isDrillDownAvailable ? i18n.translate('xpack.ml.annotationsTable.openInSingleMetricViewerAriaLabel', { defaultMessage: 'Open in Single Metric Viewer', }) @@ -540,19 +544,11 @@ class AnnotationsTableUI extends Component { 'xpack.ml.annotationsTable.jobConfigurationNotSupportedInSingleMetricViewerAriaLabel', { defaultMessage: 'Job configuration not supported in Single Metric Viewer' } ); - - return ( - this.openSingleMetricView(annotation)} - > - {openInSingleMetricViewerTooltipText} - - ); }, + enabled: (annotation) => isTimeSeriesViewJob(this.getJob(annotation.job_id)), + icon: 'visLine', + type: 'icon', + onClick: (annotation) => this.openSingleMetricView(annotation), }); } diff --git a/x-pack/plugins/ml/public/application/components/help_popover/help_popover.tsx b/x-pack/plugins/ml/public/application/components/help_popover/help_popover.tsx index 8cd6a3fbd1138..95c66d58dbb75 100644 --- a/x-pack/plugins/ml/public/application/components/help_popover/help_popover.tsx +++ b/x-pack/plugins/ml/public/application/components/help_popover/help_popover.tsx @@ -6,6 +6,7 @@ */ import React, { ReactNode } from 'react'; +import { i18n } from '@kbn/i18n'; import { EuiButtonIcon, EuiLinkButtonProps, @@ -22,6 +23,9 @@ export const HelpPopoverButton = ({ onClick }: { onClick: EuiLinkButtonProps['on className="mlHelpPopover__buttonIcon" size="s" iconType="help" + aria-label={i18n.translate('xpack.ml.helpPopover.ariaLabel', { + defaultMessage: 'Help', + })} onClick={onClick} /> ); diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx index 6dd4e6c14589b..f282b2fde2b3a 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx @@ -42,6 +42,7 @@ import { Anomaly } from '../../../jobs/new_job/common/results_loader/results_loa import { parseInterval } from '../../../../../common/util/parse_interval'; import { CreateCalendar, CalendarEvent } from './create_calendar'; import { timeFormatter } from '../../../../../common/util/date_utils'; +import { toastNotificationServiceProvider } from '../../../services/toast_notification_service'; interface Props { snapshot: ModelSnapshot; @@ -139,6 +140,10 @@ export const RevertModelSnapshotFlyout: FC = ({ }) ); refresh(); + }) + .catch((error) => { + const { displayErrorToast } = toastNotificationServiceProvider(toasts); + displayErrorToast(error); }); hideRevertModal(); closeFlyout(); diff --git a/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts b/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts index 621ce44204730..45afab0cce4fd 100644 --- a/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts +++ b/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts @@ -223,7 +223,9 @@ const loadExplorerDataProvider = ( swimlaneLimit, viewByPerPage, viewByFromPage, - swimlaneContainerWidth + swimlaneContainerWidth, + selectionInfluencers, + influencersFilterQuery ) : Promise.resolve([]), }).pipe( diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_query_bar/explorer_query_bar.tsx b/x-pack/plugins/ml/public/application/explorer/components/explorer_query_bar/explorer_query_bar.tsx index 6f5ae5e17590a..57e051e1b8417 100644 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_query_bar/explorer_query_bar.tsx +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_query_bar/explorer_query_bar.tsx @@ -95,7 +95,6 @@ function getInitSearchInputState({ interface ExplorerQueryBarProps { filterActive: boolean; - filterIconTriggeredQuery: string; filterPlaceHolder: string; indexPattern: IIndexPattern; queryString?: string; @@ -104,7 +103,6 @@ interface ExplorerQueryBarProps { export const ExplorerQueryBar: FC = ({ filterActive, - filterIconTriggeredQuery, filterPlaceHolder, indexPattern, queryString, @@ -116,14 +114,12 @@ export const ExplorerQueryBar: FC = ({ ); const [errorMessage, setErrorMessage] = useState(undefined); - useEffect(() => { - if (filterIconTriggeredQuery !== undefined) { - setSearchInput({ - language: searchInput.language, - query: filterIconTriggeredQuery, - }); - } - }, [filterIconTriggeredQuery]); + useEffect( + function updateSearchInputFromFilter() { + setSearchInput(getInitSearchInputState({ filterActive, queryString })); + }, + [filterActive, queryString] + ); const searchChangeHandler = (query: Query) => { if (searchInput.language !== query.language) { @@ -131,6 +127,7 @@ export const ExplorerQueryBar: FC = ({ } setSearchInput(query); }; + const applyInfluencersFilterQuery = (query: Query) => { try { const { clearSettings, settings } = getKqlQueryValues({ diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index a11a42b9b65b2..81474c212d265 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer.js @@ -86,7 +86,6 @@ const ExplorerPage = ({ filterPlaceHolder, indexPattern, queryString, - filterIconTriggeredQuery, updateLanguage, }) => (
@@ -121,7 +120,6 @@ const ExplorerPage = ({ filterPlaceHolder={filterPlaceHolder} indexPattern={indexPattern} queryString={queryString} - filterIconTriggeredQuery={filterIconTriggeredQuery} updateLanguage={updateLanguage} />
@@ -151,7 +149,7 @@ export class ExplorerUI extends React.Component { selectedJobsRunning: PropTypes.bool.isRequired, }; - state = { filterIconTriggeredQuery: undefined, language: DEFAULT_QUERY_LANG }; + state = { language: DEFAULT_QUERY_LANG }; htmlIdGen = htmlIdGenerator(); componentDidMount() { @@ -200,8 +198,6 @@ export class ExplorerUI extends React.Component { } } - this.setState({ filterIconTriggeredQuery: `${newQueryString}` }); - try { const { clearSettings, settings } = getKqlQueryValues({ inputString: `${newQueryString}`, @@ -261,6 +257,30 @@ export class ExplorerUI extends React.Component { } = this.props.explorerState; const { annotationsData, aggregations, error: annotationsError } = annotations; + const annotationsCnt = Array.isArray(annotationsData) ? annotationsData.length : 0; + const allAnnotationsCnt = Array.isArray(aggregations?.event?.buckets) + ? aggregations.event.buckets.reduce((acc, v) => acc + v.doc_count, 0) + : annotationsCnt; + + const badge = + allAnnotationsCnt > annotationsCnt ? ( + + + + ) : ( + + + + ); + const jobSelectorProps = { dateFormatTz: getDateFormatTz(), }; @@ -303,7 +323,6 @@ export class ExplorerUI extends React.Component { influencers={influencers} filterActive={filterActive} filterPlaceHolder={filterPlaceHolder} - filterIconTriggeredQuery={this.state.filterIconTriggeredQuery} indexPattern={indexPattern} queryString={queryString} updateLanguage={this.updateLanguage} @@ -404,7 +423,7 @@ export class ExplorerUI extends React.Component { {loading === false && tableData.anomalies?.length ? ( ) : null} - {annotationsData.length > 0 && ( + {annotationsCnt > 0 && ( <> - - - ), + badge, }} /> diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js index 7efd36bbe57c6..27a934fa841fe 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js @@ -270,12 +270,6 @@ export class ExplorerChartDistribution extends React.Component { const tickValuesStart = Math.max(config.selectedEarliest, config.plotEarliest); // +1 ms to account for the ms that was subtracted for query aggregations. const interval = config.selectedLatest - config.selectedEarliest + 1; - const tickValues = getTickValues( - tickValuesStart, - interval, - config.plotEarliest, - config.plotLatest - ); const xAxis = d3.svg .axis() @@ -286,10 +280,18 @@ export class ExplorerChartDistribution extends React.Component { .tickPadding(10) .tickFormat((d) => moment(d).format(xAxisTickFormat)); - // With tooManyBuckets the chart would end up with no x-axis labels - // because the ticks are based on the span of the emphasis section, - // and the highlighted area spans the whole chart. - if (tooManyBuckets === false) { + // With tooManyBuckets, or when the chart is used as an embeddable, + // the chart would end up with no x-axis labels because the ticks are based on the span of the + // emphasis section, and the selected area spans the whole chart. + const useAutoTicks = + tooManyBuckets === true || interval >= config.plotLatest - config.plotEarliest; + if (useAutoTicks === false) { + const tickValues = getTickValues( + tickValuesStart, + interval, + config.plotEarliest, + config.plotLatest + ); xAxis.tickValues(tickValues); } else { xAxis.ticks(numTicksForDateFormat(vizWidth, xAxisTickFormat)); @@ -327,7 +329,7 @@ export class ExplorerChartDistribution extends React.Component { }); } - if (tooManyBuckets === false) { + if (useAutoTicks === false) { removeLabelOverlap(gAxis, tickValuesStart, interval, vizWidth); } } diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js index 11a15b192fc52..8d2f66a870c75 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js @@ -139,7 +139,7 @@ describe('ExplorerChart', () => { expect(+selectedInterval.getAttribute('height')).toBe(166); const xAxisTicks = wrapper.getDOMNode().querySelector('.x').querySelectorAll('.tick'); - expect([...xAxisTicks]).toHaveLength(0); + expect([...xAxisTicks]).toHaveLength(1); const yAxisTicks = wrapper.getDOMNode().querySelector('.y').querySelectorAll('.tick'); expect([...yAxisTicks]).toHaveLength(5); const emphasizedAxisLabel = wrapper diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js index dd07a7d6cbdee..19390017244a8 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js @@ -196,12 +196,6 @@ export class ExplorerChartSingleMetric extends React.Component { const tickValuesStart = Math.max(config.selectedEarliest, config.plotEarliest); // +1 ms to account for the ms that was subtracted for query aggregations. const interval = config.selectedLatest - config.selectedEarliest + 1; - const tickValues = getTickValues( - tickValuesStart, - interval, - config.plotEarliest, - config.plotLatest - ); const xAxis = d3.svg .axis() @@ -212,10 +206,18 @@ export class ExplorerChartSingleMetric extends React.Component { .tickPadding(10) .tickFormat((d) => moment(d).format(xAxisTickFormat)); - // With tooManyBuckets the chart would end up with no x-axis labels - // because the ticks are based on the span of the emphasis section, - // and the highlighted area spans the whole chart. - if (tooManyBuckets === false) { + // With tooManyBuckets, or when the chart is used as an embeddable, + // the chart would end up with no x-axis labels because the ticks are based on the span of the + // emphasis section, and the selected area spans the whole chart. + const useAutoTicks = + tooManyBuckets === true || interval >= config.plotLatest - config.plotEarliest; + if (useAutoTicks === false) { + const tickValues = getTickValues( + tickValuesStart, + interval, + config.plotEarliest, + config.plotLatest + ); xAxis.tickValues(tickValues); } else { xAxis.ticks(numTicksForDateFormat(vizWidth, xAxisTickFormat)); @@ -243,7 +245,7 @@ export class ExplorerChartSingleMetric extends React.Component { axes.append('g').attr('class', 'y axis').call(yAxis); - if (tooManyBuckets === false) { + if (useAutoTicks === false) { removeLabelOverlap(gAxis, tickValuesStart, interval, vizWidth); } } diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js index 981f7515d3d70..00172965bc216 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js @@ -144,7 +144,7 @@ describe('ExplorerChart', () => { expect(+selectedInterval.getAttribute('height')).toBe(166); const xAxisTicks = wrapper.getDOMNode().querySelector('.x').querySelectorAll('.tick'); - expect([...xAxisTicks]).toHaveLength(0); + expect([...xAxisTicks]).toHaveLength(1); const yAxisTicks = wrapper.getDOMNode().querySelector('.y').querySelectorAll('.tick'); expect([...yAxisTicks]).toHaveLength(10); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts b/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts index ebab308b86027..e8abe9e45c09a 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts @@ -181,15 +181,6 @@ declare interface LoadOverallDataResponse { overallSwimlaneData: OverallSwimlaneData; } -export declare const loadViewByTopFieldValuesForSelectedTime: ( - earliestMs: number, - latestMs: number, - selectedJobs: ExplorerJob[], - viewBySwimlaneFieldName: string, - swimlaneLimit: number, - noInfluencersConfigured: boolean -) => Promise; - export declare interface FilterData { influencersFilterQuery: InfluencersFilterQuery; filterActive: boolean; diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index 82f8a90fafb7d..86ec2014c8339 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -411,7 +411,7 @@ export const SwimlaneContainer: FC = ({ > <>
-
+
{showSwimlane && !isLoading && ( = ({ queryText, setFilters }) = }, [queryText]); const onChange: EuiSearchBarProps['onChange'] = ({ query, error: queryError }) => { - if (error) { + if (queryError) { setError(queryError); } else { setFilters(query); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts index e6e6b5305b1f8..65c0d30414eed 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts @@ -74,6 +74,7 @@ export function getScoresByRecord( timestamp: { gte: earliestMs, lte: latestMs, + // @ts-ignore format: 'epoch_millis', }, }, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts index 67673901494c7..88ed17cba0003 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts @@ -6,7 +6,7 @@ */ import { useContext, useState } from 'react'; - +import { i18n } from '@kbn/i18n'; import { JobCreatorContext } from '../../../job_creator_context'; import { EVENT_RATE_FIELD_ID } from '../../../../../../../../../common/types/fields'; import { BucketSpanEstimatorData } from '../../../../../../../../../common/types/job_service'; @@ -76,10 +76,16 @@ export function useEstimateBucketSpan() { async function estimateBucketSpan() { setStatus(ESTIMATE_STATUS.RUNNING); - const { name, error, message } = await ml.estimateBucketSpan(data); + const { name, error, message: text } = await ml.estimateBucketSpan(data); setStatus(ESTIMATE_STATUS.NOT_RUNNING); if (error === true) { - getToastNotificationService().displayErrorToast(message); + const title = i18n.translate( + 'xpack.ml.newJob.wizard.pickFieldsStep.bucketSpanEstimator.errorTitle', + { + defaultMessage: 'Bucket span could not be estimated', + } + ); + getToastNotificationService().displayWarningToast({ title, text }); } else { jobCreator.bucketSpan = name; jobCreatorUpdate(); diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/insufficient_license_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/insufficient_license_page.tsx index 6d0c3639d939c..7fcac0ad6992a 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/insufficient_license_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/insufficient_license_page.tsx @@ -30,7 +30,7 @@ export const InsufficientLicensePage: FC = ({ basePath }) => (

} @@ -38,13 +38,13 @@ export const InsufficientLicensePage: FC = ({ basePath }) => (

), diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index a0a81f77b7b08..f645a0753f8c2 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -127,7 +127,7 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim to: globalState.time.to, }); } - }, [globalState?.time?.from, globalState?.time?.to]); + }, [lastRefresh, globalState?.time?.from, globalState?.time?.to]); const getJobsWithStoppedPartitions = useCallback(async (selectedJobIds: string[]) => { try { diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx index ef669a7703c1f..6eb4386276753 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx @@ -142,10 +142,10 @@ export const TimeSeriesExplorerUrlStateManager: FC selectedEarliestMs || chartRange.max < selectedLatestBucketStart) && - chartRange.max - chartRange.min < selectedLatestBucketStart - selectedEarliestMs + (chartRange.min > selectedEarliestBucketCeil || chartRange.max < selectedLatestBucketStart) && + chartRange.max - chartRange.min < selectedLatestBucketStart - selectedEarliestBucketCeil ) { tooManyBuckets = true; } diff --git a/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts b/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts index e11eb4048c374..6f2b5417eff5f 100644 --- a/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts +++ b/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts @@ -25,6 +25,8 @@ import { } from '../explorer/explorer_utils'; import { OVERALL_LABEL, VIEW_BY_JOB_LABEL } from '../explorer/explorer_constants'; import { MlResultsService } from './results_service'; +import { EntityField } from '../../../common/util/anomaly_utils'; +import { InfluencersFilterQuery } from '../../../common/types/es_client'; /** * Service for retrieving anomaly swim lanes data. @@ -241,7 +243,9 @@ export class AnomalyTimelineService { swimlaneLimit: number, perPage: number, fromPage: number, - swimlaneContainerWidth: number + swimlaneContainerWidth: number, + selectionInfluencers: EntityField[], + influencersFilterQuery: InfluencersFilterQuery ) { const selectedJobIds = selectedJobs.map((d) => d.id); @@ -254,7 +258,9 @@ export class AnomalyTimelineService { latestMs, swimlaneLimit, perPage, - fromPage + fromPage, + selectionInfluencers, + influencersFilterQuery ); if (resp.influencers[viewBySwimlaneFieldName] === undefined) { return []; @@ -276,6 +282,8 @@ export class AnomalyTimelineService { earliestMs, latestMs, this.getSwimlaneBucketInterval(selectedJobs, swimlaneContainerWidth).asMilliseconds(), + perPage, + fromPage, swimlaneLimit ); return Object.keys(resp.results); diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts index 25ef36782207f..a9f6dbb45f6e3 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts @@ -16,6 +16,11 @@ import { JobId } from '../../../../common/types/anomaly_detection_jobs'; import { JOB_ID, PARTITION_FIELD_VALUE } from '../../../../common/constants/anomalies'; import { PartitionFieldsDefinition } from '../results_service/result_service_rx'; import { PartitionFieldsConfig } from '../../../../common/types/storage'; +import { + ESSearchRequest, + ESSearchResponse, +} from '../../../../../../../src/core/types/elasticsearch'; +import { MLAnomalyDoc } from '../../../../common/types/anomalies'; export const resultsApiProvider = (httpService: HttpService) => ({ getAnomaliesTableData( @@ -112,18 +117,18 @@ export const resultsApiProvider = (httpService: HttpService) => ({ }); }, - anomalySearch(query: any, jobIds: string[]) { + anomalySearch(query: ESSearchRequest, jobIds: string[]) { const body = JSON.stringify({ query, jobIds }); - return httpService.http({ + return httpService.http>({ path: `${basePath()}/results/anomaly_search`, method: 'POST', body, }); }, - anomalySearch$(query: any, jobIds: string[]) { + anomalySearch$(query: ESSearchRequest, jobIds: string[]) { const body = JSON.stringify({ query, jobIds }); - return httpService.http$({ + return httpService.http$>({ path: `${basePath()}/results/anomaly_search`, method: 'POST', body, diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts index ea07d32bfff1d..1848b13cb5a1f 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts +++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts @@ -23,7 +23,8 @@ export function resultsServiceProvider( intervalMs: number, perPage?: number, fromPage?: number, - swimLaneSeverity?: number + swimLaneSeverity?: number, + influencersFilterQuery?: InfluencersFilterQuery ): Promise; getTopInfluencers( selectedJobIds: string[], @@ -32,7 +33,7 @@ export function resultsServiceProvider( maxFieldValues: number, perPage?: number, fromPage?: number, - influencers?: any[], + influencers?: EntityField[], influencersFilterQuery?: InfluencersFilterQuery ): Promise; getTopInfluencerValues(): Promise; diff --git a/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts b/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts index 6cb5f67149fb6..56221f9a72c89 100644 --- a/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts +++ b/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts @@ -85,7 +85,6 @@ export function modelSnapshotProvider(client: IScopedClusterClient, mlClient: Ml ), events: calendarEvents.map((s) => ({ calendar_id: calendarId, - event_id: '', description: s.description, start_time: `${s.start}`, end_time: `${s.end}`, diff --git a/x-pack/plugins/monitoring/public/alerts/enable_alerts_modal.tsx b/x-pack/plugins/monitoring/public/alerts/enable_alerts_modal.tsx index 914446c42aaa7..fadf4c5872507 100644 --- a/x-pack/plugins/monitoring/public/alerts/enable_alerts_modal.tsx +++ b/x-pack/plugins/monitoring/public/alerts/enable_alerts_modal.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useEffect, useState, useContext } from 'react'; +import React, { useEffect, useState } from 'react'; import { EuiButton, @@ -22,14 +22,16 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { AlertsContext } from './context'; import { Legacy } from '../legacy_shims'; -export const EnableAlertsModal: React.FC<{}> = () => { +interface Props { + alerts: {}; +} + +export const EnableAlertsModal: React.FC = ({ alerts }: Props) => { const [isModalVisible, setIsModalVisible] = useState(false); const $injector = Legacy.shims.getAngularInjector(); const alertsEnableModalProvider: any = $injector.get('enableAlertsModal'); - const alertsContext = useContext(AlertsContext); const closeModal = () => { setIsModalVisible(false); @@ -58,10 +60,10 @@ export const EnableAlertsModal: React.FC<{}> = () => { }; useEffect(() => { - if (alertsEnableModalProvider.shouldShowAlertsModal(alertsContext)) { + if (alertsEnableModalProvider.shouldShowAlertsModal(alerts)) { setIsModalVisible(true); } - }, [alertsEnableModalProvider, alertsContext]); + }, [alertsEnableModalProvider, alerts]); const confirmButtonClick = () => { if (radioIdSelected === 'create-alerts') { diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index a5b7d4906b586..9f84165a27ba9 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -93,7 +93,10 @@ export class MonitoringPlugin category: DEFAULT_APP_CATEGORIES.management, mount: async (params: AppMountParameters) => { const [coreStart, pluginsStart] = await core.getStartServices(); - const { AngularApp } = await import('./angular'); + const [, { AngularApp }] = await Promise.all([ + pluginsStart.kibanaLegacy.loadAngularBootstrap(), + import('./angular'), + ]); const deps: MonitoringStartPluginDependencies = { navigation: pluginsStart.navigation, kibanaLegacy: pluginsStart.kibanaLegacy, diff --git a/x-pack/plugins/monitoring/public/services/enable_alerts_modal.js b/x-pack/plugins/monitoring/public/services/enable_alerts_modal.js index 0232e302517af..438c5ab83f5e3 100644 --- a/x-pack/plugins/monitoring/public/services/enable_alerts_modal.js +++ b/x-pack/plugins/monitoring/public/services/enable_alerts_modal.js @@ -12,12 +12,10 @@ export function enableAlertsModalProvider($http, $window, $injector) { const modalHasBeenShown = $window.sessionStorage.getItem('ALERTS_MODAL_HAS_BEEN_SHOWN'); const decisionMade = $window.localStorage.getItem('ALERTS_MODAL_DECISION_MADE'); - if (Object.keys(alerts.allAlerts).length > 0) { + if (Object.keys(alerts).length > 0) { $window.localStorage.setItem('ALERTS_MODAL_DECISION_MADE', true); return false; - } - - if (!modalHasBeenShown && !decisionMade) { + } else if (!modalHasBeenShown && !decisionMade) { return true; } diff --git a/x-pack/plugins/monitoring/public/views/cluster/listing/index.js b/x-pack/plugins/monitoring/public/views/cluster/listing/index.js index 9f9eec3848604..8b365292aeb13 100644 --- a/x-pack/plugins/monitoring/public/views/cluster/listing/index.js +++ b/x-pack/plugins/monitoring/public/views/cluster/listing/index.js @@ -13,6 +13,7 @@ import { MonitoringViewBaseEuiTableController } from '../../'; import template from './index.html'; import { Listing } from '../../../components/cluster/listing'; import { CODE_PATH_ALL } from '../../../../common/constants'; +import { EnableAlertsModal } from '../../../alerts/enable_alerts_modal.tsx'; const CODE_PATHS = [CODE_PATH_ALL]; @@ -21,6 +22,10 @@ const getPageData = ($injector) => { return monitoringClusters(undefined, undefined, CODE_PATHS); }; +const getAlerts = (clusters) => { + return clusters.reduce((alerts, cluster) => ({ ...alerts, ...cluster.alerts.list }), {}); +}; + uiRoutes .when('/home', { template, @@ -71,18 +76,21 @@ uiRoutes () => this.data, (data) => { this.renderReact( -

+ <> + + + ); } ); diff --git a/x-pack/plugins/monitoring/public/views/cluster/overview/index.js b/x-pack/plugins/monitoring/public/views/cluster/overview/index.js index bf34650bdb700..20e694ad8548f 100644 --- a/x-pack/plugins/monitoring/public/views/cluster/overview/index.js +++ b/x-pack/plugins/monitoring/public/views/cluster/overview/index.js @@ -83,7 +83,7 @@ uiRoutes.when('/overview', { setupMode={setupMode} showLicenseExpiration={showLicenseExpiration} /> - + {bottomBarComponent} )} diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index c4a0687bef497..b920f2bfacf80 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -20,6 +20,7 @@ import type { ActionsApiRequestHandlerContext, } from '../../actions/server'; import type { AlertingApiRequestHandlerContext } from '../../alerting/server'; +import type { RacApiRequestHandlerContext } from '../../rule_registry/server'; import { PluginStartContract as AlertingPluginStartContract, PluginSetupContract as AlertingPluginSetupContract, @@ -57,6 +58,7 @@ export interface RequestHandlerContextMonitoringPlugin extends RequestHandlerCon actions?: ActionsApiRequestHandlerContext; alerting?: AlertingApiRequestHandlerContext; infra: InfraRequestHandlerContext; + ruleRegistry?: RacApiRequestHandlerContext; } export interface PluginsStart { diff --git a/x-pack/plugins/observability/.storybook/jest_setup.js b/x-pack/plugins/observability/.storybook/jest_setup.js new file mode 100644 index 0000000000000..32071b8aa3f62 --- /dev/null +++ b/x-pack/plugins/observability/.storybook/jest_setup.js @@ -0,0 +1,11 @@ +/* + * 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 { setGlobalConfig } from '@storybook/testing-react'; +import * as globalStorybookConfig from './preview'; + +setGlobalConfig(globalStorybookConfig); diff --git a/x-pack/plugins/observability/.storybook/preview.js b/x-pack/plugins/observability/.storybook/preview.js new file mode 100644 index 0000000000000..18343c15a6465 --- /dev/null +++ b/x-pack/plugins/observability/.storybook/preview.js @@ -0,0 +1,10 @@ +/* + * 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 { EuiThemeProviderDecorator } from '../../../../src/plugins/kibana_react/common'; + +export const decorators = [EuiThemeProviderDecorator]; diff --git a/x-pack/plugins/observability/jest.config.js b/x-pack/plugins/observability/jest.config.js index 66d42122382f3..6fdeab06df053 100644 --- a/x-pack/plugins/observability/jest.config.js +++ b/x-pack/plugins/observability/jest.config.js @@ -9,4 +9,5 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/observability'], + setupFiles: ['/x-pack/plugins/observability/.storybook/jest_setup.js'], }; diff --git a/x-pack/plugins/observability/public/components/shared/core_web_vitals/__stories__/core_vitals.stories.tsx b/x-pack/plugins/observability/public/components/shared/core_web_vitals/__stories__/core_vitals.stories.tsx index 5f5cf2cb4da21..5c07b4626cf19 100644 --- a/x-pack/plugins/observability/public/components/shared/core_web_vitals/__stories__/core_vitals.stories.tsx +++ b/x-pack/plugins/observability/public/components/shared/core_web_vitals/__stories__/core_vitals.stories.tsx @@ -9,7 +9,6 @@ import React, { ComponentType } from 'react'; import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { Observable } from 'rxjs'; import { CoreStart } from 'src/core/public'; -import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { createKibanaReactContext } from '../../../../../../../../src/plugins/kibana_react/public'; import { CoreVitalItem } from '../core_vital_item'; import { LCP_HELP_LABEL, LCP_LABEL } from '../translations'; @@ -25,9 +24,7 @@ export default { (Story: ComponentType) => ( - - - + ), diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts index dd48cf3f7eeb8..ba1f2214223e3 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts @@ -44,6 +44,7 @@ import { TAGS_LABEL, TBT_LABEL, URL_LABEL, + BACKEND_TIME_LABEL, } from './labels'; export const DEFAULT_TIME = { from: 'now-1h', to: 'now' }; @@ -66,7 +67,7 @@ export const FieldLabels: Record = { [TBT_FIELD]: TBT_LABEL, [FID_FIELD]: FID_LABEL, [CLS_FIELD]: CLS_LABEL, - [TRANSACTION_TIME_TO_FIRST_BYTE]: 'Page load time', + [TRANSACTION_TIME_TO_FIRST_BYTE]: BACKEND_TIME_LABEL, 'monitor.id': MONITOR_ID_LABEL, 'monitor.status': MONITOR_STATUS_LABEL, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts index 0be64677586c1..ae70bbdcfa3b8 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts @@ -411,6 +411,7 @@ describe('Lens Attribute', () => { sourceField: USER_AGENT_NAME, layerId: 'layer0', indexPattern: mockIndexPattern, + labels: layerConfig.seriesConfig.labels, }); expect(lnsAttr.visualization.layers).toEqual([ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index 5734cd1592692..dfb17ee470d35 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -30,7 +30,6 @@ import { import { urlFiltersToKueryString } from '../utils/stringify_kueries'; import { ExistsFilter, IndexPattern } from '../../../../../../../../src/plugins/data/common'; import { - FieldLabels, FILTER_RECORDS, USE_BREAK_DOWN_COLUMN, TERMS_COLUMN, @@ -125,17 +124,19 @@ export class LensAttributes { getBreakdownColumn({ sourceField, layerId, + labels, indexPattern, }: { sourceField: string; layerId: string; + labels: Record; indexPattern: IndexPattern; }): TermsIndexPatternColumn { const fieldMeta = indexPattern.getFieldByName(sourceField); return { sourceField, - label: `Top values of ${FieldLabels[sourceField]}`, + label: `Top values of ${labels[sourceField]}`, dataType: fieldMeta?.type as DataType, operationType: 'terms', scale: 'ordinal', @@ -304,6 +305,7 @@ export class LensAttributes { layerId, indexPattern: layerConfig.indexPattern, sourceField: layerConfig.breakdown || layerConfig.seriesConfig.breakdownFields[0], + labels: layerConfig.seriesConfig.labels, }); } @@ -344,7 +346,7 @@ export class LensAttributes { if (fieldName === RECORDS_FIELD || columnType === FILTER_RECORDS) { return this.getRecordsColumn( - columnLabel || label, + label || columnLabel, colIndex !== undefined ? columnFilters?.[colIndex] : undefined, timeScale ); @@ -433,6 +435,8 @@ export class LensAttributes { if (yAxisColumns.length === 1) { return lensColumns; } + + // starting from 1 index since 0 column is used as main column for (let i = 1; i < yAxisColumns.length; i++) { const { sourceField, operationType, label } = yAxisColumns[i]; @@ -555,16 +559,21 @@ export class LensAttributes { const layerConfigs = this.layerConfigs; layerConfigs.forEach((layerConfig, index) => { - const { breakdown } = layerConfig; + const { breakdown, seriesConfig } = layerConfig; const layerId = `layer${index}`; const columnFilter = this.getLayerFilters(layerConfig, layerConfigs.length); const timeShift = this.getTimeShift(this.layerConfigs[0], layerConfig, index); const mainYAxis = this.getMainYAxis(layerConfig, layerId, columnFilter); + + const { sourceField } = seriesConfig.xAxisColumn; + layers[layerId] = { columnOrder: [ `x-axis-column-${layerId}`, - ...(breakdown ? [`breakdown-column-${layerId}`] : []), + ...(breakdown && sourceField !== USE_BREAK_DOWN_COLUMN + ? [`breakdown-column-${layerId}`] + : []), `y-axis-column-${layerId}`, ...Object.keys(this.getChildYAxises(layerConfig, layerId, columnFilter)), ], @@ -576,13 +585,14 @@ export class LensAttributes { filter: { query: columnFilter, language: 'kuery' }, ...(timeShift ? { timeShift } : {}), }, - ...(breakdown && breakdown !== USE_BREAK_DOWN_COLUMN + ...(breakdown && sourceField !== USE_BREAK_DOWN_COLUMN ? // do nothing since this will be used a x axis source { [`breakdown-column-${layerId}`]: this.getBreakdownColumn({ layerId, sourceField: breakdown, indexPattern: layerConfig.indexPattern, + labels: layerConfig.seriesConfig.labels, }), } : {}), @@ -617,7 +627,10 @@ export class LensAttributes { { forAccessor: `y-axis-column-layer${index}` }, ], xAccessor: `x-axis-column-layer${index}`, - ...(layerConfig.breakdown ? { splitAccessor: `breakdown-column-layer${index}` } : {}), + ...(layerConfig.breakdown && + layerConfig.seriesConfig.xAxisColumn.sourceField !== USE_BREAK_DOWN_COLUMN + ? { splitAccessor: `breakdown-column-layer${index}` } + : {}), })), ...(this.layerConfigs[0].seriesConfig.yTitle ? { yTitle: this.layerConfigs[0].seriesConfig.yTitle } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts index 98979b9922a86..d1612a08f5551 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts @@ -6,7 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, USE_BREAK_DOWN_COLUMN } from '../constants'; +import { FieldLabels, REPORT_METRIC_FIELD, USE_BREAK_DOWN_COLUMN } from '../constants'; import { buildPhraseFilter } from '../utils'; import { SERVICE_NAME } from '../constants/elasticsearch_fieldnames'; import { MOBILE_APP, NUMBER_OF_DEVICES } from '../constants/labels'; @@ -22,9 +22,8 @@ export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps) }, yAxisColumns: [ { - sourceField: 'labels.device_id', + sourceField: REPORT_METRIC_FIELD, operationType: 'unique_count', - label: NUMBER_OF_DEVICES, }, ], hasOperationType: false, @@ -39,6 +38,13 @@ export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps) ...MobileFields, [SERVICE_NAME]: MOBILE_APP, }, + metricOptions: [ + { + id: 'labels.device_id', + field: 'labels.device_id', + label: NUMBER_OF_DEVICES, + }, + ], definitionFields: [SERVICE_NAME], }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts index b9894347d96c0..9b1c4c8da3e9b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts @@ -6,7 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; +import { FieldLabels, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; import { buildPhrasesFilter } from '../utils'; import { METRIC_SYSTEM_CPU_USAGE, @@ -49,19 +49,16 @@ export function getMobileKPIDistributionConfig({ indexPattern }: ConfigProps): S label: RESPONSE_LATENCY, field: TRANSACTION_DURATION, id: TRANSACTION_DURATION, - columnType: OPERATION_COLUMN, }, { label: MEMORY_USAGE, field: METRIC_SYSTEM_MEMORY_USAGE, id: METRIC_SYSTEM_MEMORY_USAGE, - columnType: OPERATION_COLUMN, }, { label: CPU_USAGE, field: METRIC_SYSTEM_CPU_USAGE, id: METRIC_SYSTEM_CPU_USAGE, - columnType: OPERATION_COLUMN, }, ], }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts new file mode 100644 index 0000000000000..07bb13f957e45 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts @@ -0,0 +1,39 @@ +/* + * 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 { mockAppIndexPattern, mockIndexPattern } from '../../rtl_helpers'; +import { getDefaultConfigs } from '../default_configs'; +import { LayerConfig, LensAttributes } from '../lens_attributes'; +import { sampleAttributeCoreWebVital } from '../test_data/sample_attribute_cwv'; +import { SERVICE_NAME, USER_AGENT_OS } from '../constants/elasticsearch_fieldnames'; + +describe('Core web vital config test', function () { + mockAppIndexPattern(); + + const seriesConfig = getDefaultConfigs({ + reportType: 'core-web-vitals', + dataType: 'ux', + indexPattern: mockIndexPattern, + }); + + let lnsAttr: LensAttributes; + + const layerConfig: LayerConfig = { + seriesConfig, + indexPattern: mockIndexPattern, + reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] }, + time: { from: 'now-15m', to: 'now' }, + breakdown: USER_AGENT_OS, + }; + + beforeEach(() => { + lnsAttr = new LensAttributes([layerConfig]); + }); + it('should return expected json', function () { + expect(lnsAttr.getJSON()).toEqual(sampleAttributeCoreWebVital); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts index 1d04a9b389503..62455df248085 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts @@ -31,6 +31,7 @@ import { URL_FULL, SERVICE_ENVIRONMENT, } from '../constants/elasticsearch_fieldnames'; +import { CLS_LABEL, FID_LABEL, LCP_LABEL } from '../constants/labels'; export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesConfig { const statusPallete = euiPaletteForStatus(3); @@ -91,7 +92,7 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesCon metricOptions: [ { id: LCP_FIELD, - label: 'Largest contentful paint', + label: LCP_LABEL, columnType: FILTER_RECORDS, columnFilters: [ { @@ -109,7 +110,7 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesCon ], }, { - label: 'First input delay', + label: FID_LABEL, id: FID_FIELD, columnType: FILTER_RECORDS, columnFilters: [ @@ -128,7 +129,7 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesCon ], }, { - label: 'Cumulative layout shift', + label: CLS_LABEL, id: CLS_FIELD, columnType: FILTER_RECORDS, columnFilters: [ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts new file mode 100644 index 0000000000000..2087b85b81886 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts @@ -0,0 +1,149 @@ +/* + * 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 sampleAttributeCoreWebVital = { + description: '', + references: [ + { + id: 'apm-*', + name: 'indexpattern-datasource-current-indexpattern', + type: 'index-pattern', + }, + { + id: 'apm-*', + name: 'indexpattern-datasource-layer-layer0', + type: 'index-pattern', + }, + ], + state: { + datasourceStates: { + indexpattern: { + layers: { + layer0: { + columnOrder: [ + 'x-axis-column-layer0', + 'y-axis-column-layer0', + 'y-axis-column-1', + 'y-axis-column-2', + ], + columns: { + 'x-axis-column-layer0': { + dataType: 'string', + isBucketed: true, + label: 'Top values of Operating system', + operationType: 'terms', + params: { + missingBucket: false, + orderBy: { + columnId: 'y-axis-column-layer0', + type: 'column', + }, + orderDirection: 'desc', + otherBucket: true, + size: 10, + }, + scale: 'ordinal', + sourceField: 'user_agent.os.name', + }, + 'y-axis-column-1': { + dataType: 'number', + filter: { + language: 'kuery', + query: + 'transaction.marks.agent.largestContentfulPaint > 2500 and transaction.marks.agent.largestContentfulPaint < 4000', + }, + isBucketed: false, + label: 'Average', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + 'y-axis-column-2': { + dataType: 'number', + filter: { + language: 'kuery', + query: 'transaction.marks.agent.largestContentfulPaint > 4000', + }, + isBucketed: false, + label: 'Poor', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + 'y-axis-column-layer0': { + dataType: 'number', + filter: { + language: 'kuery', + query: 'transaction.type: page-load and processor.event: transaction', + }, + isBucketed: false, + label: 'Good', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + }, + incompleteColumns: {}, + }, + }, + }, + }, + filters: [], + query: { + language: 'kuery', + query: '', + }, + visualization: { + axisTitlesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + curveType: 'CURVE_MONOTONE_X', + fittingFunction: 'Linear', + gridlinesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + layers: [ + { + accessors: ['y-axis-column-layer0', 'y-axis-column-1', 'y-axis-column-2'], + layerId: 'layer0', + seriesType: 'bar_horizontal_percentage_stacked', + xAccessor: 'x-axis-column-layer0', + yConfig: [ + { + color: '#209280', + forAccessor: 'y-axis-column', + }, + { + color: '#d6bf57', + forAccessor: 'y-axis-column-1', + }, + { + color: '#cc5642', + forAccessor: 'y-axis-column-2', + }, + ], + }, + ], + legend: { + isVisible: true, + position: 'right', + }, + preferredSeriesType: 'line', + tickLabelsVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + valueLabels: 'hide', + }, + }, + title: 'Prefilled from exploratory view app', + visualizationType: 'lnsXY', +}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx index 07048d47b2bc3..12ae8560453c9 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx @@ -36,7 +36,6 @@ describe('ReportTypesCol', function () { fireEvent.click(screen.getByText(/KPI over time/i)); expect(setSeries).toHaveBeenCalledWith(seriesId, { - breakdown: 'user_agent.name', dataType: 'ux', selectedMetricField: undefined, reportType: 'kpi-over-time', diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx index 396f8c4f1deb3..c4eebbfaca3eb 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx @@ -78,6 +78,7 @@ export function ReportTypesCol({ seriesId, reportTypes }: Props) { ...restSeries, reportType, selectedMetricField: undefined, + breakdown: undefined, time: restSeries?.time ?? DEFAULT_TIME, }); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts index 634408dd614da..964de86ddf377 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts @@ -100,11 +100,14 @@ export class ObservabilityIndexPatterns { if (defaultFieldFormats && defaultFieldFormats.length > 0) { let isParamsDifferent = false; defaultFieldFormats.forEach(({ field, format }) => { - const fieldFormat = indexPattern.getFormatterForField(indexPattern.getFieldByName(field)!); - const params = fieldFormat.params(); - if (!isParamsSame(params, format.params)) { - indexPattern.setFieldFormat(field, format); - isParamsDifferent = true; + const fieldByName = indexPattern.getFieldByName(field); + if (fieldByName) { + const fieldFormat = indexPattern.getFormatterForField(fieldByName); + const params = fieldFormat.params(); + if (!isParamsSame(params, format.params)) { + indexPattern.setFieldFormat(field, format); + isParamsDifferent = true; + } } }); if (isParamsDifferent) { diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/__stories__/field_value_selection.stories.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/__stories__/field_value_selection.stories.tsx index 80a25b82eb8cb..1152ba32960ed 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/__stories__/field_value_selection.stories.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/__stories__/field_value_selection.stories.tsx @@ -10,7 +10,6 @@ import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { Observable } from 'rxjs'; import { CoreStart } from 'src/core/public'; import { text } from '@storybook/addon-knobs'; -import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { createKibanaReactContext } from '../../../../../../../../src/plugins/kibana_react/public'; import { FieldValueSelectionProps } from '../types'; import { FieldValueSelection } from '../field_value_selection'; @@ -31,16 +30,14 @@ export default { (Story: ComponentType) => ( - - {}} - selectedValue={[]} - loading={false} - setQuery={() => {}} - /> - + {}} + selectedValue={[]} + loading={false} + setQuery={() => {}} + /> ), diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index 0561eab08fb45..6bafe465fd024 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -68,5 +68,9 @@ export { createExploratoryViewUrl } from './components/shared/exploratory_view/c export { FilterValueLabel } from './components/shared/filter_value_label/filter_value_label'; export type { SeriesUrl } from './components/shared/exploratory_view/types'; -export type { ObservabilityRuleTypeRegistry } from './rules/create_observability_rule_type_registry'; +export type { + ObservabilityRuleTypeFormatter, + ObservabilityRuleTypeModel, + ObservabilityRuleTypeRegistry, +} from './rules/create_observability_rule_type_registry'; export { createObservabilityRuleTypeRegistryMock } from './rules/observability_rule_type_registry_mock'; diff --git a/x-pack/plugins/observability/public/pages/landing/landing.stories.tsx b/x-pack/plugins/observability/public/pages/landing/landing.stories.tsx index 86922b045c742..ef3ded61492c7 100644 --- a/x-pack/plugins/observability/public/pages/landing/landing.stories.tsx +++ b/x-pack/plugins/observability/public/pages/landing/landing.stories.tsx @@ -6,7 +6,6 @@ */ import React, { ComponentType } from 'react'; -import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common'; import { PluginContext, PluginContextValue } from '../../context/plugin_context'; import { LandingPage } from './'; @@ -27,9 +26,7 @@ export default { } as unknown) as PluginContextValue; return ( - - - + ); }, diff --git a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx index dd424cf221d15..2982333235331 100644 --- a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx +++ b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx @@ -10,7 +10,6 @@ import { storiesOf } from '@storybook/react'; import { AppMountParameters, CoreStart } from 'kibana/public'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common'; import { UI_SETTINGS } from '../../../../../../src/plugins/data/public'; import { HasDataContextProvider } from '../../context/has_data_context'; import { PluginContext } from '../../context/plugin_context'; @@ -65,9 +64,7 @@ const withCore = makeDecorator({ ObservabilityPageTemplate: KibanaPageTemplate, }} > - - {storyFn(context)} - + {storyFn(context)} ); diff --git a/x-pack/plugins/observability/public/rules/create_observability_rule_type_registry.ts b/x-pack/plugins/observability/public/rules/create_observability_rule_type_registry.ts index 35f2dc18c2f22..d6f8c08359888 100644 --- a/x-pack/plugins/observability/public/rules/create_observability_rule_type_registry.ts +++ b/x-pack/plugins/observability/public/rules/create_observability_rule_type_registry.ts @@ -5,19 +5,29 @@ * 2.0. */ -import { AlertTypeModel, AlertTypeRegistryContract } from '../../../triggers_actions_ui/public'; +import { + AlertTypeModel, + AlertTypeParams, + AlertTypeRegistryContract, +} from '../../../triggers_actions_ui/public'; import { ParsedTechnicalFields } from '../../../rule_registry/common/parse_technical_fields'; import { AsDuration, AsPercent } from '../../common/utils/formatters'; -export type Formatter = (options: { +export type ObservabilityRuleTypeFormatter = (options: { fields: ParsedTechnicalFields & Record; formatters: { asDuration: AsDuration; asPercent: AsPercent }; }) => { reason: string; link: string }; +export interface ObservabilityRuleTypeModel + extends AlertTypeModel { + format: ObservabilityRuleTypeFormatter; +} + export function createObservabilityRuleTypeRegistry(alertTypeRegistry: AlertTypeRegistryContract) { - const formatters: Array<{ typeId: string; fn: Formatter }> = []; + const formatters: Array<{ typeId: string; fn: ObservabilityRuleTypeFormatter }> = []; + return { - register: (type: AlertTypeModel & { format: Formatter }) => { + register: (type: ObservabilityRuleTypeModel) => { const { format, ...rest } = type; formatters.push({ typeId: type.id, fn: format }); alertTypeRegistry.register(rest); diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index d820a6c0a6f76..868e234fcb2a1 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -38,47 +38,49 @@ export class ObservabilityPlugin implements Plugin { } public setup(core: CoreSetup, plugins: PluginSetup) { - plugins.features.registerKibanaFeature({ - id: casesFeatureId, - name: i18n.translate('xpack.observability.featureRegistry.linkObservabilityTitle', { - defaultMessage: 'Cases', - }), - order: 1100, - category: DEFAULT_APP_CATEGORIES.observability, - app: [casesFeatureId, 'kibana'], - catalogue: [observabilityFeatureId], - cases: [observabilityFeatureId], - privileges: { - all: { - app: [casesFeatureId, 'kibana'], - catalogue: [observabilityFeatureId], - cases: { - all: [observabilityFeatureId], - }, - api: [], - savedObject: { - all: [], - read: [], - }, - ui: ['crud_cases', 'read_cases'], // uiCapabilities[casesFeatureId].crud_cases or read_cases - }, - read: { - app: [casesFeatureId, 'kibana'], - catalogue: [observabilityFeatureId], - cases: { - read: [observabilityFeatureId], + const config = this.initContext.config.get(); + + if (config.unsafe.cases.enabled) { + plugins.features.registerKibanaFeature({ + id: casesFeatureId, + name: i18n.translate('xpack.observability.featureRegistry.linkObservabilityTitle', { + defaultMessage: 'Cases', + }), + order: 1100, + category: DEFAULT_APP_CATEGORIES.observability, + app: [casesFeatureId, 'kibana'], + catalogue: [observabilityFeatureId], + cases: [observabilityFeatureId], + privileges: { + all: { + app: [casesFeatureId, 'kibana'], + catalogue: [observabilityFeatureId], + cases: { + all: [observabilityFeatureId], + }, + api: [], + savedObject: { + all: [], + read: [], + }, + ui: ['crud_cases', 'read_cases'], // uiCapabilities[casesFeatureId].crud_cases or read_cases }, - api: [], - savedObject: { - all: [], - read: [], + read: { + app: [casesFeatureId, 'kibana'], + catalogue: [observabilityFeatureId], + cases: { + read: [observabilityFeatureId], + }, + api: [], + savedObject: { + all: [], + read: [], + }, + ui: ['read_cases'], // uiCapabilities[uiCapabilities[casesFeatureId]].read_cases }, - ui: ['read_cases'], // uiCapabilities[uiCapabilities[casesFeatureId]].read_cases }, - }, - }); - - const config = this.initContext.config.get(); + }); + } let annotationsApiPromise: Promise | undefined; @@ -99,6 +101,7 @@ export class ObservabilityPlugin implements Plugin { const start = () => core.getStartServices().then(([coreStart]) => coreStart); const ruleDataClient = plugins.ruleRegistry.ruleDataService.getRuleDataClient( + 'observability', plugins.ruleRegistry.ruleDataService.getFullAssetName(), () => Promise.resolve() ); diff --git a/x-pack/plugins/osquery/public/action_results/action_agents_status.tsx b/x-pack/plugins/osquery/public/action_results/action_agents_status.tsx new file mode 100644 index 0000000000000..2de5ab11664ae --- /dev/null +++ b/x-pack/plugins/osquery/public/action_results/action_agents_status.tsx @@ -0,0 +1,91 @@ +/* + * 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, EuiFlexItem, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useEffect, useMemo, useState } from 'react'; + +import { Direction } from '../../common/search_strategy'; +import { AgentStatusBar } from './action_agents_status_bar'; +import { ActionAgentsStatusBadges } from './action_agents_status_badges'; +import { useActionResults } from './use_action_results'; + +interface ActionAgentsStatusProps { + actionId: string; + expirationDate?: string; + agentIds?: string[]; +} + +const ActionAgentsStatusComponent: React.FC = ({ + actionId, + expirationDate, + agentIds, +}) => { + const [isLive, setIsLive] = useState(true); + const expired = useMemo(() => (!expirationDate ? false : new Date(expirationDate) < new Date()), [ + expirationDate, + ]); + const { + // @ts-expect-error update types + data: { aggregations }, + } = useActionResults({ + actionId, + activePage: 0, + agentIds, + limit: 0, + direction: Direction.asc, + sortField: '@timestamp', + isLive, + }); + + const agentStatus = useMemo(() => { + const notRespondedCount = !agentIds?.length ? 0 : agentIds.length - aggregations.totalResponded; + + return { + success: aggregations.successful, + pending: notRespondedCount, + failed: aggregations.failed, + }; + }, [agentIds?.length, aggregations.failed, aggregations.successful, aggregations.totalResponded]); + + useEffect( + () => + setIsLive(() => { + if (!agentIds?.length || expired) return false; + + return !!(aggregations.totalResponded !== agentIds?.length); + }), + [agentIds?.length, aggregations.totalResponded, expired] + ); + + return ( + <> + + + + + + + + + + + + + + + + + ); +}; + +export const ActionAgentsStatus = React.memo(ActionAgentsStatusComponent); diff --git a/x-pack/plugins/osquery/public/action_results/action_agents_status_badges.tsx b/x-pack/plugins/osquery/public/action_results/action_agents_status_badges.tsx new file mode 100644 index 0000000000000..95b96ca454610 --- /dev/null +++ b/x-pack/plugins/osquery/public/action_results/action_agents_status_badges.tsx @@ -0,0 +1,50 @@ +/* + * 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, EuiHealth, EuiNotificationBadge, EuiFlexItem } from '@elastic/eui'; +import React, { memo } from 'react'; + +import { + AGENT_STATUSES, + getColorForAgentStatus, + getLabelForAgentStatus, +} from './services/agent_status'; +import type { ActionAgentStatus } from './types'; + +export const ActionAgentsStatusBadges = memo<{ + agentStatus: { [k in ActionAgentStatus]: number }; + expired: boolean; +}>(({ agentStatus, expired }) => ( + + {AGENT_STATUSES.map((status) => ( + + + + ))} + +)); + +ActionAgentsStatusBadges.displayName = 'ActionAgentsStatusBadges'; + +const AgentStatusBadge = memo<{ expired: boolean; status: ActionAgentStatus; count: number }>( + ({ expired, status, count }) => ( + <> + + + {getLabelForAgentStatus(status, expired)} + + + {count} + + + + + + ) +); + +AgentStatusBadge.displayName = 'AgentStatusBadge'; diff --git a/x-pack/plugins/osquery/public/action_results/action_agents_status_bar.tsx b/x-pack/plugins/osquery/public/action_results/action_agents_status_bar.tsx new file mode 100644 index 0000000000000..21866566cb7e3 --- /dev/null +++ b/x-pack/plugins/osquery/public/action_results/action_agents_status_bar.tsx @@ -0,0 +1,46 @@ +/* + * 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 styled from 'styled-components'; +import { EuiColorPaletteDisplay } from '@elastic/eui'; +import React, { useMemo } from 'react'; + +import { AGENT_STATUSES, getColorForAgentStatus } from './services/agent_status'; +import type { ActionAgentStatus } from './types'; + +const StyledEuiColorPaletteDisplay = styled(EuiColorPaletteDisplay)` + &.osquery-action-agent-status-bar { + border: none; + border-radius: 0; + &:after { + border: none; + } + } +`; + +export const AgentStatusBar: React.FC<{ + agentStatus: { [k in ActionAgentStatus]: number }; +}> = ({ agentStatus }) => { + const palette = useMemo(() => { + let stop = 0; + return AGENT_STATUSES.reduce((acc, status) => { + stop += agentStatus[status] || 0; + acc.push({ + stop, + color: getColorForAgentStatus(status), + }); + return acc; + }, [] as Array<{ stop: number; color: string }>); + }, [agentStatus]); + return ( + + ); +}; diff --git a/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx b/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx index 257c89047aab0..bf4c97d63d74c 100644 --- a/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx +++ b/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx @@ -8,37 +8,18 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { i18n } from '@kbn/i18n'; -import { - EuiLink, - EuiFlexGroup, - EuiFlexItem, - EuiCard, - EuiTextColor, - EuiSpacer, - EuiDescriptionList, - EuiInMemoryTable, - EuiCodeBlock, - EuiProgress, -} from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; -import styled from 'styled-components'; +import { EuiInMemoryTable, EuiCodeBlock } from '@elastic/eui'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { PLUGIN_ID } from '../../../fleet/common'; -import { pagePathGetters } from '../../../fleet/public'; +import { AgentIdToName } from '../agents/agent_id_to_name'; import { useActionResults } from './use_action_results'; import { useAllResults } from '../results/use_all_results'; import { Direction } from '../../common/search_strategy'; -import { useKibana } from '../common/lib/kibana'; - -const StyledEuiCard = styled(EuiCard)` - position: relative; -`; interface ActionResultsSummaryProps { actionId: string; - expirationDate: Date; + expirationDate?: string; agentIds?: string[]; - isLive?: boolean; } const renderErrorMessage = (error: string) => ( @@ -51,14 +32,15 @@ const ActionResultsSummaryComponent: React.FC = ({ actionId, expirationDate, agentIds, - isLive, }) => { - const getUrlForApp = useKibana().services.application.getUrlForApp; // @ts-expect-error update types const [pageIndex, setPageIndex] = useState(0); // @ts-expect-error update types const [pageSize, setPageSize] = useState(50); - const expired = useMemo(() => expirationDate < new Date(), [expirationDate]); + const expired = useMemo(() => (!expirationDate ? false : new Date(expirationDate) < new Date()), [ + expirationDate, + ]); + const [isLive, setIsLive] = useState(true); const { // @ts-expect-error update types data: { aggregations, edges }, @@ -69,7 +51,7 @@ const ActionResultsSummaryComponent: React.FC = ({ limit: pageSize, direction: Direction.asc, sortField: '@timestamp', - isLive: !expired && isLive, + isLive, }); const { data: logsResults } = useAllResults({ @@ -82,72 +64,10 @@ const ActionResultsSummaryComponent: React.FC = ({ direction: Direction.asc, }, ], - isLive: !expired && isLive, + isLive, }); - const notRespondedCount = useMemo(() => { - if (!agentIds || !aggregations.totalResponded) { - return '-'; - } - - return agentIds.length - aggregations.totalResponded; - }, [aggregations.totalResponded, agentIds]); - - const listItems = useMemo( - () => [ - { - title: i18n.translate( - 'xpack.osquery.liveQueryActionResults.summary.agentsQueriedLabelText', - { - defaultMessage: 'Agents queried', - } - ), - description: agentIds?.length, - }, - { - title: i18n.translate('xpack.osquery.liveQueryActionResults.summary.successfulLabelText', { - defaultMessage: 'Successful', - }), - description: aggregations.successful, - }, - { - title: expired - ? i18n.translate('xpack.osquery.liveQueryActionResults.summary.expiredLabelText', { - defaultMessage: 'Expired', - }) - : i18n.translate('xpack.osquery.liveQueryActionResults.summary.pendingLabelText', { - defaultMessage: 'Not yet responded', - }), - description: notRespondedCount, - }, - { - title: i18n.translate('xpack.osquery.liveQueryActionResults.summary.failedLabelText', { - defaultMessage: 'Failed', - }), - description: ( - - {aggregations.failed} - - ), - }, - ], - [agentIds, aggregations.failed, aggregations.successful, notRespondedCount, expired] - ); - - const renderAgentIdColumn = useCallback( - (agentId) => ( - - {agentId} - - ), - [getUrlForApp] - ); + const renderAgentIdColumn = useCallback((agentId) => , []); const renderRowsColumn = useCallback( (_, item) => { @@ -236,30 +156,26 @@ const ActionResultsSummaryComponent: React.FC = ({ [] ); - return ( - <> - - - - {!expired && notRespondedCount ? : null} - - - - + useEffect(() => { + setIsLive(() => { + if (!agentIds?.length || expired) return false; - {edges.length ? ( - <> - - - - ) : null} - - ); + const uniqueAgentsRepliedCount = + // @ts-expect-error update types + logsResults?.rawResponse.aggregations?.unique_agents.value ?? 0; + + return !!(uniqueAgentsRepliedCount !== agentIds?.length - aggregations.failed); + }); + }, [ + agentIds?.length, + aggregations.failed, + expired, + logsResults?.rawResponse.aggregations?.unique_agents, + ]); + + return edges.length ? ( + + ) : null; }; export const ActionResultsSummary = React.memo(ActionResultsSummaryComponent); diff --git a/x-pack/plugins/osquery/public/action_results/services/agent_status.tsx b/x-pack/plugins/osquery/public/action_results/services/agent_status.tsx new file mode 100644 index 0000000000000..39a033f49ec90 --- /dev/null +++ b/x-pack/plugins/osquery/public/action_results/services/agent_status.tsx @@ -0,0 +1,59 @@ +/* + * 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 { euiPaletteColorBlindBehindText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import type { ActionAgentStatus } from '../types'; + +const visColors = euiPaletteColorBlindBehindText(); +const colorToHexMap = { + default: '#d3dae6', + primary: visColors[1], + secondary: visColors[0], + accent: visColors[2], + warning: visColors[5], + danger: visColors[9], +}; + +export const AGENT_STATUSES: ActionAgentStatus[] = ['success', 'pending', 'failed']; + +export function getColorForAgentStatus(agentStatus: ActionAgentStatus): string { + switch (agentStatus) { + case 'success': + return colorToHexMap.secondary; + case 'pending': + return colorToHexMap.default; + case 'failed': + return colorToHexMap.danger; + default: + throw new Error(`Unsupported action agent status ${agentStatus}`); + } +} + +export function getLabelForAgentStatus(agentStatus: ActionAgentStatus, expired: boolean): string { + switch (agentStatus) { + case 'success': + return i18n.translate('xpack.osquery.liveQueryActionResults.summary.successfulLabelText', { + defaultMessage: 'Successful', + }); + case 'pending': + return expired + ? i18n.translate('xpack.osquery.liveQueryActionResults.summary.expiredLabelText', { + defaultMessage: 'Expired', + }) + : i18n.translate('xpack.osquery.liveQueryActionResults.summary.pendingLabelText', { + defaultMessage: 'Not yet responded', + }); + case 'failed': + return i18n.translate('xpack.osquery.liveQueryActionResults.summary.failedLabelText', { + defaultMessage: 'Failed', + }); + default: + throw new Error(`Unsupported action agent status ${agentStatus}`); + } +} diff --git a/x-pack/plugins/osquery/public/action_results/types.ts b/x-pack/plugins/osquery/public/action_results/types.ts new file mode 100644 index 0000000000000..ce9415986ba02 --- /dev/null +++ b/x-pack/plugins/osquery/public/action_results/types.ts @@ -0,0 +1,8 @@ +/* + * 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 ActionAgentStatus = 'success' | 'pending' | 'failed'; diff --git a/x-pack/plugins/osquery/public/action_results/use_action_results.ts b/x-pack/plugins/osquery/public/action_results/use_action_results.ts index ab69bf86dc326..29bff0819956a 100644 --- a/x-pack/plugins/osquery/public/action_results/use_action_results.ts +++ b/x-pack/plugins/osquery/public/action_results/use_action_results.ts @@ -83,7 +83,7 @@ export const useActionResults = ({ const totalResponded = // @ts-expect-error update types - responseData.rawResponse?.aggregations?.aggs.responses_by_action_id?.doc_count; + responseData.rawResponse?.aggregations?.aggs.responses_by_action_id?.doc_count ?? 0; const aggsBuckets = // @ts-expect-error update types responseData.rawResponse?.aggregations?.aggs.responses_by_action_id?.responses.buckets; @@ -120,7 +120,7 @@ export const useActionResults = ({ failed: 0, }, }, - refetchInterval: isLive ? 1000 : false, + refetchInterval: isLive ? 5000 : false, keepPreviousData: true, enabled: !skip && !!agentIds?.length, onSuccess: () => setErrorToast(), diff --git a/x-pack/plugins/osquery/public/actions/actions_table.tsx b/x-pack/plugins/osquery/public/actions/actions_table.tsx index 0ee928ad8aa14..045c1f67b070d 100644 --- a/x-pack/plugins/osquery/public/actions/actions_table.tsx +++ b/x-pack/plugins/osquery/public/actions/actions_table.tsx @@ -9,6 +9,7 @@ import { isArray } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiBasicTable, EuiButtonIcon, EuiCodeBlock, formatDate } from '@elastic/eui'; import React, { useState, useCallback, useMemo } from 'react'; +import { useHistory } from 'react-router-dom'; import { useAllActions } from './use_all_actions'; import { Direction } from '../../common/search_strategy'; @@ -27,6 +28,7 @@ const ActionTableResultsButton = React.memo(({ ac ActionTableResultsButton.displayName = 'ActionTableResultsButton'; const ActionsTableComponent = () => { + const { push } = useHistory(); const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(20); @@ -67,6 +69,16 @@ const ActionsTableComponent = () => { [] ); + const handlePlayClick = useCallback( + (item) => + push('/live_queries/new', { + form: { + query: item._source?.data?.query, + }, + }), + [push] + ); + const columns = useMemo( () => [ { @@ -106,6 +118,11 @@ const ActionsTableComponent = () => { defaultMessage: 'View details', }), actions: [ + { + type: 'icon', + icon: 'play', + onClick: handlePlayClick, + }, { render: renderActionsColumn, }, @@ -113,6 +130,7 @@ const ActionsTableComponent = () => { }, ], [ + handlePlayClick, renderActionsColumn, renderAgentsColumn, renderCreatedByColumn, @@ -135,6 +153,7 @@ const ActionsTableComponent = () => { = ({ policyId } const href = useMemo( () => getUrlForApp(PLUGIN_ID, { - path: `#` + pagePathGetters.policy_details({ policyId }), + path: `#` + pagePathGetters.policy_details({ policyId })[1], }), [getUrlForApp, policyId] ); @@ -38,7 +38,7 @@ const AgentsPolicyLinkComponent: React.FC = ({ policyId } event.preventDefault(); return navigateToApp(PLUGIN_ID, { - path: `#` + pagePathGetters.policy_details({ policyId }), + path: `#` + pagePathGetters.policy_details({ policyId })[1], }); } }, diff --git a/x-pack/plugins/osquery/public/agent_policies/use_agent_policies.ts b/x-pack/plugins/osquery/public/agent_policies/use_agent_policies.ts index 6f87610667198..c51f2d2f44a5c 100644 --- a/x-pack/plugins/osquery/public/agent_policies/use_agent_policies.ts +++ b/x-pack/plugins/osquery/public/agent_policies/use_agent_policies.ts @@ -30,7 +30,6 @@ export const useAgentPolicies = () => { }), { initialData: { items: [], total: 0, page: 1, perPage: 100 }, - placeholderData: [], keepPreviousData: true, select: (response) => response.items, onSuccess: () => setErrorToast(), diff --git a/x-pack/plugins/osquery/public/agents/agent_id_to_name.tsx b/x-pack/plugins/osquery/public/agents/agent_id_to_name.tsx new file mode 100644 index 0000000000000..6db21c028ece8 --- /dev/null +++ b/x-pack/plugins/osquery/public/agents/agent_id_to_name.tsx @@ -0,0 +1,37 @@ +/* + * 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 { EuiLink } from '@elastic/eui'; +import React from 'react'; + +import { useAgentDetails } from './use_agent_details'; +import { PLUGIN_ID } from '../../../fleet/common'; +import { pagePathGetters } from '../../../fleet/public'; +import { useKibana } from '../common/lib/kibana'; + +interface AgentIdToNameProps { + agentId: string; +} + +const AgentIdToNameComponent: React.FC = ({ agentId }) => { + const getUrlForApp = useKibana().services.application.getUrlForApp; + const { data } = useAgentDetails({ agentId }); + + return ( + + {data?.item.local_metadata.host.name ?? agentId} + + ); +}; + +export const AgentIdToName = React.memo(AgentIdToNameComponent); diff --git a/x-pack/plugins/osquery/public/agents/agents_table.tsx b/x-pack/plugins/osquery/public/agents/agents_table.tsx index 7e8f49c051614..53e2ce1d53420 100644 --- a/x-pack/plugins/osquery/public/agents/agents_table.tsx +++ b/x-pack/plugins/osquery/public/agents/agents_table.tsx @@ -21,7 +21,12 @@ import { generateAgentSelection, } from './helpers'; -import { SELECT_AGENT_LABEL, generateSelectedAgentsMessage } from './translations'; +import { + SELECT_AGENT_LABEL, + generateSelectedAgentsMessage, + ALL_AGENTS_LABEL, + AGENT_POLICY_LABEL, +} from './translations'; import { AGENT_GROUP_KEY, @@ -72,8 +77,17 @@ const AgentsTableComponent: React.FC = ({ agentSelection, onCh useEffect(() => { if (agentSelection && !defaultValueInitialized.current && options.length) { - if (agentSelection.policiesSelected) { - const policyOptions = find(['label', 'Policy'], options); + if (agentSelection.allAgentsSelected) { + const allAgentsOptions = find(['label', ALL_AGENTS_LABEL], options); + + if (allAgentsOptions?.options) { + setSelectedOptions(allAgentsOptions.options); + defaultValueInitialized.current = true; + } + } + + if (agentSelection.policiesSelected.length) { + const policyOptions = find(['label', AGENT_POLICY_LABEL], options); if (policyOptions) { const defaultOptions = policyOptions.options?.filter((option) => @@ -82,12 +96,12 @@ const AgentsTableComponent: React.FC = ({ agentSelection, onCh if (defaultOptions?.length) { setSelectedOptions(defaultOptions); + defaultValueInitialized.current = true; } - defaultValueInitialized.current = true; } } } - }, [agentSelection, options]); + }, [agentSelection, options, selectedOptions]); useEffect(() => { // update the groups when groups or agents have changed diff --git a/x-pack/plugins/osquery/public/agents/use_agent_details.ts b/x-pack/plugins/osquery/public/agents/use_agent_details.ts new file mode 100644 index 0000000000000..1a0663812dec3 --- /dev/null +++ b/x-pack/plugins/osquery/public/agents/use_agent_details.ts @@ -0,0 +1,36 @@ +/* + * 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 { useQuery } from 'react-query'; + +import { GetOneAgentResponse, agentRouteService } from '../../../fleet/common'; +import { useErrorToast } from '../common/hooks/use_error_toast'; +import { useKibana } from '../common/lib/kibana'; + +interface UseAgentDetails { + agentId: string; +} + +export const useAgentDetails = ({ agentId }: UseAgentDetails) => { + const { http } = useKibana().services; + const setErrorToast = useErrorToast(); + return useQuery( + ['agentDetails', agentId], + () => http.get(agentRouteService.getInfoPath(agentId)), + { + enabled: agentId.length > 0, + onSuccess: () => setErrorToast(), + onError: (error) => + setErrorToast(error as Error, { + title: i18n.translate('xpack.osquery.agentDetails.fetchError', { + defaultMessage: 'Error while fetching agent details', + }), + }), + } + ); +}; diff --git a/x-pack/plugins/osquery/public/agents/use_all_agents.ts b/x-pack/plugins/osquery/public/agents/use_all_agents.ts index 30ba4d2f57907..cda15cc805437 100644 --- a/x-pack/plugins/osquery/public/agents/use_all_agents.ts +++ b/x-pack/plugins/osquery/public/agents/use_all_agents.ts @@ -38,7 +38,7 @@ export const useAllAgents = ( let kuery = `last_checkin_status: online and (${policyFragment})`; if (searchValue) { - kuery += `and (local_metadata.host.hostname:*${searchValue}* or local_metadata.elastic.agent.id:*${searchValue}*)`; + kuery += ` and (local_metadata.host.hostname:*${searchValue}* or local_metadata.elastic.agent.id:*${searchValue}*)`; } return http.get(agentRouteService.getListPath(), { diff --git a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx index 28d69a6a7b15a..63036f5f693f7 100644 --- a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx +++ b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx @@ -57,7 +57,7 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo< return getUrlForApp(PLUGIN_ID, { path: `#` + - pagePathGetters.policy_details({ policyId: policy?.policy_id }) + + pagePathGetters.policy_details({ policyId: policy?.policy_id })[1] + '?openEnrollmentFlyout=true', }); }, [getUrlForApp, policy?.policy_id]); diff --git a/x-pack/plugins/osquery/public/index.ts b/x-pack/plugins/osquery/public/index.ts index f0e956b64ee06..fadd61cce85ef 100644 --- a/x-pack/plugins/osquery/public/index.ts +++ b/x-pack/plugins/osquery/public/index.ts @@ -13,4 +13,4 @@ import { OsqueryPlugin } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { return new OsqueryPlugin(initializerContext); } -export { OsqueryPluginSetup, OsqueryPluginStart } from './types'; +export type { OsqueryPluginSetup, OsqueryPluginStart } from './types'; diff --git a/x-pack/plugins/osquery/public/live_queries/agent_results/index.tsx b/x-pack/plugins/osquery/public/live_queries/agent_results/index.tsx deleted file mode 100644 index d1ef18e2e12ea..0000000000000 --- a/x-pack/plugins/osquery/public/live_queries/agent_results/index.tsx +++ /dev/null @@ -1,30 +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 { EuiCodeBlock, EuiSpacer } from '@elastic/eui'; -import React from 'react'; -import { useParams } from 'react-router-dom'; - -import { useActionDetails } from '../../actions/use_action_details'; -import { ResultsTable } from '../../results/results_table'; - -const QueryAgentResultsComponent = () => { - const { actionId, agentId } = useParams<{ actionId: string; agentId: string }>(); - const { data } = useActionDetails({ actionId }); - - return ( - <> - - {data?.actionDetails._source?.data?.query} - - - - - ); -}; - -export const QueryAgentResults = React.memo(QueryAgentResultsComponent); diff --git a/x-pack/plugins/osquery/public/live_queries/form/index.tsx b/x-pack/plugins/osquery/public/live_queries/form/index.tsx index 9e952810e3352..8654a74fecfb4 100644 --- a/x-pack/plugins/osquery/public/live_queries/form/index.tsx +++ b/x-pack/plugins/osquery/public/live_queries/form/index.tsx @@ -18,6 +18,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useCallback, useMemo, useState } from 'react'; import { useMutation } from 'react-query'; +import deepMerge from 'deepmerge'; import { UseField, Form, FormData, useForm, useFormData, FIELD_TYPES } from '../../shared_imports'; import { AgentsTableField } from './agents_table_field'; @@ -33,12 +34,19 @@ const FORM_ID = 'liveQueryForm'; export const MAX_QUERY_LENGTH = 2000; +const GhostFormField = () => <>; + interface LiveQueryFormProps { + agentId?: string | undefined; defaultValue?: Partial | undefined; onSuccess?: () => void; } -const LiveQueryFormComponent: React.FC = ({ defaultValue, onSuccess }) => { +const LiveQueryFormComponent: React.FC = ({ + agentId, + defaultValue, + onSuccess, +}) => { const { http } = useKibana().services; const [showSavedQueryFlyout, setShowSavedQueryFlyout] = useState(false); const setErrorToast = useErrorToast(); @@ -71,8 +79,6 @@ const LiveQueryFormComponent: React.FC = ({ defaultValue, on } ); - const expirationDate = useMemo(() => new Date(data?.actions[0].expiration), [data?.actions]); - const formSchema = { query: { type: FIELD_TYPES.TEXT, @@ -100,9 +106,18 @@ const LiveQueryFormComponent: React.FC = ({ defaultValue, on options: { stripEmptyFields: false, }, - defaultValue: defaultValue ?? { - query: '', - }, + defaultValue: deepMerge( + { + agentSelection: { + agents: [], + allAgentsSelected: false, + platformsSelected: [], + policiesSelected: [], + }, + query: '', + }, + defaultValue ?? {} + ), }); const { submit } = form; @@ -147,6 +162,59 @@ const LiveQueryFormComponent: React.FC = ({ defaultValue, on const flyoutFormDefaultValue = useMemo(() => ({ query }), [query]); + const queryFieldStepContent = useMemo( + () => ( + <> + + + + {!agentId && ( + + + + + + )} + + + + + + + + ), + [ + agentId, + agentSelected, + handleShowSaveQueryFlout, + queryComponentProps, + queryValueProvided, + resultsStatus, + submit, + ] + ); + + const resultsStepContent = useMemo( + () => + actionId ? ( + + ) : null, + [actionId, agentIds, data?.actions] + ); + const formSteps: EuiContainedStepProps[] = useMemo( () => [ { @@ -160,73 +228,34 @@ const LiveQueryFormComponent: React.FC = ({ defaultValue, on title: i18n.translate('xpack.osquery.liveQueryForm.steps.queryStepHeading', { defaultMessage: 'Enter query', }), - children: ( - <> - - - - - - - - - - - - - - - - ), + children: queryFieldStepContent, status: queryStatus, }, { title: i18n.translate('xpack.osquery.liveQueryForm.steps.resultsStepHeading', { defaultMessage: 'Check results', }), - children: actionId ? ( - - ) : null, + children: resultsStepContent, status: resultsStatus, }, ], - [ - actionId, - agentIds, - agentSelected, - handleShowSaveQueryFlout, - queryComponentProps, - queryStatus, - queryValueProvided, - expirationDate, - resultsStatus, - submit, - ] + [agentSelected, queryFieldStepContent, queryStatus, resultsStepContent, resultsStatus] + ); + + const singleAgentForm = useMemo( + () => ( + + + {queryFieldStepContent} + {resultsStepContent} + + ), + [queryFieldStepContent, resultsStepContent] ); return ( <> -
- - +
{agentId ? singleAgentForm : } {showSavedQueryFlyout ? ( = ({ disabled, field }) => { const { value, setValue, errors } = field; const error = errors[0]?.message; + const savedQueriesDropdownRef = useRef(null); const handleSavedQueryChange = useCallback( (savedQuery) => { - setValue(savedQuery.query); + setValue(savedQuery?.query ?? ''); }, [setValue] ); const handleEditorChange = useCallback( (newValue) => { + savedQueriesDropdownRef.current?.clearSelection(); setValue(newValue); }, [setValue] @@ -39,7 +44,11 @@ const LiveQueryQueryFieldComponent: React.FC = ({ disa return ( <> - + }> diff --git a/x-pack/plugins/osquery/public/plugin.ts b/x-pack/plugins/osquery/public/plugin.ts index 631f3adba4c47..12f9025e406db 100644 --- a/x-pack/plugins/osquery/public/plugin.ts +++ b/x-pack/plugins/osquery/public/plugin.ts @@ -31,6 +31,7 @@ import { LazyOsqueryManagedPolicyEditExtension, LazyOsqueryManagedCustomButtonExtension, } from './fleet_integration'; +import { getLazyOsqueryAction } from './shared_components'; export function toggleOsqueryPlugin( updater$: Subject, @@ -160,7 +161,14 @@ export class OsqueryPlugin implements Plugin = ({ - actionId, - agentIds, - expirationDate, - endDate, - isLive, - startDate, -}) => { - const tabs = useMemo( - () => [ - { - id: 'status', - name: 'Status', - content: ( - <> - - - - ), - }, - { - id: 'results', - name: 'Results', - content: ( - <> - - - - ), - }, - ], - [actionId, agentIds, endDate, isLive, startDate, expirationDate] - ); - - return ( - - ); -}; - -export const ResultTabs = React.memo(ResultTabsComponent); diff --git a/x-pack/plugins/osquery/public/results/results_table.tsx b/x-pack/plugins/osquery/public/results/results_table.tsx index 6ff60d30d23bf..d82737ab51e7c 100644 --- a/x-pack/plugins/osquery/public/results/results_table.tsx +++ b/x-pack/plugins/osquery/public/results/results_table.tsx @@ -14,6 +14,8 @@ import { EuiDataGridColumn, EuiLink, EuiLoadingContent, + EuiProgress, + EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { createContext, useEffect, useState, useCallback, useContext, useMemo } from 'react'; @@ -37,17 +39,16 @@ interface ResultsTableComponentProps { selectedAgent?: string; agentIds?: string[]; endDate?: string; - isLive?: boolean; startDate?: string; } const ResultsTableComponent: React.FC = ({ actionId, agentIds, - isLive, startDate, endDate, }) => { + const [isLive, setIsLive] = useState(true); const { // @ts-expect-error update types data: { aggregations }, @@ -60,13 +61,13 @@ const ResultsTableComponent: React.FC = ({ sortField: '@timestamp', isLive, }); - + const expired = useMemo(() => (!endDate ? false : new Date(endDate) < new Date()), [endDate]); const { getUrlForApp } = useKibana().services.application; const getFleetAppUrl = useCallback( (agentId) => getUrlForApp('fleet', { - path: `#` + pagePathGetters.agent_details({ agentId }), + path: `#` + pagePathGetters.agent_details({ agentId })[1], }), [getUrlForApp] ); @@ -216,29 +217,56 @@ const ResultsTableComponent: React.FC = ({ [actionId, endDate, startDate] ); - if (!aggregations.totalResponded) { - return ; - } + useEffect( + () => + setIsLive(() => { + if (!agentIds?.length || expired) return false; + + const uniqueAgentsRepliedCount = + // @ts-expect-error-type + allResultsData?.rawResponse.aggregations?.unique_agents.value ?? 0; + + return !!(uniqueAgentsRepliedCount !== agentIds?.length - aggregations.failed); + }), + [ + agentIds?.length, + aggregations.failed, + // @ts-expect-error-type + allResultsData?.rawResponse.aggregations?.unique_agents.value, + expired, + ] + ); - if (aggregations.totalResponded && isFetched && !allResultsData?.edges.length) { - return ; + if (!isFetched) { + return ; } return ( - // @ts-expect-error update types - - - + <> + {isLive && } + + {isFetched && !allResultsData?.edges.length ? ( + <> + + + + ) : ( + // @ts-expect-error update types + + + + )} + ); }; diff --git a/x-pack/plugins/osquery/public/results/translations.ts b/x-pack/plugins/osquery/public/results/translations.ts index 8e77e78ec76e2..e4f71d818f01d 100644 --- a/x-pack/plugins/osquery/public/results/translations.ts +++ b/x-pack/plugins/osquery/public/results/translations.ts @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; export const generateEmptyDataMessage = (agentsResponded: number): string => { return i18n.translate('xpack.osquery.results.multipleAgentsResponded', { defaultMessage: - '{agentsResponded, plural, one {# agent has} other {# agents have}} responded, but no osquery data has been reported.', + '{agentsResponded, plural, one {# agent has} other {# agents have}} responded, no osquery data has been reported.', values: { agentsResponded }, }); }; diff --git a/x-pack/plugins/osquery/public/results/use_all_results.ts b/x-pack/plugins/osquery/public/results/use_all_results.ts index 1121898410278..a13fceedfa07a 100644 --- a/x-pack/plugins/osquery/public/results/use_all_results.ts +++ b/x-pack/plugins/osquery/public/results/use_all_results.ts @@ -78,7 +78,7 @@ export const useAllResults = ({ }; }, { - refetchInterval: isLive ? 1000 : false, + refetchInterval: isLive ? 5000 : false, enabled: !skip, onSuccess: () => setErrorToast(), onError: (error: Error) => diff --git a/x-pack/plugins/osquery/public/routes/live_queries/details/index.tsx b/x-pack/plugins/osquery/public/routes/live_queries/details/index.tsx index e4f1bb447a15a..02f5c8b6fb2a5 100644 --- a/x-pack/plugins/osquery/public/routes/live_queries/details/index.tsx +++ b/x-pack/plugins/osquery/public/routes/live_queries/details/index.tsx @@ -6,54 +6,24 @@ */ import { get } from 'lodash'; -import { - EuiButtonEmpty, - EuiTextColor, - EuiFlexGroup, - EuiFlexItem, - EuiCodeBlock, - EuiSpacer, - EuiDescriptionList, - EuiDescriptionListTitle, - EuiDescriptionListDescription, -} from '@elastic/eui'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiCodeBlock, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useMemo } from 'react'; import { useParams } from 'react-router-dom'; -import styled from 'styled-components'; -import { Direction } from '../../../../common/search_strategy'; import { useRouterNavigate } from '../../../common/lib/kibana'; import { WithHeaderLayout } from '../../../components/layouts'; -import { useActionResults } from '../../../action_results/use_action_results'; import { useActionDetails } from '../../../actions/use_action_details'; import { ResultTabs } from '../../saved_queries/edit/tabs'; import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs'; import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge'; -const Divider = styled.div` - width: 0; - height: 100%; - border-left: ${({ theme }) => theme.eui.euiBorderThin}; -`; - const LiveQueryDetailsPageComponent = () => { const { actionId } = useParams<{ actionId: string }>(); useBreadcrumbs('live_query_details', { liveQueryId: actionId }); const liveQueryListProps = useRouterNavigate('live_queries'); const { data } = useActionDetails({ actionId }); - const expirationDate = useMemo(() => new Date(data?.actionDetails._source.expiration), [ - data?.actionDetails, - ]); - const expired = useMemo(() => expirationDate < new Date(), [expirationDate]); - const { data: actionResultsData } = useActionResults({ - actionId, - activePage: 0, - limit: 0, - direction: Direction.asc, - sortField: '@timestamp', - }); const LeftColumn = useMemo( () => ( @@ -82,72 +52,14 @@ const LiveQueryDetailsPageComponent = () => { [liveQueryListProps] ); - const failed = useMemo(() => { - let result = actionResultsData?.aggregations.failed; - if (expired) { - result = '-'; - if (data?.actionDetails?.fields?.agents && actionResultsData?.aggregations) { - result = - data.actionDetails.fields.agents.length - actionResultsData.aggregations.successful; - } - } - return result; - }, [expired, actionResultsData?.aggregations, data?.actionDetails?.fields?.agents]); - - const RightColumn = useMemo( - () => ( - - - <> - - - - - - {/* eslint-disable-next-line react-perf/jsx-no-new-object-as-prop */} - - - - - - {data?.actionDetails?.fields?.agents?.length ?? '0'} - - - - - - - - {/* eslint-disable-next-line react-perf/jsx-no-new-object-as-prop */} - - - - - - {failed} - - - - - ), - [data?.actionDetails?.fields?.agents?.length, failed] - ); - return ( - + {data?.actionDetails._source?.data?.query} { useBreadcrumbs('live_query_new'); + const { replace } = useHistory(); const location = useLocation(); const liveQueryListProps = useRouterNavigate('live_queries'); const formDefaultValue = useMemo(() => { const queryParams = qs.parse(location.search); + if (location.state?.form.query) { + replace({ state: null }); + return { query: location.state?.form.query }; + } + if (queryParams?.agentPolicyId) { return { agentSelection: { @@ -37,7 +43,7 @@ const NewLiveQueryPageComponent = () => { } return undefined; - }, [location.search]); + }, [location.search, location.state, replace]); const LeftColumn = useMemo( () => ( diff --git a/x-pack/plugins/osquery/public/routes/saved_queries/edit/index.tsx b/x-pack/plugins/osquery/public/routes/saved_queries/edit/index.tsx index 4aaf8e4fc4fc3..5bdba133fad72 100644 --- a/x-pack/plugins/osquery/public/routes/saved_queries/edit/index.tsx +++ b/x-pack/plugins/osquery/public/routes/saved_queries/edit/index.tsx @@ -33,7 +33,7 @@ const EditSavedQueryPageComponent = () => { const updateSavedQueryMutation = useUpdateSavedQuery({ savedQueryId }); const deleteSavedQueryMutation = useDeleteSavedQuery({ savedQueryId }); - useBreadcrumbs('saved_query_edit', { savedQueryId: savedQueryDetails?.attributes?.id ?? '' }); + useBreadcrumbs('saved_query_edit', { savedQueryName: savedQueryDetails?.attributes?.id ?? '' }); const handleCloseDeleteConfirmationModal = useCallback(() => { setIsDeleteModalVisible(false); diff --git a/x-pack/plugins/osquery/public/routes/saved_queries/edit/tabs.tsx b/x-pack/plugins/osquery/public/routes/saved_queries/edit/tabs.tsx index 1946cd6dd3450..1f56daaa3bdb5 100644 --- a/x-pack/plugins/osquery/public/routes/saved_queries/edit/tabs.tsx +++ b/x-pack/plugins/osquery/public/routes/saved_queries/edit/tabs.tsx @@ -10,12 +10,11 @@ import React, { useMemo } from 'react'; import { ResultsTable } from '../../../results/results_table'; import { ActionResultsSummary } from '../../../action_results/action_results_summary'; +import { ActionAgentsStatus } from '../../../action_results/action_agents_status'; interface ResultTabsProps { actionId: string; agentIds?: string[]; - expirationDate: Date; - isLive?: boolean; startDate?: string; endDate?: string; } @@ -24,8 +23,6 @@ const ResultTabsComponent: React.FC = ({ actionId, agentIds, endDate, - expirationDate, - isLive, startDate, }) => { const tabs = useMemo( @@ -39,7 +36,6 @@ const ResultTabsComponent: React.FC = ({ @@ -55,23 +51,26 @@ const ResultTabsComponent: React.FC = ({ ), }, ], - [actionId, agentIds, endDate, expirationDate, isLive, startDate] + [actionId, agentIds, endDate, startDate] ); return ( - + <> + + + + ); }; diff --git a/x-pack/plugins/osquery/public/routes/saved_queries/list/index.tsx b/x-pack/plugins/osquery/public/routes/saved_queries/list/index.tsx index 7e8e8e543dfab..8738c06d06597 100644 --- a/x-pack/plugins/osquery/public/routes/saved_queries/list/index.tsx +++ b/x-pack/plugins/osquery/public/routes/saved_queries/list/index.tsx @@ -16,6 +16,7 @@ import { import React, { useCallback, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { useHistory } from 'react-router-dom'; import { SavedObject } from 'kibana/public'; import { WithHeaderLayout } from '../../../components/layouts'; @@ -51,6 +52,7 @@ const EditButton = React.memo(EditButtonComponent); const SavedQueriesPageComponent = () => { useBreadcrumbs('saved_queries'); + const { push } = useHistory(); const newQueryLinkProps = useRouterNavigate('saved_queries/new'); const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(10); @@ -59,21 +61,15 @@ const SavedQueriesPageComponent = () => { const { data } = useSavedQueries({ isLive: true }); - // const handlePlayClick = useCallback( - // (item) => - // push({ - // search: qs.stringify({ - // tab: 'live_query', - // }), - // state: { - // query: { - // id: item.id, - // query: item.attributes.query, - // }, - // }, - // }), - // [push] - // ); + const handlePlayClick = useCallback( + (item) => + push('/live_queries/new', { + form: { + savedQueryId: item.id, + }, + }), + [push] + ); const renderEditAction = useCallback( (item: SavedObject<{ name: string }>) => ( @@ -96,45 +92,53 @@ const SavedQueriesPageComponent = () => { () => [ { field: 'attributes.id', - name: 'Query ID', + name: i18n.translate('xpack.osquery.savedQueries.table.queryIdColumnTitle', { + defaultMessage: 'Query ID', + }), sortable: true, truncateText: true, }, { field: 'attributes.description', - name: 'Description', + name: i18n.translate('xpack.osquery.savedQueries.table.descriptionColumnTitle', { + defaultMessage: 'Description', + }), sortable: true, truncateText: true, }, { field: 'attributes.created_by', - name: 'Created by', + name: i18n.translate('xpack.osquery.savedQueries.table.createdByColumnTitle', { + defaultMessage: 'Created by', + }), sortable: true, truncateText: true, }, { field: 'attributes.updated_at', - name: 'Last updated at', + name: i18n.translate('xpack.osquery.savedQueries.table.updatedAtColumnTitle', { + defaultMessage: 'Last updated at', + }), sortable: (item: SavedObject<{ updated_at: string }>) => item.attributes.updated_at ? Date.parse(item.attributes.updated_at) : 0, truncateText: true, render: renderUpdatedAt, }, { - name: 'Actions', + name: i18n.translate('xpack.osquery.savedQueries.table.actionsColumnTitle', { + defaultMessage: 'Actions', + }), actions: [ - // { - // name: 'Live query', - // description: 'Run live query', - // type: 'icon', - // icon: 'play', - // onClick: handlePlayClick, - // }, + { + type: 'icon', + icon: 'play', + onClick: handlePlayClick, + }, { render: renderEditAction }, ], }, ], - [renderEditAction, renderUpdatedAt] + [handlePlayClick, renderEditAction, renderUpdatedAt] ); const onTableChange = useCallback(({ page = {}, sort = {} }) => { diff --git a/x-pack/plugins/osquery/public/saved_queries/form/index.tsx b/x-pack/plugins/osquery/public/saved_queries/form/index.tsx index 174227eb5e6e5..9bbf847c4d2a0 100644 --- a/x-pack/plugins/osquery/public/saved_queries/form/index.tsx +++ b/x-pack/plugins/osquery/public/saved_queries/form/index.tsx @@ -7,6 +7,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle, EuiText } from '@elastic/eui'; import React from 'react'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { ALL_OSQUERY_VERSIONS_OPTIONS } from '../../scheduled_query_groups/queries/constants'; @@ -57,7 +58,12 @@ const SavedQueryFormComponent = () => ( euiFieldProps={{ noSuggestions: false, singleSelection: { asPlainText: true }, - placeholder: ALL_OSQUERY_VERSIONS_OPTIONS[0].label, + placeholder: i18n.translate( + 'xpack.osquery.scheduledQueryGroup.queriesTable.osqueryVersionAllLabel', + { + defaultMessage: 'ALL', + } + ), options: ALL_OSQUERY_VERSIONS_OPTIONS, onCreateOption: undefined, }} diff --git a/x-pack/plugins/osquery/public/saved_queries/saved_queries_dropdown.tsx b/x-pack/plugins/osquery/public/saved_queries/saved_queries_dropdown.tsx index e30954a695b2d..fc7cee2fc804c 100644 --- a/x-pack/plugins/osquery/public/saved_queries/saved_queries_dropdown.tsx +++ b/x-pack/plugins/osquery/public/saved_queries/saved_queries_dropdown.tsx @@ -6,45 +6,83 @@ */ import { find } from 'lodash/fp'; -import { EuiCodeBlock, EuiFormRow, EuiComboBox, EuiText } from '@elastic/eui'; -import React, { useCallback, useState } from 'react'; +import { EuiCodeBlock, EuiFormRow, EuiComboBox, EuiTextColor } from '@elastic/eui'; +import React, { + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useState, +} from 'react'; import { SimpleSavedObject } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { useHistory, useLocation } from 'react-router-dom'; +import styled from 'styled-components'; import { useSavedQueries } from './use_saved_queries'; +export interface SavedQueriesDropdownRef { + clearSelection: () => void; +} + +const TextTruncate = styled.div` + overflow: hidden; + text-overflow: ellipsis; +`; + +const StyledEuiCodeBlock = styled(EuiCodeBlock)` + .euiCodeBlock__line { + white-space: nowrap; + } +`; + interface SavedQueriesDropdownProps { disabled?: boolean; onChange: ( - value: SimpleSavedObject<{ - id: string; - description?: string | undefined; - query: string; - }>['attributes'] + value: + | SimpleSavedObject<{ + id: string; + description?: string | undefined; + query: string; + }>['attributes'] + | null ) => void; } -const SavedQueriesDropdownComponent: React.FC = ({ - disabled, - onChange, -}) => { +const SavedQueriesDropdownComponent = forwardRef< + SavedQueriesDropdownRef, + SavedQueriesDropdownProps +>(({ disabled, onChange }, ref) => { + const { replace } = useHistory(); + const location = useLocation(); const [selectedOptions, setSelectedOptions] = useState([]); const { data } = useSavedQueries({}); - const queryOptions = - data?.savedObjects?.map((savedQuery) => ({ - label: savedQuery.attributes.id ?? '', - value: { - id: savedQuery.attributes.id, - description: savedQuery.attributes.description, - query: savedQuery.attributes.query, - }, - })) ?? []; + const queryOptions = useMemo( + () => + data?.savedObjects?.map((savedQuery) => ({ + label: savedQuery.attributes.id ?? '', + value: { + savedObjectId: savedQuery.id, + id: savedQuery.attributes.id, + description: savedQuery.attributes.description, + query: savedQuery.attributes.query, + }, + })) ?? [], + [data?.savedObjects] + ); const handleSavedQueryChange = useCallback( (newSelectedOptions) => { + if (!newSelectedOptions.length) { + onChange(null); + setSelectedOptions(newSelectedOptions); + return; + } + const selectedSavedQuery = find( ['attributes.id', newSelectedOptions[0].value.id], data?.savedObjects @@ -62,17 +100,41 @@ const SavedQueriesDropdownComponent: React.FC = ({ ({ value }) => ( <> {value.id} - -

{value.description}

-
- - {value.query} - + + {value.description} + + + {value.query.split('\n').join(' ')} + ), [] ); + const clearSelection = useCallback(() => setSelectedOptions([]), []); + + useEffect(() => { + const savedQueryId = location.state?.form?.savedQueryId; + + if (savedQueryId) { + const savedQueryOption = find(['value.savedObjectId', savedQueryId], queryOptions); + + if (savedQueryOption) { + handleSavedQueryChange([savedQueryOption]); + } + + replace({ state: null }); + } + }, [handleSavedQueryChange, replace, location.state, queryOptions]); + + useImperativeHandle( + ref, + () => ({ + clearSelection, + }), + [clearSelection] + ); + return ( = ({ selectedOptions={selectedOptions} onChange={handleSavedQueryChange} renderOption={renderOption} - rowHeight={90} + rowHeight={110} /> ); -}; +}); export const SavedQueriesDropdown = React.memo(SavedQueriesDropdownComponent); diff --git a/x-pack/plugins/osquery/public/saved_queries/use_saved_queries.ts b/x-pack/plugins/osquery/public/saved_queries/use_saved_queries.ts index 324d4aace1647..bb5a73d9d50fa 100644 --- a/x-pack/plugins/osquery/public/saved_queries/use_saved_queries.ts +++ b/x-pack/plugins/osquery/public/saved_queries/use_saved_queries.ts @@ -40,7 +40,7 @@ export const useSavedQueries = ({ { keepPreviousData: true, // Refetch the data every 10 seconds - refetchInterval: isLive ? 10000 : false, + refetchInterval: isLive ? 5000 : false, } ); }; diff --git a/x-pack/plugins/osquery/public/saved_queries/use_update_saved_query.ts b/x-pack/plugins/osquery/public/saved_queries/use_update_saved_query.ts index 1260413676a4e..6f4aa51710811 100644 --- a/x-pack/plugins/osquery/public/saved_queries/use_update_saved_query.ts +++ b/x-pack/plugins/osquery/public/saved_queries/use_update_saved_query.ts @@ -56,7 +56,7 @@ export const useUpdateSavedQuery = ({ savedQueryId }: UseUpdateSavedQueryProps) i18n.translate('xpack.osquery.editSavedQuery.successToastMessageText', { defaultMessage: 'Successfully updated "{savedQueryName}" query', values: { - savedQueryName: payload.attributes?.name ?? '', + savedQueryName: payload.attributes?.id ?? '', }, }) ); diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx index b37c315849f60..95a31efeaf135 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx @@ -18,9 +18,10 @@ import { EuiFlexItem, EuiButtonEmpty, EuiButton, - EuiDescribedFormGroup, + EuiText, } from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { satisfies } from 'semver'; @@ -70,10 +71,14 @@ const QueryFlyoutComponent: React.FC = ({ [integrationPackageVersion] ); - const { submit, setFieldValue } = form; + const { submit, setFieldValue, reset } = form; const handleSetQueryValue = useCallback( (savedQuery) => { + if (!savedQuery) { + return reset(); + } + setFieldValue('id', savedQuery.id); setFieldValue('query', savedQuery.query); @@ -93,7 +98,7 @@ const QueryFlyoutComponent: React.FC = ({ setFieldValue('version', [savedQuery.version]); } }, - [isFieldSupported, setFieldValue] + [isFieldSupported, setFieldValue, reset] ); return ( @@ -128,10 +133,7 @@ const QueryFlyoutComponent: React.FC = ({ - Set heading level based on context} - description={'Will be wrapped in a small, subdued EuiText block.'} - > + = ({ + + + + + } // eslint-disable-next-line react-perf/jsx-no-new-object-as-prop euiFieldProps={{ isDisabled: !isFieldSupported, noSuggestions: false, singleSelection: { asPlainText: true }, - placeholder: ALL_OSQUERY_VERSIONS_OPTIONS[0].label, + placeholder: i18n.translate( + 'xpack.osquery.scheduledQueryGroup.queriesTable.osqueryVersionAllLabel', + { + defaultMessage: 'ALL', + } + ), options: ALL_OSQUERY_VERSIONS_OPTIONS, onCreateOption: undefined, }} @@ -160,7 +177,7 @@ const QueryFlyoutComponent: React.FC = ({ euiFieldProps={{ disabled: !isFieldSupported }} /> - + {!isFieldSupported ? ( diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/schema.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/schema.tsx index d8dbaad2f17e8..0b23ce924f930 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/schema.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/schema.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -65,14 +65,6 @@ export const formSchema = { defaultMessage="Minimum Osquery version" /> - - - - - ) as unknown) as string, validations: [], diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx index 36d15587086f2..01acf2dc0d826 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx @@ -258,7 +258,7 @@ const ViewResultsInDiscoverActionComponent: React.FC (props) => { + const OsqueryAction = lazy(() => import('./osquery_action')); + return ( + + + + ); +}; diff --git a/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx b/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx new file mode 100644 index 0000000000000..cf8a85cea244c --- /dev/null +++ b/x-pack/plugins/osquery/public/shared_components/osquery_action/index.tsx @@ -0,0 +1,99 @@ +/* + * 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 { EuiErrorBoundary, EuiLoadingContent } from '@elastic/eui'; +import React, { useEffect, useState } from 'react'; +import { QueryClientProvider } from 'react-query'; +import { KibanaContextProvider, useKibana } from '../../common/lib/kibana'; + +import { LiveQueryForm } from '../../live_queries/form'; +import { queryClient } from '../../query_client'; + +interface OsqueryActionProps { + hostId?: string | undefined; +} + +const OsqueryActionComponent: React.FC = ({ hostId }) => { + const [agentId, setAgentId] = useState(); + const { indexPatterns, search } = useKibana().services.data; + + useEffect(() => { + if (hostId) { + const findAgent = async () => { + const searchSource = await search.searchSource.create(); + const indexPattern = await indexPatterns.find('.fleet-agents'); + + searchSource.setField('index', indexPattern[0]); + searchSource.setField('filter', [ + { + meta: { + alias: null, + disabled: false, + negate: false, + key: 'local_metadata.host.id', + value: hostId, + }, + query: { + match_phrase: { + 'local_metadata.host.id': hostId, + }, + }, + }, + { + meta: { + alias: null, + disabled: false, + negate: false, + key: 'active', + value: 'true', + }, + query: { + match_phrase: { + active: 'true', + }, + }, + }, + ]); + + const response = await searchSource.fetch$().toPromise(); + + if (response.rawResponse.hits.hits.length && response.rawResponse.hits.hits[0]._id) { + setAgentId(response.rawResponse.hits.hits[0]._id); + } + }; + + findAgent(); + } + }); + + if (!agentId) { + return ; + } + + return ( + // eslint-disable-next-line react-perf/jsx-no-new-object-as-prop + + ); +}; + +export const OsqueryAction = React.memo(OsqueryActionComponent); + +// @ts-expect-error update types +const OsqueryActionWrapperComponent = ({ services, ...props }) => ( + + + + + + + +); + +const OsqueryActionWrapper = React.memo(OsqueryActionWrapperComponent); + +// eslint-disable-next-line import/no-default-export +export { OsqueryActionWrapper as default }; diff --git a/x-pack/plugins/osquery/public/types.ts b/x-pack/plugins/osquery/public/types.ts index 9a466dfc619b6..fd21b39d25504 100644 --- a/x-pack/plugins/osquery/public/types.ts +++ b/x-pack/plugins/osquery/public/types.ts @@ -16,11 +16,13 @@ import { TriggersAndActionsUIPublicPluginSetup, TriggersAndActionsUIPublicPluginStart, } from '../../triggers_actions_ui/public'; +import { getLazyOsqueryAction } from './shared_components'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface OsqueryPluginSetup {} -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface OsqueryPluginStart {} +export interface OsqueryPluginStart { + OsqueryAction?: ReturnType; +} export interface AppPluginStartDependencies { navigation: NavigationPublicPluginStart; diff --git a/x-pack/plugins/osquery/server/lib/saved_query/saved_object_mappings.ts b/x-pack/plugins/osquery/server/lib/saved_query/saved_object_mappings.ts index 537b6d7874ab8..5535d707cf5c0 100644 --- a/x-pack/plugins/osquery/server/lib/saved_query/saved_object_mappings.ts +++ b/x-pack/plugins/osquery/server/lib/saved_query/saved_object_mappings.ts @@ -24,7 +24,7 @@ export const savedQuerySavedObjectMappings: SavedObjectsType['mappings'] = { type: 'date', }, created_by: { - type: 'text', + type: 'keyword', }, platform: { type: 'keyword', @@ -36,7 +36,7 @@ export const savedQuerySavedObjectMappings: SavedObjectsType['mappings'] = { type: 'date', }, updated_by: { - type: 'text', + type: 'keyword', }, interval: { type: 'keyword', @@ -57,19 +57,19 @@ export const packSavedObjectMappings: SavedObjectsType['mappings'] = { type: 'text', }, name: { - type: 'text', + type: 'keyword', }, created_at: { type: 'date', }, created_by: { - type: 'text', + type: 'keyword', }, updated_at: { type: 'date', }, updated_by: { - type: 'text', + type: 'keyword', }, queries: { properties: { @@ -77,7 +77,7 @@ export const packSavedObjectMappings: SavedObjectsType['mappings'] = { type: 'keyword', }, interval: { - type: 'text', + type: 'keyword', }, }, }, diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/all/query.all_actions.dsl.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/all/query.all_actions.dsl.ts index 63b1b207f02e3..55bec687d3e2c 100644 --- a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/all/query.all_actions.dsl.ts +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/all/query.all_actions.dsl.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { estypes } from '@elastic/elasticsearch'; + import { ISearchRequestParams } from '../../../../../../../../../src/plugins/data/common'; import { AgentsRequestOptions } from '../../../../../../common/search_strategy'; // import { createQueryFilterClauses } from '../../../../../../common/utils/build_query'; @@ -24,10 +26,23 @@ export const buildActionsQuery = ({ body: { // query: { bool: { filter } }, query: { - term: { - type: { - value: 'INPUT_ACTION', - }, + bool: { + must: [ + { + term: { + type: { + value: 'INPUT_ACTION', + }, + }, + }, + { + term: { + input_type: { + value: 'osquery', + }, + }, + }, + ] as estypes.QueryDslQueryContainer[], }, }, from: cursorStart, diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/query.all_results.dsl.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/query.all_results.dsl.ts index b560fd3c364e9..406ff26991f0e 100644 --- a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/query.all_results.dsl.ts +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/query.all_results.dsl.ts @@ -47,12 +47,17 @@ export const buildResultsQuery = ({ size: 10000, }, }, + unique_agents: { + cardinality: { + field: 'elastic_agent.id', + }, + }, }, query: { bool: { filter } }, from: activePage * querySize, size: querySize, track_total_hits: true, - fields: agentId ? ['osquery.*'] : ['agent.*', 'osquery.*'], + fields: ['elastic_agent.*', 'agent.*', 'osquery.*'], sort: sort?.map((sortConfig) => ({ [sortConfig.field]: { diff --git a/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap b/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap index 9ce249aa32a1d..8007acad93e4b 100644 --- a/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap +++ b/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap @@ -424,7 +424,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -480,7 +480,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -522,7 +521,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -1489,7 +1487,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -1531,7 +1528,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -2512,7 +2508,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -2554,7 +2549,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -3582,7 +3576,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -3624,7 +3617,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -4685,7 +4677,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -4727,7 +4718,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -5755,7 +5745,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -5797,7 +5786,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -6825,7 +6813,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -6867,7 +6854,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -7895,7 +7881,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -7937,7 +7922,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -8965,7 +8949,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
@@ -9007,7 +8990,6 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTableCellContent euiTableCellContent--overflowingContent" >
{ILM_POLICY_NAME}, }} @@ -33,7 +33,10 @@ const i18nTexts = { buttonLabel: i18n.translate( 'xpack.reporting.listing.ilmPolicyCallout.migrateIndicesButtonLabel', { - defaultMessage: 'Migrate indices', + defaultMessage: 'Apply {ilmPolicyName} policy', + values: { + ilmPolicyName: ILM_POLICY_NAME, + }, } ), migrateErrorTitle: i18n.translate( @@ -45,7 +48,7 @@ const i18nTexts = { migrateSuccessTitle: i18n.translate( 'xpack.reporting.listing.ilmPolicyCallout.migrateIndicesSuccessTitle', { - defaultMessage: 'Successfully migrated reporting indices', + defaultMessage: 'Reporting policy active for all reporting indices', } ), }; diff --git a/x-pack/plugins/reporting/public/management/report_listing.tsx b/x-pack/plugins/reporting/public/management/report_listing.tsx index 749e42de526d3..dd41314b4883f 100644 --- a/x-pack/plugins/reporting/public/management/report_listing.tsx +++ b/x-pack/plugins/reporting/public/management/report_listing.tsx @@ -151,6 +151,7 @@ class ReportListingUi extends Component { return ( <> @@ -375,7 +376,7 @@ class ReportListingUi extends Component { }), render: (objectTitle: string, record: Job) => { return ( -
+
{objectTitle}
{record.object_type} diff --git a/x-pack/plugins/reporting/public/share_context_menu/__snapshots__/screen_capture_panel_content.test.tsx.snap b/x-pack/plugins/reporting/public/share_context_menu/__snapshots__/screen_capture_panel_content.test.tsx.snap index 01a8be98bc4be..83dc0c9e215b0 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/__snapshots__/screen_capture_panel_content.test.tsx.snap +++ b/x-pack/plugins/reporting/public/share_context_menu/__snapshots__/screen_capture_panel_content.test.tsx.snap @@ -312,9 +312,7 @@ exports[`ScreenCapturePanelContent properly renders a view with "canvas" layout onResize={[Function]} >
-
+
@@ -752,9 +750,7 @@ exports[`ScreenCapturePanelContent properly renders a view with "print" layout o onResize={[Function]} >
-
+
@@ -1064,9 +1060,7 @@ exports[`ScreenCapturePanelContent renders the default view properly 1`] = ` onResize={[Function]} >
-
+
diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index 1141437eae0ef..eb2abf4036c03 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -26,6 +26,18 @@ import { HeadlessChromiumDriver } from '../driver'; import { args } from './args'; import { Metrics, getMetrics } from './metrics'; +// Puppeteer type definitions do not match the documentation. +// See https://pptr.dev/#?product=Puppeteer&version=v8.0.0&show=api-puppeteerlaunchoptions +interface ReportingLaunchOptions extends puppeteer.LaunchOptions { + userDataDir?: string; + ignoreHTTPSErrors?: boolean; + args?: string[]; +} + +declare module 'puppeteer' { + function launch(options: ReportingLaunchOptions): Promise; +} + type BrowserConfig = CaptureConfig['browser']['chromium']; type ViewportConfig = CaptureConfig['viewport']; @@ -85,11 +97,12 @@ export class HeadlessChromiumDriverFactory { userDataDir: this.userDataDir, executablePath: this.binaryPath, ignoreHTTPSErrors: true, + handleSIGHUP: false, args: chromiumArgs, env: { TZ: browserTimezone, }, - } as puppeteer.LaunchOptions); + }); page = await browser.newPage(); devTools = await page.target().createCDPSession(); diff --git a/x-pack/plugins/rule_registry/README.md b/x-pack/plugins/rule_registry/README.md index 3fe6305a0d9f6..945b8f161eb84 100644 --- a/x-pack/plugins/rule_registry/README.md +++ b/x-pack/plugins/rule_registry/README.md @@ -27,9 +27,7 @@ On plugin setup, rule type producers can create the index template as follows: ```ts // get the FQN of the component template. All assets are prefixed with the configured `index` value, which is `.alerts` by default. -const componentTemplateName = plugins.ruleRegistry.getFullAssetName( - 'apm-mappings' -); +const componentTemplateName = plugins.ruleRegistry.getFullAssetName('apm-mappings'); // if write is disabled, don't install these templates if (!plugins.ruleRegistry.isWriteEnabled()) { @@ -73,14 +71,10 @@ await plugins.ruleRegistry.createOrUpdateComponentTemplate({ await plugins.ruleRegistry.createOrUpdateIndexTemplate({ name: plugins.ruleRegistry.getFullAssetName('apm-index-template'), body: { - index_patterns: [ - plugins.ruleRegistry.getFullAssetName('observability-apm*'), - ], + index_patterns: [plugins.ruleRegistry.getFullAssetName('observability-apm*')], composed_of: [ // Technical component template, required - plugins.ruleRegistry.getFullAssetName( - TECHNICAL_COMPONENT_TEMPLATE_NAME - ), + plugins.ruleRegistry.getFullAssetName(TECHNICAL_COMPONENT_TEMPLATE_NAME), componentTemplateName, ], }, @@ -107,8 +101,7 @@ await ruleDataClient.getWriter().bulk({ // to read data, simply call ruleDataClient.getReader().search: const response = await ruleDataClient.getReader().search({ body: { - query: { - }, + query: {}, size: 100, fields: ['*'], sort: { @@ -132,6 +125,7 @@ The following fields are defined in the technical field component template and s - `rule.name`: the name of the rule (as specified by the user). - `rule.category`: the name of the rule type (as defined by the rule type producer) - `kibana.rac.alert.producer`: the producer of the rule type. Usually a Kibana plugin. e.g., `APM`. +- `kibana.rac.alert.owner`: the feature which produced the alert. Usually a Kibana feature id like `apm`, `siem`... - `kibana.rac.alert.id`: the id of the alert, that is unique within the context of the rule execution it was created in. E.g., for a rule that monitors latency for all services in all environments, this might be `opbeans-java:production`. - `kibana.rac.alert.uuid`: the unique identifier for the alert during its lifespan. If an alert recovers (or closes), this identifier is re-generated when it is opened again. - `kibana.rac.alert.status`: the status of the alert. Can be `open` or `closed`. @@ -145,3 +139,16 @@ The following fields are defined in the technical field component template and s - `kibana.rac.alert.ancestors`: the array of ancestors (if any) for the alert. - `kibana.rac.alert.depth`: the depth of the alert in the ancestral tree (default 0). - `kibana.rac.alert.building_block_type`: the building block type of the alert (default undefined). + +# Alerts as data + +Alerts as data can be interacted with using the AlertsClient api found in `x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts` + +This api includes public methods such as + +[x] getFullAssetName +[x] getAlertsIndex +[x] get +[x] update +[ ] bulkUpdate (TODO) +[ ] find (TODO) diff --git a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts index a946e9523548c..6d70c581802c1 100644 --- a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts +++ b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts @@ -18,6 +18,7 @@ import { ALERT_UUID, EVENT_ACTION, EVENT_KIND, + OWNER, PRODUCER, RULE_CATEGORY, RULE_ID, @@ -40,6 +41,7 @@ export const technicalRuleFieldMap = { RULE_CATEGORY, TAGS ), + [OWNER]: { type: 'keyword' }, [PRODUCER]: { type: 'keyword' }, [ALERT_UUID]: { type: 'keyword' }, [ALERT_ID]: { type: 'keyword' }, diff --git a/x-pack/plugins/rule_registry/common/constants.ts b/x-pack/plugins/rule_registry/common/constants.ts new file mode 100644 index 0000000000000..72793b1087e7b --- /dev/null +++ b/x-pack/plugins/rule_registry/common/constants.ts @@ -0,0 +1,8 @@ +/* + * 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 BASE_RAC_ALERTS_API_PATH = '/internal/rac/alerts'; diff --git a/x-pack/plugins/rule_registry/docs/README.md b/x-pack/plugins/rule_registry/docs/README.md new file mode 100644 index 0000000000000..0eb2463005193 --- /dev/null +++ b/x-pack/plugins/rule_registry/docs/README.md @@ -0,0 +1,44 @@ +# Alerts as data Client API Docs + +This directory contains generated docs using `typedoc` for the alerts as data client (alerts client) API that can be called from other server +plugins. This README will describe how to generate a new version of these markdown docs in the event that new methods +or parameters are added. + +## TypeDoc Info + +See more info at: +and: for the markdown plugin + +## Install dependencies + +```bash +yarn global add typedoc typedoc-plugin-markdown +``` + +## Generate the docs + +```bash +cd x-pack/plugins/rule_registry/docs +npx typedoc --gitRemote upstream --options alerts_client_typedoc.json +``` + +After running the above commands the files in the `server` directory will be updated to match the new tsdocs. +If additional markdown directory should be created we can create a new typedoc configuration file and adjust the `out` +directory accordingly. + +## Troubleshooting + +This will use the global `tsc` so ensure typescript is installed globally and one of typescript version `3.9, 4.0, 4.1, 4.2`. + +``` +$ tsc --version +Version 4.2.4 +``` + +If you run into tsc errors that seem unrelated to the cases plugin try executing these commands before running `typedoc` + +```bash +cd +npx yarn kbn bootstrap +node scripts/build_ts_refs.js --clean --no-cache +``` diff --git a/x-pack/plugins/rule_registry/docs/alerts_client/alerts_client_api.md b/x-pack/plugins/rule_registry/docs/alerts_client/alerts_client_api.md new file mode 100644 index 0000000000000..b94a19f8e3f38 --- /dev/null +++ b/x-pack/plugins/rule_registry/docs/alerts_client/alerts_client_api.md @@ -0,0 +1,14 @@ +Alerts as data client API Interface + +# Alerts as data client API Interface + +## Table of contents + +### Classes + +- [AlertsClient](classes/alertsclient.md) + +### Interfaces + +- [ConstructorOptions](interfaces/constructoroptions.md) +- [UpdateOptions](interfaces/updateoptions.md) diff --git a/x-pack/plugins/rule_registry/docs/alerts_client/classes/alertsclient.md b/x-pack/plugins/rule_registry/docs/alerts_client/classes/alertsclient.md new file mode 100644 index 0000000000000..359834bf9c2e7 --- /dev/null +++ b/x-pack/plugins/rule_registry/docs/alerts_client/classes/alertsclient.md @@ -0,0 +1,191 @@ +[Alerts as data client API Interface](../alerts_client_api.md) / AlertsClient + +# Class: AlertsClient + +Provides apis to interact with alerts as data +ensures the request is authorized to perform read / write actions +on alerts as data. + +## Table of contents + +### Constructors + +- [constructor](alertsclient.md#constructor) + +### Properties + +- [auditLogger](alertsclient.md#auditlogger) +- [authorization](alertsclient.md#authorization) +- [esClient](alertsclient.md#esclient) +- [logger](alertsclient.md#logger) + +### Methods + +- [fetchAlert](alertsclient.md#fetchalert) +- [get](alertsclient.md#get) +- [getAlertsIndex](alertsclient.md#getalertsindex) +- [getAuthorizedAlertsIndices](alertsclient.md#getauthorizedalertsindices) +- [update](alertsclient.md#update) + +## Constructors + +### constructor + +• **new AlertsClient**(`__namedParameters`) + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `__namedParameters` | [ConstructorOptions](../interfaces/constructoroptions.md) | + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:59](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L59) + +## Properties + +### auditLogger + +• `Private` `Optional` `Readonly` **auditLogger**: `AuditLogger` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:57](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L57) + +___ + +### authorization + +• `Private` `Readonly` **authorization**: `PublicMethodsOf` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:58](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L58) + +___ + +### esClient + +• `Private` `Readonly` **esClient**: `ElasticsearchClient` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:59](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L59) + +___ + +### logger + +• `Private` `Readonly` **logger**: `Logger` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:56](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L56) + +## Methods + +### fetchAlert + +▸ `Private` **fetchAlert**(`__namedParameters`): `Promise`\>, ``"kibana.rac.alert.owner"`` \| ``"rule.id"``\> & { `kibana.rac.alert.owner`: `string` ; `rule.id`: `string` } & { `_version`: `undefined` \| `string` }\> + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `__namedParameters` | `GetAlertParams` | + +#### Returns + +`Promise`\>, ``"kibana.rac.alert.owner"`` \| ``"rule.id"``\> & { `kibana.rac.alert.owner`: `string` ; `rule.id`: `string` } & { `_version`: `undefined` \| `string` }\> + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:79](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L79) + +___ + +### get + +▸ **get**(`__namedParameters`): `Promise`\>\> + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `__namedParameters` | `GetAlertParams` | + +#### Returns + +`Promise`\>\> + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:115](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L115) + +___ + +### getAlertsIndex + +▸ **getAlertsIndex**(`featureIds`, `operations`): `Promise`<`Object`\> + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `featureIds` | `string`[] | +| `operations` | (`ReadOperations` \| `WriteOperations`)[] | + +#### Returns + +`Promise`<`Object`\> + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:68](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L68) + +___ + +### getAuthorizedAlertsIndices + +▸ **getAuthorizedAlertsIndices**(`featureIds`): `Promise` + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `featureIds` | `string`[] | + +#### Returns + +`Promise` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:219](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L219) + +___ + +### update + +▸ **update**(`__namedParameters`): `Promise` + +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `Params` | `Params`: `AlertTypeParams` = `never` | + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `__namedParameters` | [UpdateOptions](../interfaces/updateoptions.md) | + +#### Returns + +`Promise` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:160](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L160) diff --git a/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/constructoroptions.md b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/constructoroptions.md new file mode 100644 index 0000000000000..051a5affc0379 --- /dev/null +++ b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/constructoroptions.md @@ -0,0 +1,52 @@ +[Alerts as data client API Interface](../alerts_client_api.md) / ConstructorOptions + +# Interface: ConstructorOptions + +## Table of contents + +### Properties + +- [auditLogger](constructoroptions.md#auditlogger) +- [authorization](constructoroptions.md#authorization) +- [esClient](constructoroptions.md#esclient) +- [logger](constructoroptions.md#logger) + +## Properties + +### auditLogger + +• `Optional` **auditLogger**: `AuditLogger` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:34](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L34) + +___ + +### authorization + +• **authorization**: `PublicMethodsOf` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:33](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L33) + +___ + +### esClient + +• **esClient**: `ElasticsearchClient` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:35](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L35) + +___ + +### logger + +• **logger**: `Logger` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:32](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L32) diff --git a/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/updateoptions.md b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/updateoptions.md new file mode 100644 index 0000000000000..10e793155c196 --- /dev/null +++ b/x-pack/plugins/rule_registry/docs/alerts_client/interfaces/updateoptions.md @@ -0,0 +1,58 @@ +[Alerts as data client API Interface](../alerts_client_api.md) / UpdateOptions + +# Interface: UpdateOptions + +## Type parameters + +| Name | Type | +| :------ | :------ | +| `Params` | `Params`: `AlertTypeParams` | + +## Table of contents + +### Properties + +- [\_version](updateoptions.md#_version) +- [id](updateoptions.md#id) +- [index](updateoptions.md#index) +- [status](updateoptions.md#status) + +## Properties + +### \_version + +• **\_version**: `undefined` \| `string` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:41](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L41) + +___ + +### id + +• **id**: `string` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:39](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L39) + +___ + +### index + +• **index**: `string` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:42](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L42) + +___ + +### status + +• **status**: `string` + +#### Defined in + +[rule_registry/server/alert_data_client/alerts_client.ts:40](https://github.com/elastic/kibana/blob/f2a94addc85/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L40) diff --git a/x-pack/plugins/rule_registry/docs/alerts_client_typedoc.json b/x-pack/plugins/rule_registry/docs/alerts_client_typedoc.json new file mode 100644 index 0000000000000..5f117323eeb1c --- /dev/null +++ b/x-pack/plugins/rule_registry/docs/alerts_client_typedoc.json @@ -0,0 +1,17 @@ +{ + "entryPoints": [ + "../server/alert_data_client/alerts_client.ts" + ], + "exclude": [ + "**/mock.ts", + "../server/alert_data_client/+(mock.ts|utils.ts|utils.test.ts|types.ts)" + ], + "excludeExternals": true, + "out": "alerts_client", + "theme": "markdown", + "plugin": "typedoc-plugin-markdown", + "entryDocument": "alerts_client_api.md", + "readme": "none", + "name": "Alerts as data client API Interface" +} + diff --git a/x-pack/plugins/rule_registry/kibana.json b/x-pack/plugins/rule_registry/kibana.json index 8c1e8d0f5e40e..f74bebf585edd 100644 --- a/x-pack/plugins/rule_registry/kibana.json +++ b/x-pack/plugins/rule_registry/kibana.json @@ -12,5 +12,6 @@ "spaces", "triggersActionsUi" ], + "optionalPlugins": ["security"], "server": true } diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts new file mode 100644 index 0000000000000..73c6b4dd40526 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PublicMethodsOf } from '@kbn/utility-types'; +import { AlertsClient } from './alerts_client'; + +type Schema = PublicMethodsOf; +export type AlertsClientMock = jest.Mocked; + +const createAlertsClientMock = () => { + const mocked: AlertsClientMock = { + get: jest.fn(), + getAlertsIndex: jest.fn(), + update: jest.fn(), + getAuthorizedAlertsIndices: jest.fn(), + }; + return mocked; +}; + +export const alertsClientMock: { + create: () => AlertsClientMock; +} = { + create: createAlertsClientMock, +}; diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts new file mode 100644 index 0000000000000..553c5ce4472a6 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts @@ -0,0 +1,243 @@ +/* + * 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 { PublicMethodsOf } from '@kbn/utility-types'; +import { decodeVersion, encodeHitVersion } from '@kbn/securitysolution-es-utils'; +import { AlertTypeParams } from '../../../alerting/server'; +import { + ReadOperations, + AlertingAuthorization, + WriteOperations, + AlertingAuthorizationEntity, +} from '../../../alerting/server'; +import { Logger, ElasticsearchClient } from '../../../../../src/core/server'; +import { alertAuditEvent, AlertAuditAction } from './audit_events'; +import { AuditLogger } from '../../../security/server'; +import { ALERT_STATUS, OWNER, RULE_ID } from '../../common/technical_rule_data_field_names'; +import { ParsedTechnicalFields } from '../../common/parse_technical_fields'; +import { mapConsumerToIndexName, validFeatureIds, isValidFeatureId } from '../utils/rbac'; + +// TODO: Fix typings https://github.com/elastic/kibana/issues/101776 +type NonNullableProps = Omit & + { [K in Props]-?: NonNullable }; +type AlertType = NonNullableProps; + +const isValidAlert = (source?: ParsedTechnicalFields): source is AlertType => { + return source?.[RULE_ID] != null && source?.[OWNER] != null; +}; +export interface ConstructorOptions { + logger: Logger; + authorization: PublicMethodsOf; + auditLogger?: AuditLogger; + esClient: ElasticsearchClient; +} + +export interface UpdateOptions { + id: string; + status: string; + _version: string | undefined; + index: string; +} + +interface GetAlertParams { + id: string; + index?: string; +} + +/** + * Provides apis to interact with alerts as data + * ensures the request is authorized to perform read / write actions + * on alerts as data. + */ +export class AlertsClient { + private readonly logger: Logger; + private readonly auditLogger?: AuditLogger; + private readonly authorization: PublicMethodsOf; + private readonly esClient: ElasticsearchClient; + + constructor({ auditLogger, authorization, logger, esClient }: ConstructorOptions) { + this.logger = logger; + this.authorization = authorization; + this.esClient = esClient; + this.auditLogger = auditLogger; + } + + public async getAlertsIndex( + featureIds: string[], + operations: Array + ) { + return this.authorization.getAugmentedRuleTypesWithAuthorization( + featureIds.length !== 0 ? featureIds : validFeatureIds, + operations, + AlertingAuthorizationEntity.Alert + ); + } + + private async fetchAlert({ + id, + index, + }: GetAlertParams): Promise<(AlertType & { _version: string | undefined }) | null | undefined> { + try { + const result = await this.esClient.search({ + // Context: Originally thought of always just searching `.alerts-*` but that could + // result in a big performance hit. If the client already knows which index the alert + // belongs to, passing in the index will speed things up + index: index ?? '.alerts-*', + ignore_unavailable: true, + body: { query: { term: { _id: id } } }, + seq_no_primary_term: true, + }); + + if (result == null || result.body == null || result.body.hits.hits.length === 0) { + return; + } + + if (!isValidAlert(result.body.hits.hits[0]._source)) { + const errorMessage = `Unable to retrieve alert details for alert with id of "${id}".`; + this.logger.debug(errorMessage); + throw new Error(errorMessage); + } + + return { + ...result.body.hits.hits[0]._source, + _version: encodeHitVersion(result.body.hits.hits[0]), + }; + } catch (error) { + const errorMessage = `Unable to retrieve alert with id of "${id}".`; + this.logger.debug(errorMessage); + throw error; + } + } + + public async get({ + id, + index, + }: GetAlertParams): Promise { + try { + // first search for the alert by id, then use the alert info to check if user has access to it + const alert = await this.fetchAlert({ + id, + index, + }); + + if (alert == null) { + return; + } + + // this.authorization leverages the alerting plugin's authorization + // client exposed to us for reuse + await this.authorization.ensureAuthorized({ + ruleTypeId: alert[RULE_ID], + consumer: alert[OWNER], + operation: ReadOperations.Get, + entity: AlertingAuthorizationEntity.Alert, + }); + + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.GET, + id, + }) + ); + + return alert; + } catch (error) { + this.logger.debug(`Error fetching alert with id of "${id}"`); + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.GET, + id, + error, + }) + ); + throw error; + } + } + + public async update({ + id, + status, + _version, + index, + }: UpdateOptions) { + try { + const alert = await this.fetchAlert({ + id, + index, + }); + + if (alert == null) { + return; + } + + await this.authorization.ensureAuthorized({ + ruleTypeId: alert[RULE_ID], + consumer: alert[OWNER], + operation: WriteOperations.Update, + entity: AlertingAuthorizationEntity.Alert, + }); + + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.UPDATE, + id, + outcome: 'unknown', + }) + ); + + const { body: response } = await this.esClient.update({ + ...decodeVersion(_version), + id, + index, + body: { + doc: { + [ALERT_STATUS]: status, + }, + }, + refresh: 'wait_for', + }); + + return { + ...response, + _version: encodeHitVersion(response), + }; + } catch (error) { + this.auditLogger?.log( + alertAuditEvent({ + action: AlertAuditAction.UPDATE, + id, + error, + }) + ); + throw error; + } + } + + public async getAuthorizedAlertsIndices(featureIds: string[]): Promise { + const augmentedRuleTypes = await this.authorization.getAugmentedRuleTypesWithAuthorization( + featureIds, + [ReadOperations.Find, ReadOperations.Get, WriteOperations.Update], + AlertingAuthorizationEntity.Alert + ); + + // As long as the user can read a minimum of one type of rule type produced by the provided feature, + // the user should be provided that features' alerts index. + // Limiting which alerts that user can read on that index will be done via the findAuthorizationFilter + const authorizedFeatures = new Set(); + for (const ruleType of augmentedRuleTypes.authorizedRuleTypes) { + authorizedFeatures.add(ruleType.producer); + } + + const toReturn = Array.from(authorizedFeatures).flatMap((feature) => { + if (isValidFeatureId(feature)) { + return mapConsumerToIndexName[feature]; + } + return []; + }); + + return toReturn; + } +} diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.test.ts new file mode 100644 index 0000000000000..9e1941f779722 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.test.ts @@ -0,0 +1,78 @@ +/* + * 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 { Request } from '@hapi/hapi'; + +import { AlertsClientFactory, AlertsClientFactoryProps } from './alerts_client_factory'; +import { ElasticsearchClient, KibanaRequest } from 'src/core/server'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { securityMock } from '../../../security/server/mocks'; +import { AuditLogger } from '../../../security/server'; +import { alertingAuthorizationMock } from '../../../alerting/server/authorization/alerting_authorization.mock'; + +jest.mock('./alerts_client'); + +const securityPluginSetup = securityMock.createSetup(); +const alertingAuthMock = alertingAuthorizationMock.create(); + +const alertsClientFactoryParams: AlertsClientFactoryProps = { + logger: loggingSystemMock.create().get(), + getAlertingAuthorization: (_: KibanaRequest) => alertingAuthMock, + securityPluginSetup, + esClient: {} as ElasticsearchClient, +}; + +const fakeRequest = ({ + app: {}, + headers: {}, + getBasePath: () => '', + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, +} as unknown) as Request; + +const auditLogger = { + log: jest.fn(), +} as jest.Mocked; + +describe('AlertsClientFactory', () => { + beforeEach(() => { + jest.resetAllMocks(); + + securityPluginSetup.audit.asScoped.mockReturnValue(auditLogger); + }); + + test('creates an alerts client with proper constructor arguments', async () => { + const factory = new AlertsClientFactory(); + factory.initialize({ ...alertsClientFactoryParams }); + const request = KibanaRequest.from(fakeRequest); + await factory.create(request); + + expect(jest.requireMock('./alerts_client').AlertsClient).toHaveBeenCalledWith({ + authorization: alertingAuthMock, + logger: alertsClientFactoryParams.logger, + auditLogger, + esClient: {}, + }); + }); + + test('throws an error if already initialized', () => { + const factory = new AlertsClientFactory(); + factory.initialize({ ...alertsClientFactoryParams }); + + expect(() => + factory.initialize({ ...alertsClientFactoryParams }) + ).toThrowErrorMatchingInlineSnapshot(`"AlertsClientFactory (RAC) already initialized"`); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.ts new file mode 100644 index 0000000000000..43a3827b28972 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.ts @@ -0,0 +1,55 @@ +/* + * 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, KibanaRequest, Logger } from 'src/core/server'; +import { PublicMethodsOf } from '@kbn/utility-types'; +import { SecurityPluginSetup } from '../../../security/server'; +import { AlertingAuthorization } from '../../../alerting/server'; +import { AlertsClient } from './alerts_client'; + +export interface AlertsClientFactoryProps { + logger: Logger; + esClient: ElasticsearchClient; + getAlertingAuthorization: (request: KibanaRequest) => PublicMethodsOf; + securityPluginSetup: SecurityPluginSetup | undefined; +} + +export class AlertsClientFactory { + private isInitialized = false; + private logger!: Logger; + private esClient!: ElasticsearchClient; + private getAlertingAuthorization!: ( + request: KibanaRequest + ) => PublicMethodsOf; + private securityPluginSetup!: SecurityPluginSetup | undefined; + + public initialize(options: AlertsClientFactoryProps) { + /** + * This should be called by the plugin's start() method. + */ + if (this.isInitialized) { + throw new Error('AlertsClientFactory (RAC) already initialized'); + } + + this.getAlertingAuthorization = options.getAlertingAuthorization; + this.isInitialized = true; + this.logger = options.logger; + this.esClient = options.esClient; + this.securityPluginSetup = options.securityPluginSetup; + } + + public async create(request: KibanaRequest): Promise { + const { securityPluginSetup, getAlertingAuthorization, logger } = this; + + return new AlertsClient({ + logger, + authorization: getAlertingAuthorization(request), + auditLogger: securityPluginSetup?.audit.asScoped(request), + esClient: this.esClient, + }); + } +} diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/audit_events.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/audit_events.test.ts new file mode 100644 index 0000000000000..9536a9a640a00 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/audit_events.test.ts @@ -0,0 +1,87 @@ +/* + * 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 { AlertAuditAction, alertAuditEvent } from './audit_events'; + +describe('#alertAuditEvent', () => { + test('creates event with `unknown` outcome', () => { + expect( + alertAuditEvent({ + action: AlertAuditAction.GET, + outcome: 'unknown', + id: '123', + }) + ).toMatchInlineSnapshot(` + Object { + "error": undefined, + "event": Object { + "action": "alert_get", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "access", + ], + }, + "message": "User is accessing alert [id=123]", + } + `); + }); + + test('creates event with `success` outcome', () => { + expect( + alertAuditEvent({ + action: AlertAuditAction.GET, + id: '123', + }) + ).toMatchInlineSnapshot(` + Object { + "error": undefined, + "event": Object { + "action": "alert_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed alert [id=123]", + } + `); + }); + + test('creates event with `failure` outcome', () => { + expect( + alertAuditEvent({ + action: AlertAuditAction.GET, + id: '123', + error: new Error('ERROR_MESSAGE'), + }) + ).toMatchInlineSnapshot(` + Object { + "error": Object { + "code": "Error", + "message": "ERROR_MESSAGE", + }, + "event": Object { + "action": "alert_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access alert [id=123]", + } + `); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/audit_events.ts b/x-pack/plugins/rule_registry/server/alert_data_client/audit_events.ts new file mode 100644 index 0000000000000..d07c23c7fbe9f --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/audit_events.ts @@ -0,0 +1,61 @@ +/* + * 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 { EcsEventOutcome, EcsEventType } from 'src/core/server'; +import { AuditEvent } from '../../../security/server'; + +export enum AlertAuditAction { + GET = 'alert_get', + UPDATE = 'alert_update', + FIND = 'alert_find', +} + +type VerbsTuple = [string, string, string]; + +const eventVerbs: Record = { + alert_get: ['access', 'accessing', 'accessed'], + alert_update: ['update', 'updating', 'updated'], + alert_find: ['access', 'accessing', 'accessed'], +}; + +const eventTypes: Record = { + alert_get: 'access', + alert_update: 'change', + alert_find: 'access', +}; + +export interface AlertAuditEventParams { + action: AlertAuditAction; + outcome?: EcsEventOutcome; + id?: string; + error?: Error; +} + +export function alertAuditEvent({ action, id, outcome, error }: AlertAuditEventParams): AuditEvent { + const doc = id ? `alert [id=${id}]` : 'an alert'; + const [present, progressive, past] = eventVerbs[action]; + const message = error + ? `Failed attempt to ${present} ${doc}` + : outcome === 'unknown' + ? `User is ${progressive} ${doc}` + : `User has ${past} ${doc}`; + const type = eventTypes[action]; + + return { + message, + event: { + action, + category: ['database'], + type: type ? [type] : undefined, + outcome: outcome ?? (error ? 'failure' : 'success'), + }, + error: error && { + code: error.name, + message: error.message, + }, + }; +} diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts new file mode 100644 index 0000000000000..897c17a82b982 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts @@ -0,0 +1,240 @@ +/* + * 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 { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; +import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock'; +import { AuditLogger } from '../../../../security/server'; + +const alertingAuthMock = alertingAuthorizationMock.create(); +const esClientMock = elasticsearchClientMock.createElasticsearchClient(); +const auditLogger = { + log: jest.fn(), +} as jest.Mocked; + +const alertsClientParams: jest.Mocked = { + logger: loggingSystemMock.create().get(), + authorization: alertingAuthMock, + esClient: esClientMock, + auditLogger, +}; + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('get()', () => { + test('calls ES client with given params', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + esClientMock.search.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + failed: 0, + skipped: 0, + }, + hits: { + total: 1, + max_score: 999, + hits: [ + { + found: true, + _type: 'alert', + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _version: 1, + _seq_no: 362, + _primary_term: 2, + _source: { + 'rule.id': 'apm.error_rate', + message: 'hello world 1', + 'kibana.rac.alert.owner': 'apm', + 'kibana.rac.alert.status': 'open', + }, + }, + ], + }, + }, + }) + ); + const result = await alertsClient.get({ id: '1', index: '.alerts-observability-apm' }); + expect(result).toMatchInlineSnapshot(` + Object { + "_version": "WzM2MiwyXQ==", + "kibana.rac.alert.owner": "apm", + "kibana.rac.alert.status": "open", + "message": "hello world 1", + "rule.id": "apm.error_rate", + } + `); + expect(esClientMock.search).toHaveBeenCalledTimes(1); + expect(esClientMock.search.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "body": Object { + "query": Object { + "term": Object { + "_id": "1", + }, + }, + }, + "ignore_unavailable": true, + "index": ".alerts-observability-apm", + "seq_no_primary_term": true, + }, + ] + `); + }); + + test('logs successful event in audit logger', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + esClientMock.search.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + failed: 0, + skipped: 0, + }, + hits: { + total: 1, + max_score: 999, + hits: [ + { + found: true, + _type: 'alert', + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _version: 1, + _seq_no: 362, + _primary_term: 2, + _source: { + 'rule.id': 'apm.error_rate', + message: 'hello world 1', + 'kibana.rac.alert.owner': 'apm', + 'kibana.rac.alert.status': 'open', + }, + }, + ], + }, + }, + }) + ); + await alertsClient.get({ id: '1', index: '.alerts-observability-apm' }); + + expect(auditLogger.log).toHaveBeenCalledWith({ + error: undefined, + event: { action: 'alert_get', category: ['database'], outcome: 'success', type: ['access'] }, + message: 'User has accessed alert [id=1]', + }); + }); + + test(`throws an error if ES client get fails`, async () => { + const error = new Error('something went wrong'); + const alertsClient = new AlertsClient(alertsClientParams); + esClientMock.search.mockRejectedValue(error); + + await expect( + alertsClient.get({ id: '1', index: '.alerts-observability-apm' }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"something went wrong"`); + expect(auditLogger.log).toHaveBeenCalledWith({ + error: { code: 'Error', message: 'something went wrong' }, + event: { action: 'alert_get', category: ['database'], outcome: 'failure', type: ['access'] }, + message: 'Failed attempt to access alert [id=1]', + }); + }); + + describe('authorization', () => { + beforeEach(() => { + esClientMock.search.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + failed: 0, + skipped: 0, + }, + hits: { + total: 1, + max_score: 999, + hits: [ + { + found: true, + _type: 'alert', + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _version: 1, + _seq_no: 362, + _primary_term: 2, + _source: { + 'rule.id': 'apm.error_rate', + message: 'hello world 1', + 'kibana.rac.alert.owner': 'apm', + 'kibana.rac.alert.status': 'open', + }, + }, + ], + }, + }, + }) + ); + }); + + test('returns alert if user is authorized to read alert under the consumer', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + const result = await alertsClient.get({ id: '1', index: '.alerts-observability-apm' }); + + expect(alertingAuthMock.ensureAuthorized).toHaveBeenCalledWith({ + entity: 'alert', + consumer: 'apm', + operation: 'get', + ruleTypeId: 'apm.error_rate', + }); + expect(result).toMatchInlineSnapshot(` + Object { + "_version": "WzM2MiwyXQ==", + "kibana.rac.alert.owner": "apm", + "kibana.rac.alert.status": "open", + "message": "hello world 1", + "rule.id": "apm.error_rate", + } + `); + }); + + test('throws when user is not authorized to get this type of alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + alertingAuthMock.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to get a "apm.error_rate" alert for "apm"`) + ); + + await expect( + alertsClient.get({ id: '1', index: '.alerts-observability-apm' }) + ).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to get a "apm.error_rate" alert for "apm"]` + ); + + expect(alertingAuthMock.ensureAuthorized).toHaveBeenCalledWith({ + entity: 'alert', + consumer: 'apm', + operation: 'get', + ruleTypeId: 'apm.error_rate', + }); + }); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts new file mode 100644 index 0000000000000..6fc387fe54b3b --- /dev/null +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts @@ -0,0 +1,376 @@ +/* + * 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 { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; +import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock'; +import { AuditLogger } from '../../../../security/server'; + +const alertingAuthMock = alertingAuthorizationMock.create(); +const esClientMock = elasticsearchClientMock.createElasticsearchClient(); +const auditLogger = { + log: jest.fn(), +} as jest.Mocked; + +const alertsClientParams: jest.Mocked = { + logger: loggingSystemMock.create().get(), + authorization: alertingAuthMock, + esClient: esClientMock, + auditLogger, +}; + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('update()', () => { + test('calls ES client with given params', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + esClientMock.search.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + failed: 0, + skipped: 0, + }, + hits: { + total: 1, + max_score: 999, + hits: [ + { + found: true, + _type: 'alert', + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _source: { + 'rule.id': 'apm.error_rate', + message: 'hello world 1', + 'kibana.rac.alert.owner': 'apm', + 'kibana.rac.alert.status': 'open', + }, + }, + ], + }, + }, + }) + ); + esClientMock.update.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _version: 2, + result: 'updated', + _shards: { total: 2, successful: 1, failed: 0 }, + _seq_no: 1, + _primary_term: 1, + }, + }) + ); + const result = await alertsClient.update({ + id: '1', + status: 'closed', + _version: undefined, + index: '.alerts-observability-apm', + }); + expect(result).toMatchInlineSnapshot(` + Object { + "_id": "NoxgpHkBqbdrfX07MqXV", + "_index": ".alerts-observability-apm", + "_primary_term": 1, + "_seq_no": 1, + "_shards": Object { + "failed": 0, + "successful": 1, + "total": 2, + }, + "_version": "WzEsMV0=", + "result": "updated", + } + `); + expect(esClientMock.update).toHaveBeenCalledTimes(1); + expect(esClientMock.update.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "body": Object { + "doc": Object { + "kibana.rac.alert.status": "closed", + }, + }, + "id": "1", + "index": ".alerts-observability-apm", + "refresh": "wait_for", + }, + ] + `); + }); + + test('logs successful event in audit logger', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + esClientMock.search.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + failed: 0, + skipped: 0, + }, + hits: { + total: 1, + max_score: 999, + hits: [ + { + found: true, + _type: 'alert', + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _source: { + 'rule.id': 'apm.error_rate', + message: 'hello world 1', + 'kibana.rac.alert.owner': 'apm', + 'kibana.rac.alert.status': 'open', + }, + }, + ], + }, + }, + }) + ); + esClientMock.update.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _version: 2, + result: 'updated', + _shards: { total: 2, successful: 1, failed: 0 }, + _seq_no: 1, + _primary_term: 1, + }, + }) + ); + await alertsClient.update({ + id: '1', + status: 'closed', + _version: undefined, + index: '.alerts-observability-apm', + }); + + expect(auditLogger.log).toHaveBeenCalledWith({ + error: undefined, + event: { + action: 'alert_update', + category: ['database'], + outcome: 'unknown', + type: ['change'], + }, + message: 'User is updating alert [id=1]', + }); + }); + + test(`throws an error if ES client get fails`, async () => { + const error = new Error('something went wrong on get'); + const alertsClient = new AlertsClient(alertsClientParams); + esClientMock.search.mockRejectedValue(error); + + await expect( + alertsClient.update({ + id: '1', + status: 'closed', + _version: undefined, + index: '.alerts-observability-apm', + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"something went wrong on get"`); + expect(auditLogger.log).toHaveBeenCalledWith({ + error: { code: 'Error', message: 'something went wrong on get' }, + event: { + action: 'alert_update', + category: ['database'], + outcome: 'failure', + type: ['change'], + }, + message: 'Failed attempt to update alert [id=1]', + }); + }); + + test(`throws an error if ES client update fails`, async () => { + const error = new Error('something went wrong on update'); + const alertsClient = new AlertsClient(alertsClientParams); + esClientMock.search.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + failed: 0, + skipped: 0, + }, + hits: { + total: 1, + max_score: 999, + hits: [ + { + found: true, + _type: 'alert', + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _source: { + 'rule.id': 'apm.error_rate', + message: 'hello world 1', + 'kibana.rac.alert.owner': 'apm', + 'kibana.rac.alert.status': 'open', + }, + }, + ], + }, + }, + }) + ); + esClientMock.update.mockRejectedValue(error); + + await expect( + alertsClient.update({ + id: '1', + status: 'closed', + _version: undefined, + index: '.alerts-observability-apm', + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"something went wrong on update"`); + expect(auditLogger.log).toHaveBeenCalledWith({ + error: { code: 'Error', message: 'something went wrong on update' }, + event: { + action: 'alert_update', + category: ['database'], + outcome: 'failure', + type: ['change'], + }, + message: 'Failed attempt to update alert [id=1]', + }); + }); + + describe('authorization', () => { + beforeEach(() => { + esClientMock.search.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + took: 5, + timed_out: false, + _shards: { + total: 1, + successful: 1, + failed: 0, + skipped: 0, + }, + hits: { + total: 1, + max_score: 999, + hits: [ + { + found: true, + _type: 'alert', + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _version: 2, + _seq_no: 362, + _primary_term: 2, + _source: { + 'rule.id': 'apm.error_rate', + message: 'hello world 1', + 'kibana.rac.alert.owner': 'apm', + 'kibana.rac.alert.status': 'open', + }, + }, + ], + }, + }, + }) + ); + + esClientMock.update.mockResolvedValueOnce( + elasticsearchClientMock.createApiResponse({ + body: { + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _version: 2, + result: 'updated', + _shards: { total: 2, successful: 1, failed: 0 }, + _seq_no: 1, + _primary_term: 1, + }, + }) + ); + }); + + test('returns alert if user is authorized to update alert under the consumer', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + const result = await alertsClient.update({ + id: '1', + status: 'closed', + _version: undefined, + index: '.alerts-observability-apm', + }); + + expect(alertingAuthMock.ensureAuthorized).toHaveBeenCalledWith({ + entity: 'alert', + consumer: 'apm', + operation: 'update', + ruleTypeId: 'apm.error_rate', + }); + expect(result).toMatchInlineSnapshot(` + Object { + "_id": "NoxgpHkBqbdrfX07MqXV", + "_index": ".alerts-observability-apm", + "_primary_term": 1, + "_seq_no": 1, + "_shards": Object { + "failed": 0, + "successful": 1, + "total": 2, + }, + "_version": "WzEsMV0=", + "result": "updated", + } + `); + }); + + test('throws when user is not authorized to update this type of alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + alertingAuthMock.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to get a "apm.error_rate" alert for "apm"`) + ); + + await expect( + alertsClient.update({ + id: '1', + status: 'closed', + _version: undefined, + index: '.alerts-observability-apm', + }) + ).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to get a "apm.error_rate" alert for "apm"]` + ); + + expect(alertingAuthMock.ensureAuthorized).toHaveBeenCalledWith({ + entity: 'alert', + consumer: 'apm', + operation: 'update', + ruleTypeId: 'apm.error_rate', + }); + }); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/event_log/elasticsearch/index_writer.ts b/x-pack/plugins/rule_registry/server/event_log/elasticsearch/index_writer.ts index 7f83421ec80d8..6fd1c954d8c14 100644 --- a/x-pack/plugins/rule_registry/server/event_log/elasticsearch/index_writer.ts +++ b/x-pack/plugins/rule_registry/server/event_log/elasticsearch/index_writer.ts @@ -72,7 +72,7 @@ export class IndexWriter { for (const item of items) { if (item.doc === undefined) continue; - bulkBody.push({ create: { _index: item.index } }); + bulkBody.push({ create: { _index: item.index, version: 1 } }); bulkBody.push(item.doc); } diff --git a/x-pack/plugins/rule_registry/server/index.ts b/x-pack/plugins/rule_registry/server/index.ts index 9eefc19f34670..19ea85b056bed 100644 --- a/x-pack/plugins/rule_registry/server/index.ts +++ b/x-pack/plugins/rule_registry/server/index.ts @@ -10,10 +10,16 @@ import { RuleRegistryPlugin } from './plugin'; export * from './config'; export type { RuleRegistryPluginSetupContract, RuleRegistryPluginStartContract } from './plugin'; +export type { RacRequestHandlerContext, RacApiRequestHandlerContext } from './types'; export { RuleDataClient } from './rule_data_client'; export { IRuleDataClient } from './rule_data_client/types'; export { getRuleExecutorData, RuleExecutorData } from './utils/get_rule_executor_data'; export { createLifecycleRuleTypeFactory } from './utils/create_lifecycle_rule_type_factory'; +export { + LifecycleRuleExecutor, + LifecycleAlertServices, + createLifecycleExecutor, +} from './utils/create_lifecycle_executor'; export { createPersistenceRuleTypeFactory } from './utils/create_persistence_rule_type_factory'; export const plugin = (initContext: PluginInitializerContext) => diff --git a/x-pack/plugins/rule_registry/server/plugin.ts b/x-pack/plugins/rule_registry/server/plugin.ts index 043b07f9d67c1..ca98254037732 100644 --- a/x-pack/plugins/rule_registry/server/plugin.ts +++ b/x-pack/plugins/rule_registry/server/plugin.ts @@ -4,19 +4,34 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { PluginInitializerContext, Plugin, CoreSetup, Logger } from 'src/core/server'; +import { + PluginInitializerContext, + Plugin, + CoreSetup, + Logger, + KibanaRequest, + CoreStart, + IContextProvider, +} from 'src/core/server'; +import { SecurityPluginSetup } from '../../security/server'; +import { AlertsClientFactory } from './alert_data_client/alerts_client_factory'; +import { PluginStartContract as AlertingStart } from '../../alerting/server'; +import { RacApiRequestHandlerContext, RacRequestHandlerContext } from './types'; +import { defineRoutes } from './routes'; import { SpacesPluginStart } from '../../spaces/server'; import { RuleRegistryPluginConfig } from './config'; import { RuleDataPluginService } from './rule_data_plugin_service'; import { EventLogService, IEventLogService } from './event_log'; +import { AlertsClient } from './alert_data_client/alerts_client'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface RuleRegistryPluginSetupDependencies {} +export interface RuleRegistryPluginSetupDependencies { + security?: SecurityPluginSetup; +} -interface RuleRegistryPluginStartDependencies { +export interface RuleRegistryPluginStartDependencies { spaces: SpacesPluginStart; + alerting: AlertingStart; } export interface RuleRegistryPluginSetupContract { @@ -24,7 +39,10 @@ export interface RuleRegistryPluginSetupContract { eventLogService: IEventLogService; } -export type RuleRegistryPluginStartContract = void; +export interface RuleRegistryPluginStartContract { + getRacClientWithRequest: (req: KibanaRequest) => Promise; + alerting: AlertingStart; +} export class RuleRegistryPlugin implements @@ -37,17 +55,23 @@ export class RuleRegistryPlugin private readonly config: RuleRegistryPluginConfig; private readonly logger: Logger; private eventLogService: EventLogService | null; + private readonly alertsClientFactory: AlertsClientFactory; + private ruleDataService: RuleDataPluginService | null; + private security: SecurityPluginSetup | undefined; constructor(initContext: PluginInitializerContext) { this.config = initContext.config.get(); this.logger = initContext.logger.get(); this.eventLogService = null; + this.ruleDataService = null; + this.alertsClientFactory = new AlertsClientFactory(); } public setup( - core: CoreSetup + core: CoreSetup, + plugins: RuleRegistryPluginSetupDependencies ): RuleRegistryPluginSetupContract { - const { config, logger } = this; + const { logger } = this; const startDependencies = core.getStartServices().then(([coreStart, pluginStart]) => { return { @@ -56,23 +80,36 @@ export class RuleRegistryPlugin }; }); - const ruleDataService = new RuleDataPluginService({ - logger, - isWriteEnabled: config.write.enabled, - index: config.index, + this.security = plugins.security; + + const service = new RuleDataPluginService({ + logger: this.logger, + isWriteEnabled: this.config.write.enabled, + index: this.config.index, getClusterClient: async () => { const deps = await startDependencies; return deps.core.elasticsearch.client.asInternalUser; }, }); - ruleDataService.init().catch((originalError) => { + service.init().catch((originalError) => { const error = new Error('Failed installing assets'); // @ts-ignore error.stack = originalError.stack; - logger.error(error); + this.logger.error(error); }); + this.ruleDataService = service; + + // ALERTS ROUTES + const router = core.http.createRouter(); + core.http.registerRouteHandlerContext( + 'rac', + this.createRouteHandlerContext() + ); + + defineRoutes(router); + const eventLogService = new EventLogService({ config: { indexPrefix: this.config.index, @@ -86,10 +123,47 @@ export class RuleRegistryPlugin }); this.eventLogService = eventLogService; - return { ruleDataService, eventLogService }; + + return { ruleDataService: this.ruleDataService, eventLogService }; } - public start(): RuleRegistryPluginStartContract {} + public start( + core: CoreStart, + plugins: RuleRegistryPluginStartDependencies + ): RuleRegistryPluginStartContract { + const { logger, alertsClientFactory, security } = this; + + alertsClientFactory.initialize({ + logger, + esClient: core.elasticsearch.client.asInternalUser, + // NOTE: Alerts share the authorization client with the alerting plugin + getAlertingAuthorization(request: KibanaRequest) { + return plugins.alerting.getAlertingAuthorizationWithRequest(request); + }, + securityPluginSetup: security, + }); + + const getRacClientWithRequest = (request: KibanaRequest) => { + return alertsClientFactory.create(request); + }; + + return { + getRacClientWithRequest, + alerting: plugins.alerting, + }; + } + + private createRouteHandlerContext = (): IContextProvider => { + const { alertsClientFactory } = this; + return function alertsRouteHandlerContext(context, request): RacApiRequestHandlerContext { + return { + getAlertsClient: async () => { + const createdClient = alertsClientFactory.create(request); + return createdClient; + }, + }; + }; + }; public stop() { const { eventLogService, logger } = this; diff --git a/x-pack/plugins/rule_registry/server/routes/__mocks__/request_context.ts b/x-pack/plugins/rule_registry/server/routes/__mocks__/request_context.ts new file mode 100644 index 0000000000000..6d47882ca86c4 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/__mocks__/request_context.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { coreMock, elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks'; +import { alertsClientMock } from '../../alert_data_client/alerts_client.mock'; +import { RacRequestHandlerContext } from '../../types'; + +const createMockClients = () => ({ + rac: alertsClientMock.create(), + clusterClient: elasticsearchServiceMock.createLegacyScopedClusterClient(), + newClusterClient: elasticsearchServiceMock.createScopedClusterClient(), + savedObjectsClient: savedObjectsClientMock.create(), +}); + +const createRequestContextMock = ( + clients: ReturnType = createMockClients() +) => { + const coreContext = coreMock.createRequestHandlerContext(); + return ({ + rac: { getAlertsClient: jest.fn(() => clients.rac) }, + core: { + ...coreContext, + elasticsearch: { + ...coreContext.elasticsearch, + client: clients.newClusterClient, + legacy: { ...coreContext.elasticsearch.legacy, client: clients.clusterClient }, + }, + savedObjects: { client: clients.savedObjectsClient }, + }, + } as unknown) as RacRequestHandlerContext; +}; + +const createTools = () => { + const clients = createMockClients(); + const context = createRequestContextMock(clients); + + return { clients, context }; +}; + +export const requestContextMock = { + create: createRequestContextMock, + createMockClients, + createTools, +}; diff --git a/x-pack/plugins/rule_registry/server/routes/__mocks__/request_responses.ts b/x-pack/plugins/rule_registry/server/routes/__mocks__/request_responses.ts new file mode 100644 index 0000000000000..228fcf491994f --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/__mocks__/request_responses.ts @@ -0,0 +1,27 @@ +/* + * 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 { BASE_RAC_ALERTS_API_PATH } from '../../../common/constants'; +import { requestMock } from './server'; + +export const getReadRequest = () => + requestMock.create({ + method: 'get', + path: BASE_RAC_ALERTS_API_PATH, + query: { id: 'alert-1' }, + }); + +export const getUpdateRequest = () => + requestMock.create({ + method: 'patch', + path: BASE_RAC_ALERTS_API_PATH, + body: { + status: 'closed', + ids: ['alert-1'], + index: '.alerts-observability-apm*', + }, + }); diff --git a/x-pack/plugins/rule_registry/server/routes/__mocks__/response_adapters.ts b/x-pack/plugins/rule_registry/server/routes/__mocks__/response_adapters.ts new file mode 100644 index 0000000000000..7952b33dcf9b1 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/__mocks__/response_adapters.ts @@ -0,0 +1,63 @@ +/* + * 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 { httpServerMock } from 'src/core/server/mocks'; + +const responseMock = { + create: httpServerMock.createResponseFactory, +}; + +type ResponseMock = ReturnType; +type Method = keyof ResponseMock; + +type MockCall = any; + +interface ResponseCall { + body: any; + status: number; +} + +/** + * @internal + */ +export interface Response extends ResponseCall { + calls: ResponseCall[]; +} + +const buildResponses = (method: Method, calls: MockCall[]): ResponseCall[] => { + if (!calls.length) return []; + + switch (method) { + case 'ok': + return calls.map(([call]) => ({ status: 200, body: call.body })); + case 'customError': + return calls.map(([call]) => ({ + status: call.statusCode, + body: call.body, + })); + default: + throw new Error(`Encountered unexpected call to response.${method}`); + } +}; + +export const responseAdapter = (response: ResponseMock): Response => { + const methods = Object.keys(response) as Method[]; + const calls = methods + .reduce((responses, method) => { + const methodMock = response[method]; + return [...responses, ...buildResponses(method, methodMock.mock.calls)]; + }, []) + .sort((call, other) => other.status - call.status); + + const [{ body, status }] = calls; + + return { + body, + status, + calls, + }; +}; diff --git a/x-pack/plugins/rule_registry/server/routes/__mocks__/server.ts b/x-pack/plugins/rule_registry/server/routes/__mocks__/server.ts new file mode 100644 index 0000000000000..ade72435c57d9 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/__mocks__/server.ts @@ -0,0 +1,103 @@ +/* + * 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 { RequestHandler, RouteConfig, KibanaRequest } from 'src/core/server'; +import { httpServerMock, httpServiceMock } from 'src/core/server/mocks'; +import { RacRequestHandlerContext } from '../../types'; +import { requestContextMock } from './request_context'; +import { responseAdapter } from './response_adapters'; + +export const requestMock = { + create: httpServerMock.createKibanaRequest, +}; + +export const responseFactoryMock = { + create: httpServerMock.createResponseFactory, +}; + +interface Route { + config: RouteConfig; + handler: RequestHandler; +} +const getRoute = (routerMock: MockServer['router']): Route => { + const routeCalls = [ + ...routerMock.get.mock.calls, + ...routerMock.post.mock.calls, + ...routerMock.put.mock.calls, + ...routerMock.patch.mock.calls, + ...routerMock.delete.mock.calls, + ]; + + const [route] = routeCalls; + if (!route) { + throw new Error('No route registered!'); + } + + const [config, handler] = route; + return { config, handler }; +}; + +const buildResultMock = () => ({ ok: jest.fn((x) => x), badRequest: jest.fn((x) => x) }); + +class MockServer { + constructor( + public readonly router = httpServiceMock.createRouter(), + private responseMock = responseFactoryMock.create(), + private contextMock = requestContextMock.create(), + private resultMock = buildResultMock() + ) {} + + public validate(request: KibanaRequest) { + this.validateRequest(request); + return this.resultMock; + } + + public async inject( + request: KibanaRequest, + context: RacRequestHandlerContext = this.contextMock + ) { + const validatedRequest = this.validateRequest(request); + const [rejection] = this.resultMock.badRequest.mock.calls; + if (rejection) { + throw new Error(`Request was rejected with message: '${rejection}'`); + } + + await this.getRoute().handler(context, validatedRequest, this.responseMock); + return responseAdapter(this.responseMock); + } + + private getRoute(): Route { + return getRoute(this.router); + } + + private maybeValidate(part: any, validator?: any): any { + return typeof validator === 'function' ? validator(part, this.resultMock) : part; + } + + private validateRequest(request: KibanaRequest): KibanaRequest { + const validations = this.getRoute().config.validate; + if (!validations) { + return request; + } + + const validatedRequest = requestMock.create({ + path: request.route.path, + method: request.route.method, + body: this.maybeValidate(request.body, validations.body), + query: this.maybeValidate(request.query, validations.query), + params: this.maybeValidate(request.params, validations.params), + }); + + return validatedRequest; + } +} + +const createMockServer = () => new MockServer(); + +export const serverMock = { + create: createMockServer, +}; diff --git a/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.test.ts b/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.test.ts new file mode 100644 index 0000000000000..0de1e6c585a17 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.test.ts @@ -0,0 +1,98 @@ +/* + * 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 { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; +import { ParsedTechnicalFields } from '../../common/parse_technical_fields'; +import { getAlertByIdRoute } from './get_alert_by_id'; +import { requestContextMock } from './__mocks__/request_context'; +import { getReadRequest } from './__mocks__/request_responses'; +import { requestMock, serverMock } from './__mocks__/server'; + +const getMockAlert = (): ParsedTechnicalFields => ({ + '@timestamp': '2021-06-21T21:33:05.713Z', + 'rule.id': 'apm.error_rate', + 'kibana.rac.alert.owner': 'apm', + 'kibana.rac.alert.status': 'open', +}); + +describe('getAlertByIdRoute', () => { + let server: ReturnType; + let { clients, context } = requestContextMock.createTools(); + + beforeEach(async () => { + server = serverMock.create(); + ({ clients, context } = requestContextMock.createTools()); + + clients.rac.get.mockResolvedValue(getMockAlert()); + + getAlertByIdRoute(server.router); + }); + + test('returns 200 when finding a single alert with valid params', async () => { + const response = await server.inject(getReadRequest(), context); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(getMockAlert()); + }); + + test('returns 200 when finding a single alert with index param', async () => { + const response = await server.inject( + requestMock.create({ + method: 'get', + path: BASE_RAC_ALERTS_API_PATH, + query: { id: 'alert-1', index: '.alerts-me' }, + }), + context + ); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(getMockAlert()); + }); + + describe('request validation', () => { + test('rejects invalid query params', async () => { + await expect( + server.inject( + requestMock.create({ + method: 'get', + path: BASE_RAC_ALERTS_API_PATH, + query: { id: 4 }, + }), + context + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Request was rejected with message: 'Invalid value \\"4\\" supplied to \\"id\\"'"` + ); + }); + + test('rejects unknown query params', async () => { + await expect( + server.inject( + requestMock.create({ + method: 'get', + path: BASE_RAC_ALERTS_API_PATH, + query: { notId: 4 }, + }), + context + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Request was rejected with message: 'Invalid value \\"undefined\\" supplied to \\"id\\"'"` + ); + }); + }); + + test('returns error status if rac client "GET" fails', async () => { + clients.rac.get.mockRejectedValue(new Error('Unable to get alert')); + const response = await server.inject(getReadRequest(), context); + + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + attributes: { success: false }, + message: 'Unable to get alert', + }); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.ts b/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.ts new file mode 100644 index 0000000000000..9ddec56055a5a --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.ts @@ -0,0 +1,76 @@ +/* + * 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 { IRouter } from 'kibana/server'; +import * as t from 'io-ts'; +import { id as _id } from '@kbn/securitysolution-io-ts-list-types'; +import { transformError } from '@kbn/securitysolution-es-utils'; + +import { RacRequestHandlerContext } from '../types'; +import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; +import { buildRouteValidation } from './utils/route_validation'; + +export const getAlertByIdRoute = (router: IRouter) => { + router.get( + { + path: BASE_RAC_ALERTS_API_PATH, + validate: { + query: buildRouteValidation( + t.intersection([ + t.exact( + t.type({ + id: _id, + }) + ), + t.exact( + t.partial({ + index: t.string, + }) + ), + ]) + ), + }, + options: { + tags: ['access:rac'], + }, + }, + async (context, request, response) => { + try { + const alertsClient = await context.rac.getAlertsClient(); + const { id, index } = request.query; + const alert = await alertsClient.get({ id, index }); + if (alert == null) { + return response.notFound({ + body: { message: `alert with id ${id} and index ${index} not found` }, + }); + } + return response.ok({ + body: alert, + }); + } catch (exc) { + const err = transformError(exc); + const contentType = { + 'content-type': 'application/json', + }; + const defaultedHeaders = { + ...contentType, + }; + + return response.customError({ + headers: defaultedHeaders, + statusCode: err.statusCode, + body: { + message: err.message, + attributes: { + success: false, + }, + }, + }); + } + } + ); +}; diff --git a/x-pack/plugins/rule_registry/server/routes/get_alert_index.ts b/x-pack/plugins/rule_registry/server/routes/get_alert_index.ts new file mode 100644 index 0000000000000..b8b181a493cec --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/get_alert_index.ts @@ -0,0 +1,54 @@ +/* + * 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 { IRouter } from 'kibana/server'; +import { id as _id } from '@kbn/securitysolution-io-ts-list-types'; +import { transformError } from '@kbn/securitysolution-es-utils'; + +import { RacRequestHandlerContext } from '../types'; +import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; +import { validFeatureIds } from '../utils/rbac'; + +export const getAlertsIndexRoute = (router: IRouter) => { + router.get( + { + path: `${BASE_RAC_ALERTS_API_PATH}/index`, + validate: false, + options: { + tags: ['access:rac'], + }, + }, + async (context, request, response) => { + try { + const alertsClient = await context.rac.getAlertsClient(); + const indexName = await alertsClient.getAuthorizedAlertsIndices(validFeatureIds); + return response.ok({ + body: { index_name: indexName }, + }); + } catch (exc) { + const err = transformError(exc); + const contentType = { + 'content-type': 'application/json', + }; + const defaultedHeaders = { + ...contentType, + }; + + return response.custom({ + headers: defaultedHeaders, + statusCode: err.statusCode, + body: Buffer.from( + JSON.stringify({ + message: err.message, + status_code: err.statusCode, + }) + ), + }); + } + } + ); +}; diff --git a/x-pack/plugins/rule_registry/server/routes/index.ts b/x-pack/plugins/rule_registry/server/routes/index.ts new file mode 100644 index 0000000000000..6698cd7717268 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { IRouter } from 'kibana/server'; +import { RacRequestHandlerContext } from '../types'; +import { getAlertByIdRoute } from './get_alert_by_id'; +import { updateAlertByIdRoute } from './update_alert_by_id'; +import { getAlertsIndexRoute } from './get_alert_index'; + +export function defineRoutes(router: IRouter) { + getAlertByIdRoute(router); + updateAlertByIdRoute(router); + getAlertsIndexRoute(router); +} diff --git a/x-pack/plugins/rule_registry/server/routes/update_alert_by_id.test.ts b/x-pack/plugins/rule_registry/server/routes/update_alert_by_id.test.ts new file mode 100644 index 0000000000000..7ec699491ca83 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/update_alert_by_id.test.ts @@ -0,0 +1,101 @@ +/* + * 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 { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; +import { updateAlertByIdRoute } from './update_alert_by_id'; +import { requestContextMock } from './__mocks__/request_context'; +import { getUpdateRequest } from './__mocks__/request_responses'; +import { requestMock, serverMock } from './__mocks__/server'; + +describe('updateAlertByIdRoute', () => { + let server: ReturnType; + let { clients, context } = requestContextMock.createTools(); + + beforeEach(async () => { + server = serverMock.create(); + ({ clients, context } = requestContextMock.createTools()); + + clients.rac.update.mockResolvedValue({ + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _version: 'WzM2MiwyXQ==', + result: 'updated', + _shards: { total: 2, successful: 1, failed: 0 }, + _seq_no: 1, + _primary_term: 1, + }); + + updateAlertByIdRoute(server.router); + }); + + test('returns 200 when updating a single alert with valid params', async () => { + const response = await server.inject(getUpdateRequest(), context); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + _version: 'WzM2MiwyXQ==', + result: 'updated', + _shards: { total: 2, successful: 1, failed: 0 }, + _seq_no: 1, + _primary_term: 1, + success: true, + }); + }); + + describe('request validation', () => { + test('rejects invalid query params', async () => { + await expect( + server.inject( + requestMock.create({ + method: 'patch', + path: BASE_RAC_ALERTS_API_PATH, + body: { + status: 'closed', + ids: 'alert-1', + index: '.alerts-observability-apm*', + }, + }), + context + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Request was rejected with message: 'Invalid value \\"alert-1\\" supplied to \\"ids\\"'"` + ); + }); + + test('rejects unknown query params', async () => { + await expect( + server.inject( + requestMock.create({ + method: 'patch', + path: BASE_RAC_ALERTS_API_PATH, + body: { + notStatus: 'closed', + ids: ['alert-1'], + index: '.alerts-observability-apm*', + }, + }), + context + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Request was rejected with message: 'Invalid value \\"undefined\\" supplied to \\"status\\"'"` + ); + }); + }); + + test('returns error status if rac client "GET" fails', async () => { + clients.rac.update.mockRejectedValue(new Error('Unable to update alert')); + const response = await server.inject(getUpdateRequest(), context); + + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + attributes: { success: false }, + message: 'Unable to update alert', + }); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/routes/update_alert_by_id.ts b/x-pack/plugins/rule_registry/server/routes/update_alert_by_id.ts new file mode 100644 index 0000000000000..a77688a514e77 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/update_alert_by_id.ts @@ -0,0 +1,85 @@ +/* + * 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 { IRouter } from 'kibana/server'; +import * as t from 'io-ts'; +import { id as _id } from '@kbn/securitysolution-io-ts-list-types'; +import { transformError } from '@kbn/securitysolution-es-utils'; + +import { buildRouteValidation } from './utils/route_validation'; +import { RacRequestHandlerContext } from '../types'; +import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; + +export const updateAlertByIdRoute = (router: IRouter) => { + router.post( + { + path: BASE_RAC_ALERTS_API_PATH, + validate: { + body: buildRouteValidation( + t.intersection([ + t.exact( + t.type({ + status: t.string, + ids: t.array(t.string), + index: t.string, + }) + ), + t.exact( + t.partial({ + _version: t.string, + }) + ), + ]) + ), + }, + options: { + tags: ['access:rac'], + }, + }, + async (context, req, response) => { + try { + const alertsClient = await context.rac.getAlertsClient(); + const { status, ids, index, _version } = req.body; + + const updatedAlert = await alertsClient.update({ + id: ids[0], + status, + _version, + index, + }); + + if (updatedAlert == null) { + return response.notFound({ + body: { message: `alerts with ids ${ids} and index ${index} not found` }, + }); + } + + return response.ok({ body: { success: true, ...updatedAlert } }); + } catch (exc) { + const err = transformError(exc); + + const contentType = { + 'content-type': 'application/json', + }; + const defaultedHeaders = { + ...contentType, + }; + + return response.customError({ + headers: defaultedHeaders, + statusCode: err.statusCode, + body: { + message: err.message, + attributes: { + success: false, + }, + }, + }); + } + } + ); +}; diff --git a/x-pack/plugins/rule_registry/server/routes/utils/route_validation.ts b/x-pack/plugins/rule_registry/server/routes/utils/route_validation.ts new file mode 100644 index 0000000000000..8e74760d6d15f --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/utils/route_validation.ts @@ -0,0 +1,56 @@ +/* + * 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. + */ +/* + * 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 { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import * as rt from 'io-ts'; +import { exactCheck, formatErrors } from '@kbn/securitysolution-io-ts-utils'; + +import { + RouteValidationError, + RouteValidationFunction, + RouteValidationResultFactory, +} from '../../../../../../src/core/server'; + +type RequestValidationResult = + | { + value: T; + error?: undefined; + } + | { + value?: undefined; + error: RouteValidationError; + }; + +/** + * Copied from x-pack/plugins/security_solution/server/utils/build_validation/route_validation.ts + * This really should be in @kbn/securitysolution-io-ts-utils rather than copied yet again, however, this has types + * from a lot of places such as RouteValidationResultFactory from core/server which in turn can pull in @kbn/schema + * which cannot work on the front end and @kbn/securitysolution-io-ts-utils works on both front and backend. + * + * TODO: Figure out a way to move this function into a package rather than copying it/forking it within plugins + */ +export const buildRouteValidation = >( + schema: T +): RouteValidationFunction => ( + inputValue: unknown, + validationResult: RouteValidationResultFactory +): RequestValidationResult => + pipe( + schema.decode(inputValue), + (decoded) => exactCheck(inputValue, decoded), + fold>( + (errors: rt.Errors) => validationResult.badRequest(formatErrors(errors).join()), + (validatedInput: A) => validationResult.ok(validatedInput) + ) + ); diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/types.ts b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts index 3b90079ec5238..54e9a1b3c9a6f 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/types.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts @@ -11,6 +11,7 @@ import { ElasticsearchClient } from 'kibana/server'; import { FieldDescriptor } from 'src/plugins/data/server'; import { ESSearchRequest, ESSearchResponse } from 'src/core/types/elasticsearch'; import { TechnicalRuleDataFieldName } from '../../common/technical_rule_data_field_names'; +import { ValidFeatureId } from '../utils/rbac'; export interface RuleDataReader { search( @@ -37,9 +38,17 @@ export interface IRuleDataClient { createWriteTargetIfNeeded(options: { namespace?: string }): Promise; } +/** + * The purpose of the `feature` param is to force the user to update + * the data structure which contains the mapping of consumers to alerts + * as data indices. The idea is it is typed such that it forces the + * user to go to the code and modify it. At least until a better system + * is put in place or we move the alerts as data client out of rule registry. + */ export interface RuleDataClientConstructorOptions { getClusterClient: () => Promise; isWriteEnabled: boolean; ready: () => Promise; alias: string; + feature: ValidFeatureId; } diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts index 33ff5281147e1..d84f85dbc99b7 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts @@ -20,10 +20,11 @@ import { ClusterPutComponentTemplateBody, PutIndexTemplateRequest } from '../../ import { RuleDataClient } from '../rule_data_client'; import { RuleDataWriteDisabledError } from './errors'; import { incrementIndexName } from './utils'; +import { ValidFeatureId } from '../utils/rbac'; const BOOTSTRAP_TIMEOUT = 60000; -interface RuleDataPluginServiceConstructorOptions { +export interface RuleDataPluginServiceConstructorOptions { getClusterClient: () => Promise; logger: Logger; isWriteEnabled: boolean; @@ -223,9 +224,10 @@ export class RuleDataPluginService { return [this.options.index, assetName].filter(Boolean).join('-'); } - getRuleDataClient(alias: string, initialize: () => Promise) { + getRuleDataClient(feature: ValidFeatureId, alias: string, initialize: () => Promise) { return new RuleDataClient({ alias, + feature, getClusterClient: () => this.getClusterClient(), isWriteEnabled: this.isWriteEnabled(), ready: initialize, diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock.ts new file mode 100644 index 0000000000000..275d68621864f --- /dev/null +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock.ts @@ -0,0 +1,35 @@ +/* + * 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 { PublicMethodsOf } from '@kbn/utility-types'; +import { RuleDataPluginService, RuleDataPluginServiceConstructorOptions } from './'; + +type Schema = PublicMethodsOf; + +const createRuleDataPluginServiceMock = (_: RuleDataPluginServiceConstructorOptions) => { + const mocked: jest.Mocked = { + init: jest.fn(), + isReady: jest.fn(), + wait: jest.fn(), + isWriteEnabled: jest.fn(), + getFullAssetName: jest.fn(), + createOrUpdateComponentTemplate: jest.fn(), + createOrUpdateIndexTemplate: jest.fn(), + createOrUpdateLifecyclePolicy: jest.fn(), + getRuleDataClient: jest.fn(), + updateIndexMappingsMatchingPattern: jest.fn(), + }; + return mocked; +}; + +export const ruleDataPluginServiceMock: { + create: ( + _: RuleDataPluginServiceConstructorOptions + ) => jest.Mocked>; +} = { + create: createRuleDataPluginServiceMock, +}; diff --git a/x-pack/plugins/rule_registry/server/scripts/README.md b/x-pack/plugins/rule_registry/server/scripts/README.md new file mode 100644 index 0000000000000..2b3f01f3c4d6b --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/README.md @@ -0,0 +1,24 @@ +Users with roles granting them access to monitoring (observability) and siem (security solution) should only be able to access alerts with those roles + +```bash +myterminal~$ ./get_security_solution_alert.sh observer +{ + "statusCode": 404, + "error": "Not Found", + "message": "Unauthorized to get \"rac:8.0.0:securitySolution/get\" alert\"" +} +myterminal~$ ./get_security_solution_alert.sh +{ + "success": true +} +myterminal~$ ./get_observability_alert.sh +{ + "success": true +} +myterminal~$ ./get_observability_alert.sh hunter +{ + "statusCode": 404, + "error": "Not Found", + "message": "Unauthorized to get \"rac:8.0.0:observability/get\" alert\"" +} +``` diff --git a/x-pack/plugins/rule_registry/server/scripts/get_alerts_index.sh b/x-pack/plugins/rule_registry/server/scripts/get_alerts_index.sh new file mode 100755 index 0000000000000..bfa74aa016f02 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/get_alerts_index.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +# +# 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. +# + +set -e + +USER=${1:-'observer'} + +cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd .. + +# Example: ./find_rules.sh +curl -v -k \ + -u $USER:changeme \ + -X GET "${KIBANA_URL}${SPACE_URL}/internal/rac/alerts/index" | jq . + +# -X GET "${KIBANA_URL}${SPACE_URL}/api/apm/settings/apm-alerts-as-data-indices" | jq . diff --git a/x-pack/plugins/rule_registry/server/scripts/get_observability_alert.sh b/x-pack/plugins/rule_registry/server/scripts/get_observability_alert.sh new file mode 100755 index 0000000000000..6fbd0eb3dc816 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/get_observability_alert.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +# +# 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. +# + +set -e + +USER=${1:-'observer'} +ID=${2:-'DHEnOXoB8br9Z2X1fq_l'} + +cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd .. + +# Example: ./get_observability_alert.sh hunter +curl -v -k \ + -u $USER:changeme \ + -X GET "${KIBANA_URL}${SPACE_URL}/internal/rac/alerts?id=$ID&index=.alerts-observability-apm" | jq . diff --git a/x-pack/plugins/rule_registry/server/scripts/get_security_alert.sh b/x-pack/plugins/rule_registry/server/scripts/get_security_alert.sh new file mode 100755 index 0000000000000..9bf051c1c6412 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/get_security_alert.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +# +# 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. +# + +set -e + +USER=${1:-'hunter'} +ID=${2:-'kdL4gHoBFALkyfScIsY5'} + +cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd .. + +# Example: ./get_observability_alert.sh hunter +curl -v -k \ + -u $USER:changeme \ + -X GET "${KIBANA_URL}${SPACE_URL}/internal/rac/alerts?id=$ID&index=.alerts-security-solution" | jq . diff --git a/x-pack/plugins/rule_registry/server/scripts/hunter/README.md b/x-pack/plugins/rule_registry/server/scripts/hunter/README.md new file mode 100644 index 0000000000000..a0269d5b060a3 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/hunter/README.md @@ -0,0 +1,5 @@ +This user can access the monitoring route at http://localhost:5601/security-myfakepath + +| Role | Data Sources | Security Solution ML Jobs/Results | Lists | Rules/Exceptions | Action Connectors | Signals/Alerts | +| :-----------------: | :----------: | :-------------------------------: | :---: | :--------------: | :---------------: | :------------: | +| Hunter / T3 Analyst | read, write | read | read | read, write | read | read, write | diff --git a/x-pack/plugins/rule_registry/server/scripts/hunter/delete_detections_user.sh b/x-pack/plugins/rule_registry/server/scripts/hunter/delete_detections_user.sh new file mode 100755 index 0000000000000..595f0a49282d8 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/hunter/delete_detections_user.sh @@ -0,0 +1,11 @@ + +# +# 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. +# + +curl -v -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XDELETE ${ELASTICSEARCH_URL}/_security/user/hunter diff --git a/x-pack/plugins/rule_registry/server/scripts/hunter/detections_role.json b/x-pack/plugins/rule_registry/server/scripts/hunter/detections_role.json new file mode 100644 index 0000000000000..80f63f80b849c --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/hunter/detections_role.json @@ -0,0 +1,19 @@ +{ + "elasticsearch": { + "cluster": [], + "indices": [] + }, + "kibana": [ + { + "feature": { + "ml": ["read"], + "siem": ["all"], + "actions": ["read"], + "ruleRegistry": ["all"], + "builtInAlerts": ["all"], + "alerting": ["all"] + }, + "spaces": ["*"] + } + ] +} diff --git a/x-pack/plugins/rule_registry/server/scripts/hunter/detections_user.json b/x-pack/plugins/rule_registry/server/scripts/hunter/detections_user.json new file mode 100644 index 0000000000000..f9454cc0ad2fe --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/hunter/detections_user.json @@ -0,0 +1,6 @@ +{ + "password": "changeme", + "roles": ["hunter"], + "full_name": "Hunter", + "email": "detections-reader@example.com" +} diff --git a/x-pack/plugins/rule_registry/server/scripts/hunter/get_detections_role.sh b/x-pack/plugins/rule_registry/server/scripts/hunter/get_detections_role.sh new file mode 100755 index 0000000000000..7ec850ce220bb --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/hunter/get_detections_role.sh @@ -0,0 +1,11 @@ + +# +# 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. +# + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XGET ${KIBANA_URL}/api/security/role/hunter | jq -S . diff --git a/x-pack/plugins/rule_registry/server/scripts/hunter/index.ts b/x-pack/plugins/rule_registry/server/scripts/hunter/index.ts new file mode 100644 index 0000000000000..3411589de7721 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/hunter/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as hunterUser from './detections_user.json'; +import * as hunterRole from './detections_role.json'; +export { hunterUser, hunterRole }; diff --git a/x-pack/plugins/rule_registry/server/scripts/hunter/post_detections_role.sh b/x-pack/plugins/rule_registry/server/scripts/hunter/post_detections_role.sh new file mode 100755 index 0000000000000..debffe0fcac4c --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/hunter/post_detections_role.sh @@ -0,0 +1,14 @@ + +# +# 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. +# + +ROLE=(${@:-./detections_role.json}) + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XPUT ${KIBANA_URL}/api/security/role/hunter \ +-d @${ROLE} diff --git a/x-pack/plugins/rule_registry/server/scripts/hunter/post_detections_user.sh b/x-pack/plugins/rule_registry/server/scripts/hunter/post_detections_user.sh new file mode 100755 index 0000000000000..ab2a053081394 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/hunter/post_detections_user.sh @@ -0,0 +1,14 @@ + +# +# 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. +# + +USER=(${@:-./detections_user.json}) + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + ${ELASTICSEARCH_URL}/_security/user/hunter \ +-d @${USER} diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/README.md b/x-pack/plugins/rule_registry/server/scripts/observer/README.md new file mode 100644 index 0000000000000..dc7e989ba4635 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/README.md @@ -0,0 +1,5 @@ +This user can access the monitoring route at http://localhost:5601/monitoring-myfakepath + +| Role | Data Sources | Security Solution ML Jobs/Results | Lists | Rules/Exceptions | Action Connectors | Signals/Alerts | +| :------: | :----------: | :-------------------------------: | :---: | :--------------: | :---------------: | :------------: | +| observer | read, write | read | read | read, write | read | read, write | \ No newline at end of file diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/delete_detections_user.sh b/x-pack/plugins/rule_registry/server/scripts/observer/delete_detections_user.sh new file mode 100755 index 0000000000000..017d8904a51e1 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/delete_detections_user.sh @@ -0,0 +1,11 @@ + +# +# 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. +# + +curl -v -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XDELETE ${ELASTICSEARCH_URL}/_security/user/observer diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/detections_role.json b/x-pack/plugins/rule_registry/server/scripts/observer/detections_role.json new file mode 100644 index 0000000000000..dd3d3f96e3a33 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/detections_role.json @@ -0,0 +1,20 @@ +{ + "elasticsearch": { + "cluster": [], + "indices": [] + }, + "kibana": [ + { + "feature": { + "ml": ["read"], + "monitoring": ["all"], + "apm": ["minimal_read", "alerts_all"], + "ruleRegistry": ["all"], + "actions": ["read"], + "builtInAlerts": ["all"], + "alerting": ["all"] + }, + "spaces": ["*"] + } + ] +} diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/detections_user.json b/x-pack/plugins/rule_registry/server/scripts/observer/detections_user.json new file mode 100644 index 0000000000000..9f06e7dcc29f1 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/detections_user.json @@ -0,0 +1,6 @@ +{ + "password": "changeme", + "roles": ["observer"], + "full_name": "Observer", + "email": "monitoring-observer@example.com" +} \ No newline at end of file diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/get_detections_role.sh b/x-pack/plugins/rule_registry/server/scripts/observer/get_detections_role.sh new file mode 100755 index 0000000000000..7ec850ce220bb --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/get_detections_role.sh @@ -0,0 +1,11 @@ + +# +# 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. +# + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XGET ${KIBANA_URL}/api/security/role/hunter | jq -S . diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/get_observability_alert.sh b/x-pack/plugins/rule_registry/server/scripts/observer/get_observability_alert.sh new file mode 100755 index 0000000000000..dd71e9dc6af43 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/get_observability_alert.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# +# 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. +# + +set -e + +USER=${1:-'observer'} + +cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd .. + +# Example: ./find_rules.sh +curl -s -k \ + -u $USER:changeme \ + -X GET ${KIBANA_URL}${SPACE_URL}/monitoring-myfakepath | jq . diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/get_security_solution_alert.sh b/x-pack/plugins/rule_registry/server/scripts/observer/get_security_solution_alert.sh new file mode 100755 index 0000000000000..b4348266c9634 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/get_security_solution_alert.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +# +# 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. +# + +set -e + +cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd .. + + +USER=${1:-'hunter'} + +# Example: ./find_rules.sh +curl -s -k \ + -u $USER:changeme \ + -X GET ${KIBANA_URL}${SPACE_URL}/security-myfakepath | jq . diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/index.ts b/x-pack/plugins/rule_registry/server/scripts/observer/index.ts new file mode 100644 index 0000000000000..5feebc1caeed1 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as observerUser from './detections_user.json'; +import * as observerRole from './detections_role.json'; +export { observerUser, observerRole }; diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/post_detections_role.sh b/x-pack/plugins/rule_registry/server/scripts/observer/post_detections_role.sh new file mode 100755 index 0000000000000..4dddb64befc6b --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/post_detections_role.sh @@ -0,0 +1,14 @@ + +# +# 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. +# + +ROLE=(${@:-./detections_role.json}) + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ +-XPUT ${KIBANA_URL}/api/security/role/observer \ +-d @${ROLE} \ No newline at end of file diff --git a/x-pack/plugins/rule_registry/server/scripts/observer/post_detections_user.sh b/x-pack/plugins/rule_registry/server/scripts/observer/post_detections_user.sh new file mode 100755 index 0000000000000..8a897c0d28142 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/observer/post_detections_user.sh @@ -0,0 +1,14 @@ + +# +# 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. +# + +USER=(${@:-./detections_user.json}) + +curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + ${ELASTICSEARCH_URL}/_security/user/observer \ +-d @${USER} \ No newline at end of file diff --git a/x-pack/plugins/rule_registry/server/scripts/update_observability_alert.sh b/x-pack/plugins/rule_registry/server/scripts/update_observability_alert.sh new file mode 100755 index 0000000000000..f61fcf2662aa3 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/scripts/update_observability_alert.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +# +# 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. +# + +set -e + +IDS=${1} +STATUS=${2} + +echo $IDS +echo "'"$STATUS"'" + +cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh +cd .. + +# Example: ./update_observability_alert.sh [\"my-alert-id\",\"another-alert-id\"] +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u observer:changeme \ + -X POST ${KIBANA_URL}${SPACE_URL}/internal/rac/alerts \ + -d "{\"ids\": $IDS, \"status\":\"$STATUS\", \"index\":\".alerts-observability-apm\"}" | jq . diff --git a/x-pack/plugins/rule_registry/server/types.ts b/x-pack/plugins/rule_registry/server/types.ts index 959c05fd1334e..051789b1896bb 100644 --- a/x-pack/plugins/rule_registry/server/types.ts +++ b/x-pack/plugins/rule_registry/server/types.ts @@ -5,13 +5,15 @@ * 2.0. */ +import { RequestHandlerContext } from 'kibana/server'; import { AlertInstanceContext, AlertInstanceState, AlertTypeParams, AlertTypeState, } from '../../alerting/common'; -import { AlertType } from '../../alerting/server'; +import { AlertExecutorOptions, AlertServices, AlertType } from '../../alerting/server'; +import { AlertsClient } from './alert_data_client/alerts_client'; type SimpleAlertType< TParams extends AlertTypeParams = {}, @@ -38,3 +40,31 @@ export type AlertTypeWithExecutor< > & { executor: AlertTypeExecutor; }; + +export type AlertExecutorOptionsWithExtraServices< + Params extends AlertTypeParams = never, + State extends AlertTypeState = never, + InstanceState extends AlertInstanceState = never, + InstanceContext extends AlertInstanceContext = never, + ActionGroupIds extends string = never, + TExtraServices extends {} = never +> = Omit< + AlertExecutorOptions, + 'services' +> & { + services: AlertServices & TExtraServices; +}; + +/** + * @public + */ +export interface RacApiRequestHandlerContext { + getAlertsClient: () => Promise; +} + +/** + * @internal + */ +export interface RacRequestHandlerContext extends RequestHandlerContext { + rac: RacApiRequestHandlerContext; +} diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts new file mode 100644 index 0000000000000..06c2cc8ff005d --- /dev/null +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts @@ -0,0 +1,330 @@ +/* + * 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 { Logger } from '@kbn/logging'; +import { getOrElse } from 'fp-ts/lib/Either'; +import * as rt from 'io-ts'; +import { Mutable } from 'utility-types'; +import { v4 } from 'uuid'; +import { + AlertExecutorOptions, + AlertInstance, + AlertInstanceContext, + AlertInstanceState, + AlertTypeParams, + AlertTypeState, +} from '../../../alerting/server'; +import { ParsedTechnicalFields, parseTechnicalFields } from '../../common/parse_technical_fields'; +import { + ALERT_DURATION, + ALERT_END, + ALERT_ID, + ALERT_START, + ALERT_STATUS, + ALERT_UUID, + EVENT_ACTION, + EVENT_KIND, + OWNER, + RULE_UUID, + TIMESTAMP, +} from '../../common/technical_rule_data_field_names'; +import { RuleDataClient } from '../rule_data_client'; +import { AlertExecutorOptionsWithExtraServices } from '../types'; +import { getRuleData } from './get_rule_executor_data'; + +type LifecycleAlertService< + InstanceState extends AlertInstanceState = never, + InstanceContext extends AlertInstanceContext = never, + ActionGroupIds extends string = never +> = (alert: { + id: string; + fields: Record; +}) => AlertInstance; + +export interface LifecycleAlertServices< + InstanceState extends AlertInstanceState = never, + InstanceContext extends AlertInstanceContext = never, + ActionGroupIds extends string = never +> { + alertWithLifecycle: LifecycleAlertService; +} + +export type LifecycleRuleExecutor< + Params extends AlertTypeParams = never, + State extends AlertTypeState = never, + InstanceState extends AlertInstanceState = never, + InstanceContext extends AlertInstanceContext = never, + ActionGroupIds extends string = never +> = ( + options: AlertExecutorOptionsWithExtraServices< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + LifecycleAlertServices + > +) => Promise; + +const trackedAlertStateRt = rt.type({ + alertId: rt.string, + alertUuid: rt.string, + started: rt.string, +}); + +export type TrackedLifecycleAlertState = rt.TypeOf; + +const alertTypeStateRt = () => + rt.record(rt.string, rt.unknown) as rt.Type; + +const wrappedStateRt = () => + rt.type({ + wrapped: alertTypeStateRt(), + trackedAlerts: rt.record(rt.string, trackedAlertStateRt), + }); + +/** + * This is redefined instead of derived from above `wrappedStateRt` because + * there's no easy way to instantiate generic values such as the runtime type + * factory function. + */ +export type WrappedLifecycleRuleState = AlertTypeState & { + wrapped: State | void; + trackedAlerts: Record; +}; + +export const createLifecycleExecutor = (logger: Logger, ruleDataClient: RuleDataClient) => < + Params extends AlertTypeParams = never, + State extends AlertTypeState = never, + InstanceState extends AlertInstanceState = never, + InstanceContext extends AlertInstanceContext = never, + ActionGroupIds extends string = never +>( + wrappedExecutor: LifecycleRuleExecutor< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds + > +) => async ( + options: AlertExecutorOptions< + Params, + WrappedLifecycleRuleState, + InstanceState, + InstanceContext, + ActionGroupIds + > +): Promise> => { + const { + rule, + services: { alertInstanceFactory }, + state: previousState, + } = options; + + const ruleExecutorData = getRuleData(options); + + const state = getOrElse( + (): WrappedLifecycleRuleState => ({ + wrapped: previousState as State, + trackedAlerts: {}, + }) + )(wrappedStateRt().decode(previousState)); + + const currentAlerts: Record = {}; + + const timestamp = options.startedAt.toISOString(); + + const lifecycleAlertServices: LifecycleAlertServices< + InstanceState, + InstanceContext, + ActionGroupIds + > = { + alertWithLifecycle: ({ id, fields }) => { + currentAlerts[id] = { + ...fields, + [ALERT_ID]: id, + }; + return alertInstanceFactory(id); + }, + }; + + const nextWrappedState = await wrappedExecutor({ + ...options, + state: state.wrapped != null ? state.wrapped : ({} as State), + services: { + ...options.services, + ...lifecycleAlertServices, + }, + }); + + const currentAlertIds = Object.keys(currentAlerts); + const trackedAlertIds = Object.keys(state.trackedAlerts); + const newAlertIds = currentAlertIds.filter((alertId) => !trackedAlertIds.includes(alertId)); + + const allAlertIds = [...new Set(currentAlertIds.concat(trackedAlertIds))]; + + const trackedAlertStatesOfRecovered = Object.values(state.trackedAlerts).filter( + (trackedAlertState) => !currentAlerts[trackedAlertState.alertId] + ); + + logger.debug( + `Tracking ${allAlertIds.length} alerts (${newAlertIds.length} new, ${trackedAlertStatesOfRecovered.length} recovered)` + ); + + const alertsDataMap: Record< + string, + { + [ALERT_ID]: string; + } + > = { + ...currentAlerts, + }; + + if (trackedAlertStatesOfRecovered.length) { + const { hits } = await ruleDataClient.getReader().search({ + body: { + query: { + bool: { + filter: [ + { + term: { + [RULE_UUID]: ruleExecutorData[RULE_UUID], + }, + }, + { + terms: { + [ALERT_UUID]: trackedAlertStatesOfRecovered.map( + (trackedAlertState) => trackedAlertState.alertUuid + ), + }, + }, + ], + }, + }, + size: trackedAlertStatesOfRecovered.length, + collapse: { + field: ALERT_UUID, + }, + _source: false, + fields: [{ field: '*', include_unmapped: true }], + sort: { + [TIMESTAMP]: 'desc' as const, + }, + }, + allow_no_indices: true, + }); + + hits.hits.forEach((hit) => { + const fields = parseTechnicalFields(hit.fields); + const alertId = fields[ALERT_ID]!; + alertsDataMap[alertId] = { + ...fields, + [ALERT_ID]: alertId, + }; + }); + } + + const eventsToIndex = allAlertIds.map((alertId) => { + const alertData = alertsDataMap[alertId]; + + if (!alertData) { + logger.warn(`Could not find alert data for ${alertId}`); + } + + const event: Mutable = { + ...alertData, + ...ruleExecutorData, + [TIMESTAMP]: timestamp, + [EVENT_KIND]: 'event', + [OWNER]: rule.consumer, + [ALERT_ID]: alertId, + }; + + const isNew = !state.trackedAlerts[alertId]; + const isRecovered = !currentAlerts[alertId]; + const isActiveButNotNew = !isNew && !isRecovered; + const isActive = !isRecovered; + + const { alertUuid, started } = state.trackedAlerts[alertId] ?? { + alertUuid: v4(), + started: timestamp, + }; + + event[ALERT_START] = started; + event[ALERT_UUID] = alertUuid; + + if (isNew) { + event[EVENT_ACTION] = 'open'; + } + + if (isRecovered) { + event[ALERT_END] = timestamp; + event[EVENT_ACTION] = 'close'; + event[ALERT_STATUS] = 'closed'; + } + + if (isActiveButNotNew) { + event[EVENT_ACTION] = 'active'; + } + + if (isActive) { + event[ALERT_STATUS] = 'open'; + } + + event[ALERT_DURATION] = + (options.startedAt.getTime() - new Date(event[ALERT_START]!).getTime()) * 1000; + + return event; + }); + + if (eventsToIndex.length) { + const alertEvents: Map = new Map(); + + for (const event of eventsToIndex) { + const uuid = event[ALERT_UUID]!; + let storedEvent = alertEvents.get(uuid); + if (!storedEvent) { + storedEvent = event; + } + alertEvents.set(uuid, { + ...storedEvent, + [EVENT_KIND]: 'signal', + }); + } + logger.debug(`Preparing to index ${eventsToIndex.length} alerts.`); + + if (ruleDataClient.isWriteEnabled()) { + await ruleDataClient.getWriter().bulk({ + body: eventsToIndex + .flatMap((event) => [{ index: {} }, event]) + .concat( + Array.from(alertEvents.values()).flatMap((event) => [ + { index: { _id: event[ALERT_UUID]! } }, + event, + ]) + ), + }); + } + } + + const nextTrackedAlerts = Object.fromEntries( + eventsToIndex + .filter((event) => event[ALERT_STATUS] !== 'closed') + .map((event) => { + const alertId = event[ALERT_ID]!; + const alertUuid = event[ALERT_UUID]!; + const started = new Date(event[ALERT_START]!).toISOString(); + return [alertId, { alertId, alertUuid, started }]; + }) + ); + + return { + wrapped: nextWrappedState ?? ({} as State), + trackedAlerts: ruleDataClient.isWriteEnabled() ? nextTrackedAlerts : {}, + }; +}; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts index 38ddbd3f1876b..3e7fbbe5cbc59 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts @@ -38,11 +38,11 @@ function createRule() { }); nextAlerts = []; }, - id: 'test_type', + id: 'ruleTypeId', minimumLicenseRequired: 'basic', isExportable: true, - name: 'Test type', - producer: 'test', + name: 'ruleTypeName', + producer: 'producer', actionVariables: { context: [], params: [], @@ -194,11 +194,12 @@ describe('createLifecycleRuleTypeFactory', () => { "event.kind": "event", "kibana.rac.alert.duration.us": 0, "kibana.rac.alert.id": "opbeans-java", - "kibana.rac.alert.producer": "test", + "kibana.rac.alert.owner": "consumer", + "kibana.rac.alert.producer": "producer", "kibana.rac.alert.start": "2021-06-16T09:01:00.000Z", "kibana.rac.alert.status": "open", - "rule.category": "Test type", - "rule.id": "test_type", + "rule.category": "ruleTypeName", + "rule.id": "ruleTypeId", "rule.name": "name", "rule.uuid": "alertId", "service.name": "opbeans-java", @@ -212,11 +213,12 @@ describe('createLifecycleRuleTypeFactory', () => { "event.kind": "event", "kibana.rac.alert.duration.us": 0, "kibana.rac.alert.id": "opbeans-node", - "kibana.rac.alert.producer": "test", + "kibana.rac.alert.owner": "consumer", + "kibana.rac.alert.producer": "producer", "kibana.rac.alert.start": "2021-06-16T09:01:00.000Z", "kibana.rac.alert.status": "open", - "rule.category": "Test type", - "rule.id": "test_type", + "rule.category": "ruleTypeName", + "rule.id": "ruleTypeId", "rule.name": "name", "rule.uuid": "alertId", "service.name": "opbeans-node", @@ -230,11 +232,12 @@ describe('createLifecycleRuleTypeFactory', () => { "event.kind": "signal", "kibana.rac.alert.duration.us": 0, "kibana.rac.alert.id": "opbeans-java", - "kibana.rac.alert.producer": "test", + "kibana.rac.alert.owner": "consumer", + "kibana.rac.alert.producer": "producer", "kibana.rac.alert.start": "2021-06-16T09:01:00.000Z", "kibana.rac.alert.status": "open", - "rule.category": "Test type", - "rule.id": "test_type", + "rule.category": "ruleTypeName", + "rule.id": "ruleTypeId", "rule.name": "name", "rule.uuid": "alertId", "service.name": "opbeans-java", @@ -248,11 +251,12 @@ describe('createLifecycleRuleTypeFactory', () => { "event.kind": "signal", "kibana.rac.alert.duration.us": 0, "kibana.rac.alert.id": "opbeans-node", - "kibana.rac.alert.producer": "test", + "kibana.rac.alert.owner": "consumer", + "kibana.rac.alert.producer": "producer", "kibana.rac.alert.start": "2021-06-16T09:01:00.000Z", "kibana.rac.alert.status": "open", - "rule.category": "Test type", - "rule.id": "test_type", + "rule.category": "ruleTypeName", + "rule.id": "ruleTypeId", "rule.name": "name", "rule.uuid": "alertId", "service.name": "opbeans-node", diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts index 005af59892b8a..cf1be1bd32013 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts @@ -5,50 +5,26 @@ * 2.0. */ import { Logger } from '@kbn/logging'; -import { isLeft } from 'fp-ts/lib/Either'; -import * as t from 'io-ts'; -import { Mutable } from 'utility-types'; -import v4 from 'uuid/v4'; -import { AlertInstance } from '../../../alerting/server'; import { RuleDataClient } from '..'; import { AlertInstanceContext, AlertInstanceState, AlertTypeParams, + AlertTypeState, } from '../../../alerting/common'; -import { - ALERT_DURATION, - ALERT_END, - ALERT_ID, - ALERT_START, - ALERT_STATUS, - ALERT_UUID, - EVENT_ACTION, - EVENT_KIND, - RULE_UUID, - TIMESTAMP, -} from '../../common/technical_rule_data_field_names'; +import { AlertInstance } from '../../../alerting/server'; import { AlertTypeWithExecutor } from '../types'; -import { ParsedTechnicalFields, parseTechnicalFields } from '../../common/parse_technical_fields'; -import { getRuleExecutorData } from './get_rule_executor_data'; +import { createLifecycleExecutor } from './create_lifecycle_executor'; export type LifecycleAlertService> = (alert: { id: string; fields: Record; }) => AlertInstance; -const trackedAlertStateRt = t.type({ - alertId: t.string, - alertUuid: t.string, - started: t.string, -}); - -const wrappedStateRt = t.type({ - wrapped: t.record(t.string, t.unknown), - trackedAlerts: t.record(t.string, trackedAlertStateRt), -}); - -type CreateLifecycleRuleTypeFactory = (options: { +export const createLifecycleRuleTypeFactory = ({ + logger, + ruleDataClient, +}: { ruleDataClient: RuleDataClient; logger: Logger; }) => < @@ -57,213 +33,17 @@ type CreateLifecycleRuleTypeFactory = (options: { TServices extends { alertWithLifecycle: LifecycleAlertService } >( type: AlertTypeWithExecutor -) => AlertTypeWithExecutor; - -export const createLifecycleRuleTypeFactory: CreateLifecycleRuleTypeFactory = ({ - logger, - ruleDataClient, -}) => (type) => { +): AlertTypeWithExecutor => { + const createBoundLifecycleExecutor = createLifecycleExecutor(logger, ruleDataClient); + const executor = createBoundLifecycleExecutor< + TParams, + AlertTypeState, + AlertInstanceState, + TAlertInstanceContext, + string + >(type.executor as any); return { ...type, - executor: async (options) => { - const { - services: { alertInstanceFactory }, - state: previousState, - } = options; - - const ruleExecutorData = getRuleExecutorData(type, options); - - const decodedState = wrappedStateRt.decode(previousState); - - const state = isLeft(decodedState) - ? { - wrapped: previousState, - trackedAlerts: {}, - } - : decodedState.right; - - const currentAlerts: Record = {}; - - const timestamp = options.startedAt.toISOString(); - - const nextWrappedState = await type.executor({ - ...options, - state: state.wrapped, - services: { - ...options.services, - alertWithLifecycle: ({ id, fields }) => { - currentAlerts[id] = { - ...fields, - [ALERT_ID]: id, - }; - return alertInstanceFactory(id); - }, - }, - }); - - const currentAlertIds = Object.keys(currentAlerts); - const trackedAlertIds = Object.keys(state.trackedAlerts); - const newAlertIds = currentAlertIds.filter((alertId) => !trackedAlertIds.includes(alertId)); - - const allAlertIds = [...new Set(currentAlertIds.concat(trackedAlertIds))]; - - const trackedAlertStatesOfRecovered = Object.values(state.trackedAlerts).filter( - (trackedAlertState) => !currentAlerts[trackedAlertState.alertId] - ); - - logger.debug( - `Tracking ${allAlertIds.length} alerts (${newAlertIds.length} new, ${trackedAlertStatesOfRecovered.length} recovered)` - ); - - const alertsDataMap: Record< - string, - { - [ALERT_ID]: string; - } - > = { - ...currentAlerts, - }; - - if (trackedAlertStatesOfRecovered.length) { - const { hits } = await ruleDataClient.getReader().search({ - body: { - query: { - bool: { - filter: [ - { - term: { - [RULE_UUID]: ruleExecutorData[RULE_UUID], - }, - }, - { - terms: { - [ALERT_UUID]: trackedAlertStatesOfRecovered.map( - (trackedAlertState) => trackedAlertState.alertUuid - ), - }, - }, - ], - }, - }, - size: trackedAlertStatesOfRecovered.length, - collapse: { - field: ALERT_UUID, - }, - _source: false, - fields: [{ field: '*', include_unmapped: true }], - sort: { - [TIMESTAMP]: 'desc' as const, - }, - }, - allow_no_indices: true, - }); - - hits.hits.forEach((hit) => { - const fields = parseTechnicalFields(hit.fields); - const alertId = fields[ALERT_ID]!; - alertsDataMap[alertId] = { - ...fields, - [ALERT_ID]: alertId, - }; - }); - } - - const eventsToIndex = allAlertIds.map((alertId) => { - const alertData = alertsDataMap[alertId]; - - if (!alertData) { - logger.warn(`Could not find alert data for ${alertId}`); - } - - const event: Mutable = { - ...alertData, - ...ruleExecutorData, - [TIMESTAMP]: timestamp, - [EVENT_KIND]: 'event', - [ALERT_ID]: alertId, - }; - - const isNew = !state.trackedAlerts[alertId]; - const isRecovered = !currentAlerts[alertId]; - const isActiveButNotNew = !isNew && !isRecovered; - const isActive = !isRecovered; - - const { alertUuid, started } = state.trackedAlerts[alertId] ?? { - alertUuid: v4(), - started: timestamp, - }; - - event[ALERT_START] = started; - event[ALERT_UUID] = alertUuid; - - if (isNew) { - event[EVENT_ACTION] = 'open'; - } - - if (isRecovered) { - event[ALERT_END] = timestamp; - event[EVENT_ACTION] = 'close'; - event[ALERT_STATUS] = 'closed'; - } - - if (isActiveButNotNew) { - event[EVENT_ACTION] = 'active'; - } - - if (isActive) { - event[ALERT_STATUS] = 'open'; - } - - event[ALERT_DURATION] = - (options.startedAt.getTime() - new Date(event[ALERT_START]!).getTime()) * 1000; - - return event; - }); - - if (eventsToIndex.length) { - const alertEvents: Map = new Map(); - - for (const event of eventsToIndex) { - const uuid = event[ALERT_UUID]!; - let storedEvent = alertEvents.get(uuid); - if (!storedEvent) { - storedEvent = event; - } - alertEvents.set(uuid, { - ...storedEvent, - [EVENT_KIND]: 'signal', - }); - } - - if (ruleDataClient.isWriteEnabled()) { - await ruleDataClient.getWriter().bulk({ - body: eventsToIndex - .flatMap((event) => [{ index: {} }, event]) - .concat( - Array.from(alertEvents.values()).flatMap((event) => [ - { index: { _id: event[ALERT_UUID]! } }, - event, - ]) - ), - }); - } - } - - const nextTrackedAlerts = Object.fromEntries( - eventsToIndex - .filter((event) => event[ALERT_STATUS] !== 'closed') - .map((event) => { - const alertId = event[ALERT_ID]!; - const alertUuid = event[ALERT_UUID]!; - const started = new Date(event[ALERT_START]!).toISOString(); - return [alertId, { alertId, alertUuid, started }]; - }) - ); - - return { - wrapped: nextWrappedState ?? {}, - trackedAlerts: ruleDataClient.isWriteEnabled() ? nextTrackedAlerts : {}, - }; - }, + executor: executor as any, }; }; diff --git a/x-pack/plugins/rule_registry/server/utils/get_rule_executor_data.ts b/x-pack/plugins/rule_registry/server/utils/get_rule_executor_data.ts index 1ea640add7b48..7cb02428322a6 100644 --- a/x-pack/plugins/rule_registry/server/utils/get_rule_executor_data.ts +++ b/x-pack/plugins/rule_registry/server/utils/get_rule_executor_data.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { AlertExecutorOptions } from '../../../alerting/server'; import { PRODUCER, RULE_CATEGORY, @@ -37,3 +38,14 @@ export function getRuleExecutorData( [PRODUCER]: type.producer, }; } + +export function getRuleData(options: AlertExecutorOptions) { + return { + [RULE_ID]: options.rule.ruleTypeId, + [RULE_UUID]: options.alertId, + [RULE_CATEGORY]: options.rule.ruleTypeName, + [RULE_NAME]: options.rule.name, + [TAGS]: options.tags, + [PRODUCER]: options.rule.producer, + }; +} diff --git a/x-pack/plugins/rule_registry/server/utils/rbac.ts b/x-pack/plugins/rule_registry/server/utils/rbac.ts new file mode 100644 index 0000000000000..e07c4394be2f1 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/utils/rbac.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +/** + * registering a new instance of the rule data client + * in a new plugin will require updating the below data structure + * to include the index name where the alerts as data will be written to. + * + * This doesn't work in combination with the `xpack.ruleRegistry.index` + * setting, with which the user can change the index prefix. + */ +export const mapConsumerToIndexName = { + apm: '.alerts-observability-apm', + logs: '.alerts-observability.logs', + infrastructure: '.alerts-observability.metrics', + observability: '.alerts-observability', + siem: ['.alerts-security.alerts', '.siem-signals'], +}; +export type ValidFeatureId = keyof typeof mapConsumerToIndexName; + +export const validFeatureIds = Object.keys(mapConsumerToIndexName); +export const isValidFeatureId = (a: unknown): a is ValidFeatureId => + typeof a === 'string' && validFeatureIds.includes(a); diff --git a/x-pack/plugins/rule_registry/tsconfig.json b/x-pack/plugins/rule_registry/tsconfig.json index 5aefe9769da22..f6253e441da31 100644 --- a/x-pack/plugins/rule_registry/tsconfig.json +++ b/x-pack/plugins/rule_registry/tsconfig.json @@ -7,11 +7,19 @@ "declaration": true, "declarationMap": true }, - "include": ["common/**/*", "server/**/*", "public/**/*", "../../../typings/**/*"], + "include": [ + "common/**/*", + "server/**/*", + // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 + "server/**/*.json", + "public/**/*", + "../../../typings/**/*" + ], "references": [ { "path": "../../../src/core/tsconfig.json" }, { "path": "../../../src/plugins/data/tsconfig.json" }, { "path": "../alerting/tsconfig.json" }, + { "path": "../security/tsconfig.json" }, { "path": "../spaces/tsconfig.json" }, { "path": "../triggers_actions_ui/tsconfig.json" } ] diff --git a/x-pack/plugins/security_solution/common/cti/constants.ts b/x-pack/plugins/security_solution/common/cti/constants.ts index 631a13df1ecb1..7b22e9036f566 100644 --- a/x-pack/plugins/security_solution/common/cti/constants.ts +++ b/x-pack/plugins/security_solution/common/cti/constants.ts @@ -65,9 +65,9 @@ export const CTI_DEFAULT_SOURCES = [ 'Abuse Malware', 'AlienVault OTX', 'Anomali', - 'Anomali ThreatStream', 'Malware Bazaar', 'MISP', + 'Recorded Future', ]; export const DEFAULT_CTI_SOURCE_INDEX = ['filebeat-*']; diff --git a/x-pack/plugins/security_solution/common/ecs/file/index.ts b/x-pack/plugins/security_solution/common/ecs/file/index.ts index 5e409b1095cf5..0c9dde20011f4 100644 --- a/x-pack/plugins/security_solution/common/ecs/file/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/file/index.ts @@ -14,9 +14,42 @@ export interface CodeSignature { subject_name: string[]; trusted: string[]; } + +export interface Token { + integrity_level_name: string; +} + +export interface MemoryPe { + imphash?: string; +} + +export interface StartAddressDetails { + allocation_base?: number; + allocation_protection?: string; + allocation_size?: number; + allocation_type?: string; + bytes_address?: number; + bytes_allocation_offset?: number; + bytes_compressed?: string; + bytes_compressed_present?: string; + mapped_path?: string; + mapped_pe_detected?: boolean; + memory_pe_detected?: boolean; + region_base?: number; + region_protection?: string; + region_size?: number; + region_state?: string; + strings?: string; + memory_pe?: MemoryPe; +} + export interface Ext { code_signature?: CodeSignature[] | CodeSignature; original?: Original; + token?: Token; + start_address_allocation_offset?: number; + start_address_bytes_disasm_hash?: string; + start_address_details?: StartAddressDetails; } export interface Hash { md5?: string[]; diff --git a/x-pack/plugins/security_solution/common/ecs/index.ts b/x-pack/plugins/security_solution/common/ecs/index.ts index 8054b3c8521db..610a2fd1f6e9e 100644 --- a/x-pack/plugins/security_solution/common/ecs/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/index.ts @@ -30,6 +30,8 @@ import { ProcessEcs } from './process'; import { SystemEcs } from './system'; import { ThreatEcs } from './threat'; import { Ransomware } from './ransomware'; +import { MemoryProtection } from './memory_protection'; +import { Target } from './target_type'; export interface Ecs { _id: string; @@ -63,4 +65,7 @@ export interface Ecs { // This should be temporary eql?: { parentId: string; sequenceNumber: string }; Ransomware?: Ransomware; + // eslint-disable-next-line @typescript-eslint/naming-convention + Memory_protection?: MemoryProtection; + Target?: Target; } diff --git a/x-pack/plugins/security_solution/common/ecs/memory_protection/index.ts b/x-pack/plugins/security_solution/common/ecs/memory_protection/index.ts new file mode 100644 index 0000000000000..8115fc0dcd26e --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/memory_protection/index.ts @@ -0,0 +1,13 @@ +/* + * 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 MemoryProtection { + cross_session?: boolean; + feature?: string; + parent_to_child?: boolean; + self_injection?: boolean; + unique_key_v1?: string; +} diff --git a/x-pack/plugins/security_solution/common/ecs/process/index.ts b/x-pack/plugins/security_solution/common/ecs/process/index.ts index 820ecc5560e6c..0eb2400466e64 100644 --- a/x-pack/plugins/security_solution/common/ecs/process/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/process/index.ts @@ -37,4 +37,5 @@ export interface ProcessParentData { export interface Thread { id?: number[]; start?: string[]; + Ext?: Ext; } diff --git a/x-pack/plugins/security_solution/common/ecs/target_type/index.ts b/x-pack/plugins/security_solution/common/ecs/target_type/index.ts new file mode 100644 index 0000000000000..3c19b51173a04 --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/target_type/index.ts @@ -0,0 +1,12 @@ +/* + * 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 { ProcessEcs } from '../process'; + +export interface Target { + process: ProcessEcs; +} diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index 876cb3866c614..255ab8f0a598c 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -7,6 +7,7 @@ import uuid from 'uuid'; import seedrandom from 'seedrandom'; +import { assertNever } from '@kbn/std'; import { AlertEvent, DataStream, @@ -387,6 +388,12 @@ const eventsDefaultDataStream = { namespace: 'default', }; +enum AlertTypes { + MALWARE = 'MALWARE', + MEMORY_SIGNATURE = 'MEMORY_SIGNATURE', + MEMORY_SHELLCODE = 'MEMORY_SHELLCODE', +} + const alertsDefaultDataStream = { type: 'logs', dataset: 'endpoint.alerts', @@ -509,16 +516,15 @@ export class EndpointDocGenerator extends BaseDataGenerator { data_stream: metadataDataStream, }; } - /** - * Creates an alert from the simulated host represented by this EndpointDocGenerator + * Creates a malware alert from the simulated host represented by this EndpointDocGenerator * @param ts - Timestamp to put in the event * @param entityID - entityID of the originating process * @param parentEntityID - optional entityID of the parent process, if it exists * @param ancestry - an array of ancestors for the generated alert * @param alertsDataStream the values to populate the data_stream fields when generating alert documents */ - public generateAlert({ + public generateMalwareAlert({ ts = new Date().getTime(), entityID = this.randomString(10), parentEntityID, @@ -619,37 +625,198 @@ export class EndpointDocGenerator extends BaseDataGenerator { }, }, }, - dll: [ - { - pe: { - architecture: 'x64', + dll: this.getAlertsDefaultDll(), + }; + } + + /** + * Creates a memory alert from the simulated host represented by this EndpointDocGenerator + * @param ts - Timestamp to put in the event + * @param entityID - entityID of the originating process + * @param parentEntityID - optional entityID of the parent process, if it exists + * @param ancestry - an array of ancestors for the generated alert + * @param alertsDataStream the values to populate the data_stream fields when generating alert documents + */ + public generateMemoryAlert({ + ts = new Date().getTime(), + entityID = this.randomString(10), + parentEntityID, + ancestry = [], + alertsDataStream = alertsDefaultDataStream, + alertType, + }: { + ts?: number; + entityID?: string; + parentEntityID?: string; + ancestry?: string[]; + alertsDataStream?: DataStream; + alertType?: AlertTypes; + } = {}): AlertEvent { + const processName = this.randomProcessName(); + const isShellcode = alertType === AlertTypes.MEMORY_SHELLCODE; + const newAlert: AlertEvent = { + ...this.commonInfo, + data_stream: alertsDataStream, + '@timestamp': ts, + ecs: { + version: '1.6.0', + }, + // disabling naming-convention to accommodate external field + // eslint-disable-next-line @typescript-eslint/naming-convention + Memory_protection: { + feature: isShellcode ? 'shellcode_thread' : 'signature', + self_injection: true, + }, + event: { + action: 'start', + kind: 'alert', + category: 'malware', + code: isShellcode ? 'malicious_thread' : 'memory_signature', + id: this.seededUUIDv4(), + dataset: 'endpoint', + module: 'endpoint', + type: 'info', + sequence: this.sequence++, + }, + file: {}, + process: { + pid: 2, + name: processName, + start: ts, + uptime: 0, + entity_id: entityID, + executable: `C:/fake/${processName}`, + parent: parentEntityID ? { entity_id: parentEntityID, pid: 1 } : undefined, + hash: { + md5: 'fake md5', + sha1: 'fake sha1', + sha256: 'fake sha256', + }, + Ext: { + ancestry, + code_signature: [ + { + trusted: false, + subject_name: 'bad signer', + }, + ], + user: 'SYSTEM', + token: { + integrity_level_name: 'high', }, - code_signature: { - subject_name: 'Cybereason Inc', - trusted: true, + malware_signature: { + all_names: 'Windows.Trojan.FakeAgent', + identifier: 'diagnostic-malware-signature-v1-fake', }, + }, + }, + dll: this.getAlertsDefaultDll(), + }; - hash: { - md5: '1f2d082566b0fc5f2c238a5180db7451', - sha1: 'ca85243c0af6a6471bdaa560685c51eefd6dbc0d', - sha256: '8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2', + // shellcode_thread memory alert have an additional process field + if (isShellcode) { + newAlert.Target = { + process: { + thread: { + Ext: { + start_address_allocation_offset: 0, + start_address_bytes_disasm_hash: 'a disam hash', + start_address_details: { + allocation_type: 'PRIVATE', + allocation_size: 4000, + region_size: 4000, + region_protection: 'RWX', + memory_pe: { + imphash: 'a hash', + }, + }, + }, }, + }, + }; + } + return newAlert; + } + /** + * Creates an alert from the simulated host represented by this EndpointDocGenerator + * @param ts - Timestamp to put in the event + * @param entityID - entityID of the originating process + * @param parentEntityID - optional entityID of the parent process, if it exists + * @param ancestry - an array of ancestors for the generated alert + * @param alertsDataStream the values to populate the data_stream fields when generating alert documents + */ + public generateAlert({ + ts = new Date().getTime(), + entityID = this.randomString(10), + parentEntityID, + ancestry = [], + alertsDataStream = alertsDefaultDataStream, + }: { + ts?: number; + entityID?: string; + parentEntityID?: string; + ancestry?: string[]; + alertsDataStream?: DataStream; + } = {}): AlertEvent { + const alertType = this.randomChoice(Object.values(AlertTypes)); + switch (alertType) { + case AlertTypes.MALWARE: + return this.generateMalwareAlert({ + ts, + entityID, + parentEntityID, + ancestry, + alertsDataStream, + }); + case AlertTypes.MEMORY_SIGNATURE: + case AlertTypes.MEMORY_SHELLCODE: + return this.generateMemoryAlert({ + ts, + entityID, + parentEntityID, + ancestry, + alertsDataStream, + alertType, + }); + default: + return assertNever(alertType); + } + } - path: 'C:\\Program Files\\Cybereason ActiveProbe\\AmSvc.exe', - Ext: { - compile_time: 1534424710, - mapped_address: 5362483200, - mapped_size: 0, - malware_classification: { - identifier: 'Whitelisted', - score: 0, - threshold: 0, - version: '3.0.0', - }, + /** + * Returns the default DLLs used in alerts + */ + private getAlertsDefaultDll() { + return [ + { + pe: { + architecture: 'x64', + }, + code_signature: { + subject_name: 'Cybereason Inc', + trusted: true, + }, + + hash: { + md5: '1f2d082566b0fc5f2c238a5180db7451', + sha1: 'ca85243c0af6a6471bdaa560685c51eefd6dbc0d', + sha256: '8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2', + }, + + path: 'C:\\Program Files\\Cybereason ActiveProbe\\AmSvc.exe', + Ext: { + compile_time: 1534424710, + mapped_address: 5362483200, + mapped_size: 0, + malware_classification: { + identifier: 'Whitelisted', + score: 0, + threshold: 0, + version: '3.0.0', }, }, - ], - }; + }, + ]; } /** diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts b/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts index fd4d89540f0ce..98cb7729c9440 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts @@ -23,6 +23,8 @@ export const EndpointActionLogRequestSchema = { query: schema.object({ page: schema.number({ defaultValue: 1, min: 1 }), page_size: schema.number({ defaultValue: 10, min: 1, max: 100 }), + start_date: schema.maybe(schema.string()), + end_date: schema.maybe(schema.string()), }), params: schema.object({ agent_id: schema.string(), diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index dfaad68e295eb..25fc831ca0aa4 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -60,6 +60,8 @@ export type ActivityLogEntry = ActivityLogAction | ActivityLogActionResponse; export interface ActivityLog { page: number; pageSize: number; + startDate?: string; + endDate?: string; data: ActivityLogEntry[]; } diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index 1e0d798cf7f07..076eb51a5fdc5 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -178,8 +178,6 @@ export interface HostResultList { request_page_size: number; /* the page index requested */ request_page_index: number; - /* the version of the query strategy */ - query_strategy_version: MetadataQueryStrategyVersions; /* policy IDs and versions */ policy_info?: HostInfo['policy_info']; } @@ -297,6 +295,31 @@ export type AlertEvent = Partial<{ }>; }>; }>; + // disabling naming-convention to accommodate external field + // eslint-disable-next-line @typescript-eslint/naming-convention + Memory_protection: Partial<{ + feature: ECSField; + self_injection: ECSField; + }>; + Target: Partial<{ + process: Partial<{ + thread: Partial<{ + Ext: Partial<{ + start_address_allocation_offset: ECSField; + start_address_bytes_disasm_hash: ECSField; + start_address_details: Partial<{ + allocation_type: ECSField; + allocation_size: ECSField; + region_size: ECSField; + region_protection: ECSField; + memory_pe: Partial<{ + imphash: ECSField; + }>; + }>; + }>; + }>; + }>; + }>; process: Partial<{ command_line: ECSField; ppid: ECSField; @@ -330,6 +353,10 @@ export type AlertEvent = Partial<{ >; }>; user: ECSField; + malware_signature: Partial<{ + all_names: ECSField; + identifier: ECSField; + }>; }>; }>; file: Partial<{ @@ -402,11 +429,11 @@ export enum HostStatus { * Host is inactive as indicated by its checkin status during the last checkin window */ INACTIVE = 'inactive', -} -export enum MetadataQueryStrategyVersions { - VERSION_1 = 'v1', - VERSION_2 = 'v2', + /** + * Host is unenrolled + */ + UNENROLLED = 'unenrolled', } export type PolicyInfo = Immutable<{ @@ -414,11 +441,6 @@ export type PolicyInfo = Immutable<{ id: string; }>; -export interface HostMetadataInfo { - metadata: HostMetadata; - query_strategy_version: MetadataQueryStrategyVersions; -} - export type HostInfo = Immutable<{ metadata: HostMetadata; host_status: HostStatus; @@ -438,8 +460,6 @@ export type HostInfo = Immutable<{ */ endpoint: PolicyInfo; }; - /* the version of the query strategy */ - query_strategy_version: MetadataQueryStrategyVersions; }>; // HostMetadataDetails is now just HostMetadata diff --git a/x-pack/plugins/security_solution/cypress/ccs_integration/detection_alerts/alerts_details.spec.ts b/x-pack/plugins/security_solution/cypress/ccs_integration/detection_alerts/alerts_details.spec.ts index f87399a666904..229bbcce87696 100644 --- a/x-pack/plugins/security_solution/cypress/ccs_integration/detection_alerts/alerts_details.spec.ts +++ b/x-pack/plugins/security_solution/cypress/ccs_integration/detection_alerts/alerts_details.spec.ts @@ -18,7 +18,7 @@ import { cleanKibana } from '../../tasks/common'; import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { esArchiverCCSLoad, esArchiverCCSUnload } from '../../tasks/es_archiver'; -import { unmappedCCSRule } from '../../objects/rule'; +import { getUnmappedCCSRule } from '../../objects/rule'; import { ALERTS_URL } from '../../urls/navigation'; @@ -29,7 +29,7 @@ describe('Alert details with unmapped fields', () => { loginAndWaitForPageWithoutDateRange(ALERTS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); - createCustomRuleActivated(unmappedCCSRule); + createCustomRuleActivated(getUnmappedCCSRule()); loginAndWaitForPageWithoutDateRange(ALERTS_URL); waitForAlertsPanelToBeLoaded(); expandFirstAlert(); diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts index 29105ce1582cf..e94f7d00f0b37 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts @@ -13,8 +13,8 @@ import { selectCase, } from '../../tasks/timeline'; import { DESCRIPTION_INPUT, ADD_COMMENT_INPUT } from '../../screens/create_new_case'; -import { case1 } from '../../objects/case'; -import { timeline } from '../../objects/timeline'; +import { getCase1 } from '../../objects/case'; +import { getTimeline } from '../../objects/timeline'; import { createTimeline } from '../../tasks/api_calls/timelines'; import { cleanKibana } from '../../tasks/common'; import { createCase } from '../../tasks/api_calls/cases'; @@ -23,7 +23,7 @@ describe('attach timeline to case', () => { context('without cases created', () => { beforeEach(() => { cleanKibana(); - createTimeline(timeline).then((response) => { + createTimeline(getTimeline()).then((response) => { cy.wrap(response.body.data.persistTimeline.timeline).as('myTimeline'); }); }); @@ -57,10 +57,10 @@ describe('attach timeline to case', () => { context('with cases created', () => { before(() => { cleanKibana(); - createTimeline(timeline).then((response) => + createTimeline(getTimeline()).then((response) => cy.wrap(response.body.data.persistTimeline.timeline.savedObjectId).as('timelineId') ); - createCase(case1).then((response) => cy.wrap(response.body.id).as('caseId')); + createCase(getCase1()).then((response) => cy.wrap(response.body.id).as('caseId')); }); it('attach timeline to an existing case', function () { @@ -71,7 +71,9 @@ describe('attach timeline to case', () => { cy.location('origin').then((origin) => { cy.get(ADD_COMMENT_INPUT).should( 'have.text', - `[${timeline.title}](${origin}/app/security/timelines?timeline=(id:%27${this.timelineId}%27,isOpen:!t))` + `[${getTimeline().title}](${origin}/app/security/timelines?timeline=(id:%27${ + this.timelineId + }%27,isOpen:!t))` ); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/connector_options.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/connector_options.spec.ts index 95b555c2acae6..0959f999a4b53 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/connector_options.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases/connector_options.spec.ts @@ -7,13 +7,13 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { - case1, - connectorIds, - mockConnectorsResponse, - executeResponses, - ibmResilientConnectorOptions, - jiraConnectorOptions, - serviceNowConnectorOpions, + getCase1, + getConnectorIds, + getMockConnectorsResponse, + getExecuteResponses, + getIbmResilientConnectorOptions, + getJiraConnectorOptions, + getServiceNowConnectorOptions, } from '../../objects/case'; import { createCase, @@ -30,26 +30,26 @@ import { cleanKibana } from '../../tasks/common'; describe('Cases connector incident fields', () => { beforeEach(() => { cleanKibana(); - cy.intercept('GET', '/api/cases/configure/connectors/_find', mockConnectorsResponse); - cy.intercept('POST', `/api/actions/action/${connectorIds.sn}/_execute`, (req) => { + cy.intercept('GET', '/api/cases/configure/connectors/_find', getMockConnectorsResponse()); + cy.intercept('POST', `/api/actions/action/${getConnectorIds().sn}/_execute`, (req) => { const response = req.body.params.subAction === 'getChoices' - ? executeResponses.servicenow.choices + ? getExecuteResponses().servicenow.choices : { status: 'ok', data: [] }; req.reply(response); }); - cy.intercept('POST', `/api/actions/action/${connectorIds.jira}/_execute`, (req) => { + cy.intercept('POST', `/api/actions/action/${getConnectorIds().jira}/_execute`, (req) => { const response = req.body.params.subAction === 'issueTypes' - ? executeResponses.jira.issueTypes - : executeResponses.jira.fieldsByIssueType; + ? getExecuteResponses().jira.issueTypes + : getExecuteResponses().jira.fieldsByIssueType; req.reply(response); }); - cy.intercept('POST', `/api/actions/action/${connectorIds.resilient}/_execute`, (req) => { + cy.intercept('POST', `/api/actions/action/${getConnectorIds().resilient}/_execute`, (req) => { const response = req.body.params.subAction === 'incidentTypes' - ? executeResponses.resilient.incidentTypes - : executeResponses.resilient.severity; + ? getExecuteResponses().resilient.incidentTypes + : getExecuteResponses().resilient.severity; req.reply(response); }); }); @@ -57,19 +57,19 @@ describe('Cases connector incident fields', () => { it('Correct incident fields show when connector is changed', () => { loginAndWaitForPageWithoutDateRange(CASES_URL); goToCreateNewCase(); - fillCasesMandatoryfields(case1); - fillJiraConnectorOptions(jiraConnectorOptions); - fillServiceNowConnectorOptions(serviceNowConnectorOpions); - fillIbmResilientConnectorOptions(ibmResilientConnectorOptions); + fillCasesMandatoryfields(getCase1()); + fillJiraConnectorOptions(getJiraConnectorOptions()); + fillServiceNowConnectorOptions(getServiceNowConnectorOptions()); + fillIbmResilientConnectorOptions(getIbmResilientConnectorOptions()); createCase(); - cy.get(CONNECTOR_TITLE).should('have.text', ibmResilientConnectorOptions.title); + cy.get(CONNECTOR_TITLE).should('have.text', getIbmResilientConnectorOptions().title); cy.get(CONNECTOR_CARD_DETAILS).should( 'have.text', `${ - ibmResilientConnectorOptions.title - }Incident Types: ${ibmResilientConnectorOptions.incidentTypes.join(', ')}Severity: ${ - ibmResilientConnectorOptions.severity + getIbmResilientConnectorOptions().title + }Incident Types: ${getIbmResilientConnectorOptions().incidentTypes.join(', ')}Severity: ${ + getIbmResilientConnectorOptions().severity }` ); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts index 9e55067ce4ed4..aa1bd7a5db5cc 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { serviceNowConnector } from '../../objects/case'; +import { getServiceNowConnector } from '../../objects/case'; import { SERVICE_NOW_MAPPING, TOASTER } from '../../screens/configure_cases'; @@ -77,7 +77,7 @@ describe('Cases connectors', () => { loginAndWaitForPageWithoutDateRange(CASES_URL); goToEditExternalConnection(); openAddNewConnectorOption(); - addServiceNowConnector(serviceNowConnector); + addServiceNowConnector(getServiceNowConnector()); cy.wait('@createConnector').then(({ response }) => { cy.wrap(response!.statusCode).should('eql', 200); diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts index c568aaae664a0..9e3b775156cab 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { case1 } from '../../objects/case'; +import { getCase1, TestCase } from '../../objects/case'; import { ALL_CASES_CLOSED_CASES_STATS, @@ -55,12 +55,12 @@ import { CASES_URL } from '../../urls/navigation'; describe('Cases', () => { beforeEach(() => { cleanKibana(); - createTimeline(case1.timeline).then((response) => + createTimeline(getCase1().timeline).then((response) => cy .wrap({ - ...case1, + ...getCase1(), timeline: { - ...case1.timeline, + ...getCase1().timeline, id: response.body.data.persistTimeline.timeline.savedObjectId, }, }) @@ -86,7 +86,7 @@ describe('Cases', () => { cy.get(ALL_CASES_TAGS_COUNT).should('have.text', 'Tags2'); cy.get(ALL_CASES_NAME).should('have.text', this.mycase.name); cy.get(ALL_CASES_REPORTER).should('have.text', this.mycase.reporter); - (this.mycase as typeof case1).tags.forEach((tag, index) => { + (this.mycase as TestCase).tags.forEach((tag, index) => { cy.get(ALL_CASES_TAGS(index)).should('have.text', tag); }); cy.get(ALL_CASES_COMMENTS_COUNT).should('have.text', '0'); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts index d9ca43339d412..825cc7f8081e5 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts @@ -18,7 +18,7 @@ import { cleanKibana } from '../../tasks/common'; import { esArchiverLoad } from '../../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; -import { unmappedRule } from '../../objects/rule'; +import { getUnmappedRule } from '../../objects/rule'; import { ALERTS_URL } from '../../urls/navigation'; @@ -29,7 +29,7 @@ describe('Alert details with unmapped fields', () => { loginAndWaitForPageWithoutDateRange(ALERTS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); - createCustomRuleActivated(unmappedRule); + createCustomRuleActivated(getUnmappedRule()); loginAndWaitForPageWithoutDateRange(ALERTS_URL); waitForAlertsPanelToBeLoaded(); expandFirstAlert(); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_detection_callouts_index_outdated.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_detection_callouts_index_outdated.spec.ts index fb0b96c977e32..eaed80c484f60 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_detection_callouts_index_outdated.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_detection_callouts_index_outdated.spec.ts @@ -7,7 +7,7 @@ import { ROLES } from '../../../common/test'; import { DETECTIONS_RULE_MANAGEMENT_URL, ALERTS_URL } from '../../urls/navigation'; -import { newRule } from '../../objects/rule'; +import { getNewRule } from '../../objects/rule'; import { PAGE_TITLE } from '../../screens/common/page'; import { @@ -77,7 +77,7 @@ describe('Detections > Need Admin Callouts indicating an admin is needed to migr context('On Rule Details page', () => { beforeEach(() => { - createCustomRule(newRule); + createCustomRule(getNewRule()); loadPageAsPlatformEngineerUser(DETECTIONS_RULE_MANAGEMENT_URL); waitForPageTitleToBeShown(); goToRuleDetails(); @@ -127,7 +127,7 @@ describe('Detections > Need Admin Callouts indicating an admin is needed to migr context('On Rule Details page', () => { beforeEach(() => { - createCustomRule(newRule); + createCustomRule(getNewRule()); loadPageAsPlatformEngineerUser(DETECTIONS_RULE_MANAGEMENT_URL); waitForPageTitleToBeShown(); goToRuleDetails(); @@ -177,7 +177,7 @@ describe('Detections > Need Admin Callouts indicating an admin is needed to migr context('On Rule Details page', () => { beforeEach(() => { - createCustomRule(newRule); + createCustomRule(getNewRule()); loadPageAsPlatformEngineerUser(DETECTIONS_RULE_MANAGEMENT_URL); waitForPageTitleToBeShown(); goToRuleDetails(); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts index 6cc5d2443e784..e052d1a3272ac 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { newRule } from '../../objects/rule'; +import { getNewRule } from '../../objects/rule'; import { ROLES } from '../../../common/test'; import { waitForAlertsIndexToBeCreated, waitForAlertsPanelToBeLoaded } from '../../tasks/alerts'; @@ -30,7 +30,7 @@ describe('Alerts timeline', () => { loginAndWaitForPage(ALERTS_URL, ROLES.platform_engineer); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); - createCustomRuleActivated(newRule); + createCustomRuleActivated(getNewRule()); refreshPage(); waitForAlertsToPopulate(500); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts index 6ae23733d6434..038bc30c90c1e 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { newRule } from '../../objects/rule'; +import { getNewRule } from '../../objects/rule'; import { ALERTS, ALERTS_COUNT, @@ -39,7 +39,7 @@ describe('Closing alerts', () => { loginAndWaitForPage(ALERTS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); - createCustomRuleActivated(newRule, '1', '100m', 100); + createCustomRuleActivated(getNewRule(), '1', '100m', 100); refreshPage(); waitForAlertsToPopulate(100); deleteCustomRule(); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts index b03daf74ce247..522e25590994f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { newThreatIndicatorRule } from '../../objects/rule'; +import { getNewThreatIndicatorRule } from '../../objects/rule'; import { cleanKibana, reload } from '../../tasks/common'; import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; @@ -39,7 +39,7 @@ describe('CTI Enrichment', () => { esArchiverLoad('suspicious_source_event'); loginAndWaitForPageWithoutDateRange(ALERTS_URL); goToManageAlertsDetectionRules(); - createCustomIndicatorRule(newThreatIndicatorRule); + createCustomIndicatorRule(getNewThreatIndicatorRule()); reload(); }); @@ -56,9 +56,9 @@ describe('CTI Enrichment', () => { it('Displays enrichment matched.* fields on the timeline', () => { const expectedFields = { - 'threat.indicator.matched.atomic': newThreatIndicatorRule.atomic, + 'threat.indicator.matched.atomic': getNewThreatIndicatorRule().atomic, 'threat.indicator.matched.type': 'indicator_match_rule', - 'threat.indicator.matched.field': newThreatIndicatorRule.indicatorMappingField, + 'threat.indicator.matched.field': getNewThreatIndicatorRule().indicatorMappingField, }; const fields = Object.keys(expectedFields) as Array; diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/in_progress.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/in_progress.spec.ts index cb8694d5c35af..890f8a064aa9e 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/in_progress.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/in_progress.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { newRule } from '../../objects/rule'; +import { getNewRule } from '../../objects/rule'; import { ALERTS, ALERTS_COUNT, @@ -36,7 +36,7 @@ describe('Marking alerts as in-progress', () => { loginAndWaitForPage(ALERTS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); - createCustomRuleActivated(newRule); + createCustomRuleActivated(getNewRule()); refreshPage(); waitForAlertsToPopulate(500); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/investigate_in_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/investigate_in_timeline.spec.ts index 115118b6762d9..01a06b3d59266 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/investigate_in_timeline.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/investigate_in_timeline.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { newRule } from '../../objects/rule'; +import { getNewRule } from '../../objects/rule'; import { PROVIDER_BADGE } from '../../screens/timeline'; import { @@ -27,7 +27,7 @@ describe('Alerts timeline', () => { loginAndWaitForPage(ALERTS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); - createCustomRuleActivated(newRule); + createCustomRuleActivated(getNewRule()); refreshPage(); waitForAlertsToPopulate(500); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/missing_privileges_callout.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/missing_privileges_callout.spec.ts index 20a863e742efd..0db30179284e0 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/missing_privileges_callout.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/missing_privileges_callout.spec.ts @@ -7,7 +7,7 @@ import { ROLES } from '../../../common/test'; import { DETECTIONS_RULE_MANAGEMENT_URL, ALERTS_URL } from '../../urls/navigation'; -import { newRule } from '../../objects/rule'; +import { getNewRule } from '../../objects/rule'; import { PAGE_TITLE } from '../../screens/common/page'; import { @@ -95,7 +95,7 @@ describe('Detections > Callouts', () => { context('On Rule Details page', () => { beforeEach(() => { - createCustomRule(newRule); + createCustomRule(getNewRule()); loadPageAsReadOnlyUser(DETECTIONS_RULE_MANAGEMENT_URL); waitForPageTitleToBeShown(); goToRuleDetails(); @@ -145,7 +145,7 @@ describe('Detections > Callouts', () => { context('On Rule Details page', () => { beforeEach(() => { - createCustomRule(newRule); + createCustomRule(getNewRule()); loadPageAsPlatformEngineer(DETECTIONS_RULE_MANAGEMENT_URL); waitForPageTitleToBeShown(); goToRuleDetails(); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/opening.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/opening.spec.ts index 6cbc82b93f446..4f78bdac84789 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/opening.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/opening.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { newRule } from '../../objects/rule'; +import { getNewRule } from '../../objects/rule'; import { ALERTS_COUNT, SELECTED_ALERTS, @@ -37,7 +37,7 @@ describe('Opening alerts', () => { loginAndWaitForPage(ALERTS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); - createCustomRuleActivated(newRule); + createCustomRuleActivated(getNewRule()); refreshPage(); waitForAlertsToPopulate(500); selectNumberOfAlerts(5); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts index 5f9175476795c..e30cde4989284 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts @@ -7,11 +7,11 @@ import { formatMitreAttackDescription } from '../../helpers/rules'; import { - newRule, - existingRule, - indexPatterns, - editedRule, - newOverrideRule, + getNewRule, + getExistingRule, + getIndexPatterns, + getEditedRule, + getNewOverrideRule, } from '../../objects/rule'; import { ALERT_RULE_METHOD, @@ -48,6 +48,12 @@ import { SEVERITY_DROPDOWN, TAGS_CLEAR_BUTTON, TAGS_FIELD, + EMAIL_ACTION_BTN, + CREATE_ACTION_CONNECTOR_BTN, + SAVE_ACTION_CONNECTOR_BTN, + FROM_VALIDATION_ERROR, + EMAIL_ACTION_TO_INPUT, + EMAIL_ACTION_SUBJECT_INPUT, } from '../../screens/create_new_rule'; import { ADDITIONAL_LOOK_BACK_DETAILS, @@ -99,6 +105,7 @@ import { fillAboutRule, fillAboutRuleAndContinue, fillDefineCustomRuleWithImportedQueryAndContinue, + fillEmailConnectorForm, fillScheduleRuleAndContinue, goToAboutStepTab, goToActionsStepTab, @@ -113,19 +120,19 @@ import { activatesRule } from '../../tasks/rule_details'; import { ALERTS_URL } from '../../urls/navigation'; describe('Custom detection rules creation', () => { - const expectedUrls = newRule.referenceUrls.join(''); - const expectedFalsePositives = newRule.falsePositivesExamples.join(''); - const expectedTags = newRule.tags.join(''); - const expectedMitre = formatMitreAttackDescription(newRule.mitre); + const expectedUrls = getNewRule().referenceUrls.join(''); + const expectedFalsePositives = getNewRule().falsePositivesExamples.join(''); + const expectedTags = getNewRule().tags.join(''); + const expectedMitre = formatMitreAttackDescription(getNewRule().mitre); const expectedNumberOfRules = 1; beforeEach(() => { cleanKibana(); - createTimeline(newRule.timeline).then((response) => { + createTimeline(getNewRule().timeline).then((response) => { cy.wrap({ - ...newRule, + ...getNewRule(), timeline: { - ...newRule.timeline, + ...getNewRule().timeline, id: response.body.data.persistTimeline.timeline.savedObjectId, }, }).as('rule'); @@ -194,7 +201,7 @@ describe('Custom detection rules creation', () => { cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); cy.get(DEFINITION_DETAILS).within(() => { - getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); + getDetails(INDEX_PATTERNS_DETAILS).should('have.text', getIndexPatterns().join('')); getDetails(CUSTOM_QUERY_DETAILS).should('have.text', this.rule.customQuery); getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query'); getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); @@ -202,11 +209,11 @@ describe('Custom detection rules creation', () => { cy.get(SCHEDULE_DETAILS).within(() => { getDetails(RUNS_EVERY_DETAILS).should( 'have.text', - `${newRule.runsEvery.interval}${newRule.runsEvery.type}` + `${getNewRule().runsEvery.interval}${getNewRule().runsEvery.type}` ); getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should( 'have.text', - `${newRule.lookBack.interval}${newRule.lookBack.type}` + `${getNewRule().lookBack.interval}${getNewRule().lookBack.type}` ); }); @@ -229,9 +236,9 @@ describe('Custom detection rules deletion and edition', () => { loginAndWaitForPageWithoutDateRange(ALERTS_URL); goToManageAlertsDetectionRules(); waitForAlertsIndexToBeCreated(); - createCustomRuleActivated(newRule, 'rule1'); - createCustomRuleActivated(newOverrideRule, 'rule2'); - createCustomRuleActivated(existingRule, 'rule3'); + createCustomRuleActivated(getNewRule(), 'rule1'); + createCustomRuleActivated(getNewOverrideRule(), 'rule2'); + createCustomRuleActivated(getExistingRule(), 'rule3'); reload(); }); @@ -296,16 +303,18 @@ describe('Custom detection rules deletion and edition', () => { }); context('Edition', () => { - const expectedEditedtags = editedRule.tags.join(''); + const expectedEditedtags = getEditedRule().tags.join(''); const expectedEditedIndexPatterns = - editedRule.index && editedRule.index.length ? editedRule.index : indexPatterns; + getEditedRule().index && getEditedRule().index.length + ? getEditedRule().index + : getIndexPatterns(); beforeEach(() => { cleanKibana(); loginAndWaitForPageWithoutDateRange(ALERTS_URL); goToManageAlertsDetectionRules(); waitForAlertsIndexToBeCreated(); - createCustomRuleActivated(existingRule, 'rule1'); + createCustomRuleActivated(getExistingRule(), 'rule1'); reload(); }); @@ -319,7 +328,7 @@ describe('Custom detection rules deletion and edition', () => { cy.wait('@fetchRuleDetails').then(({ response }) => { cy.wrap(response!.statusCode).should('eql', 200); - cy.wrap(response!.body.max_signals).should('eql', existingRule.maxSignals); + cy.wrap(response!.body.max_signals).should('eql', getExistingRule().maxSignals); cy.wrap(response!.body.enabled).should('eql', false); }); }); @@ -329,25 +338,25 @@ describe('Custom detection rules deletion and edition', () => { waitForKibana(); // expect define step to populate - cy.get(CUSTOM_QUERY_INPUT).should('have.value', existingRule.customQuery); - if (existingRule.index && existingRule.index.length > 0) { - cy.get(DEFINE_INDEX_INPUT).should('have.text', existingRule.index.join('')); + cy.get(CUSTOM_QUERY_INPUT).should('have.value', getExistingRule().customQuery); + if (getExistingRule().index && getExistingRule().index.length > 0) { + cy.get(DEFINE_INDEX_INPUT).should('have.text', getExistingRule().index.join('')); } goToAboutStepTab(); // expect about step to populate - cy.get(RULE_NAME_INPUT).invoke('val').should('eql', existingRule.name); - cy.get(RULE_DESCRIPTION_INPUT).should('have.text', existingRule.description); - cy.get(TAGS_FIELD).should('have.text', existingRule.tags.join('')); - cy.get(SEVERITY_DROPDOWN).should('have.text', existingRule.severity); - cy.get(DEFAULT_RISK_SCORE_INPUT).invoke('val').should('eql', existingRule.riskScore); + cy.get(RULE_NAME_INPUT).invoke('val').should('eql', getExistingRule().name); + cy.get(RULE_DESCRIPTION_INPUT).should('have.text', getExistingRule().description); + cy.get(TAGS_FIELD).should('have.text', getExistingRule().tags.join('')); + cy.get(SEVERITY_DROPDOWN).should('have.text', getExistingRule().severity); + cy.get(DEFAULT_RISK_SCORE_INPUT).invoke('val').should('eql', getExistingRule().riskScore); goToScheduleStepTab(); // expect schedule step to populate - const intervalParts = - existingRule.interval && existingRule.interval.match(/[0-9]+|[a-zA-Z]+/g); + const interval = getExistingRule().interval; + const intervalParts = interval != null && interval.match(/[0-9]+|[a-zA-Z]+/g); if (intervalParts) { const [amount, unit] = intervalParts; cy.get(SCHEDULE_INTERVAL_AMOUNT_INPUT).invoke('val').should('eql', amount); @@ -360,9 +369,20 @@ describe('Custom detection rules deletion and edition', () => { cy.get(ACTIONS_THROTTLE_INPUT).invoke('val').should('eql', 'no_actions'); + cy.get(ACTIONS_THROTTLE_INPUT).select('Weekly'); + cy.get(EMAIL_ACTION_BTN).click(); + cy.get(CREATE_ACTION_CONNECTOR_BTN).click(); + fillEmailConnectorForm(); + cy.get(SAVE_ACTION_CONNECTOR_BTN).click(); + + cy.get(EMAIL_ACTION_TO_INPUT).type('test@example.com'); + cy.get(EMAIL_ACTION_SUBJECT_INPUT).type('Subject'); + + cy.get(FROM_VALIDATION_ERROR).should('not.exist'); + goToAboutStepTab(); cy.get(TAGS_CLEAR_BUTTON).click({ force: true }); - fillAboutRule(editedRule); + fillAboutRule(getEditedRule()); cy.intercept('GET', '/api/detection_engine/rules?id').as('getRule'); @@ -371,30 +391,30 @@ describe('Custom detection rules deletion and edition', () => { cy.wait('@getRule').then(({ response }) => { cy.wrap(response!.statusCode).should('eql', 200); // ensure that editing rule does not modify max_signals - cy.wrap(response!.body.max_signals).should('eql', existingRule.maxSignals); + cy.wrap(response!.body.max_signals).should('eql', getExistingRule().maxSignals); }); - cy.get(RULE_NAME_HEADER).should('contain', `${editedRule.name}`); - cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', editedRule.description); + cy.get(RULE_NAME_HEADER).should('contain', `${getEditedRule().name}`); + cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', getEditedRule().description); cy.get(ABOUT_DETAILS).within(() => { - getDetails(SEVERITY_DETAILS).should('have.text', editedRule.severity); - getDetails(RISK_SCORE_DETAILS).should('have.text', editedRule.riskScore); + getDetails(SEVERITY_DETAILS).should('have.text', getEditedRule().severity); + getDetails(RISK_SCORE_DETAILS).should('have.text', getEditedRule().riskScore); getDetails(TAGS_DETAILS).should('have.text', expectedEditedtags); }); cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); - cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', editedRule.note); + cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', getEditedRule().note); cy.get(DEFINITION_DETAILS).within(() => { getDetails(INDEX_PATTERNS_DETAILS).should( 'have.text', expectedEditedIndexPatterns.join('') ); - getDetails(CUSTOM_QUERY_DETAILS).should('have.text', editedRule.customQuery); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', getEditedRule().customQuery); getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query'); getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); }); - if (editedRule.interval) { + if (getEditedRule().interval) { cy.get(SCHEDULE_DETAILS).within(() => { - getDetails(RUNS_EVERY_DETAILS).should('have.text', editedRule.interval); + getDetails(RUNS_EVERY_DETAILS).should('have.text', getEditedRule().interval); }); } }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts index 337e2a8ec5033..677a9b5546494 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts @@ -6,7 +6,7 @@ */ import { formatMitreAttackDescription } from '../../helpers/rules'; -import { eqlRule, eqlSequenceRule, indexPatterns } from '../../objects/rule'; +import { getEqlRule, getEqlSequenceRule, getIndexPatterns } from '../../objects/rule'; import { ALERT_RULE_METHOD, @@ -78,20 +78,20 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { ALERTS_URL } from '../../urls/navigation'; describe('Detection rules, EQL', () => { - const expectedUrls = eqlRule.referenceUrls.join(''); - const expectedFalsePositives = eqlRule.falsePositivesExamples.join(''); - const expectedTags = eqlRule.tags.join(''); - const expectedMitre = formatMitreAttackDescription(eqlRule.mitre); + const expectedUrls = getEqlRule().referenceUrls.join(''); + const expectedFalsePositives = getEqlRule().falsePositivesExamples.join(''); + const expectedTags = getEqlRule().tags.join(''); + const expectedMitre = formatMitreAttackDescription(getEqlRule().mitre); const expectedNumberOfRules = 1; const expectedNumberOfAlerts = 7; beforeEach(() => { cleanKibana(); - createTimeline(eqlRule.timeline).then((response) => { + createTimeline(getEqlRule().timeline).then((response) => { cy.wrap({ - ...eqlRule, + ...getEqlRule(), timeline: { - ...eqlRule.timeline, + ...getEqlRule().timeline, id: response.body.data.persistTimeline.timeline.savedObjectId, }, }).as('rule'); @@ -148,7 +148,7 @@ describe('Detection rules, EQL', () => { cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); cy.get(DEFINITION_DETAILS).within(() => { - getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); + getDetails(INDEX_PATTERNS_DETAILS).should('have.text', getIndexPatterns().join('')); getDetails(CUSTOM_QUERY_DETAILS).should('have.text', this.rule.customQuery); getDetails(RULE_TYPE_DETAILS).should('have.text', 'Event Correlation'); getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); @@ -182,11 +182,11 @@ describe('Detection rules, sequence EQL', () => { beforeEach(() => { cleanKibana(); - createTimeline(eqlSequenceRule.timeline).then((response) => { + createTimeline(getEqlSequenceRule().timeline).then((response) => { cy.wrap({ - ...eqlSequenceRule, + ...getEqlSequenceRule(), timeline: { - ...eqlSequenceRule.timeline, + ...getEqlSequenceRule().timeline, id: response.body.data.persistTimeline.timeline.savedObjectId, }, }).as('rule'); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/export_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/export_rule.spec.ts index 1de636010f967..03086810a8435 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/export_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/export_rule.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { expectedExportedRule, newRule } from '../../objects/rule'; +import { expectedExportedRule, getNewRule } from '../../objects/rule'; import { goToManageAlertsDetectionRules, waitForAlertsIndexToBeCreated, @@ -28,7 +28,7 @@ describe('Export rules', () => { loginAndWaitForPageWithoutDateRange(ALERTS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); - createCustomRule(newRule).as('ruleResponse'); + createCustomRule(getNewRule()).as('ruleResponse'); }); it('Exports a custom rule', function () { diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index e1268c52f75d4..07b40df53e2d5 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -6,7 +6,7 @@ */ import { formatMitreAttackDescription } from '../../helpers/rules'; -import { indexPatterns, newThreatIndicatorRule } from '../../objects/rule'; +import { getIndexPatterns, getNewThreatIndicatorRule } from '../../objects/rule'; import { ALERT_RULE_METHOD, @@ -109,10 +109,10 @@ import { ALERTS_URL, RULE_CREATION } from '../../urls/navigation'; describe('indicator match', () => { describe('Detection rules, Indicator Match', () => { - const expectedUrls = newThreatIndicatorRule.referenceUrls.join(''); - const expectedFalsePositives = newThreatIndicatorRule.falsePositivesExamples.join(''); - const expectedTags = newThreatIndicatorRule.tags.join(''); - const expectedMitre = formatMitreAttackDescription(newThreatIndicatorRule.mitre); + const expectedUrls = getNewThreatIndicatorRule().referenceUrls.join(''); + const expectedFalsePositives = getNewThreatIndicatorRule().falsePositivesExamples.join(''); + const expectedTags = getNewThreatIndicatorRule().tags.join(''); + const expectedMitre = formatMitreAttackDescription(getNewThreatIndicatorRule().mitre); const expectedNumberOfRules = 1; const expectedNumberOfAlerts = 1; @@ -134,12 +134,12 @@ describe('indicator match', () => { describe('Index patterns', () => { it('Contains a predefined index pattern', () => { - getIndicatorIndex().should('have.text', indexPatterns.join('')); + getIndicatorIndex().should('have.text', getIndexPatterns().join('')); }); it('Does NOT show invalidation text on initial page load if indicator index pattern is filled out', () => { getIndicatorIndicatorIndex().type( - `${newThreatIndicatorRule.indicatorIndexPattern}{enter}` + `${getNewThreatIndicatorRule().indicatorIndexPattern}{enter}` ); getDefineContinueButton().click(); getIndexPatternInvalidationText().should('not.exist'); @@ -148,7 +148,7 @@ describe('indicator match', () => { it('Shows invalidation text when you try to continue without filling it out', () => { getIndexPatternClearButton().click(); getIndicatorIndicatorIndex().type( - `${newThreatIndicatorRule.indicatorIndexPattern}{enter}` + `${getNewThreatIndicatorRule().indicatorIndexPattern}{enter}` ); getDefineContinueButton().click(); getIndexPatternInvalidationText().should('exist'); @@ -195,8 +195,8 @@ describe('indicator match', () => { describe('Indicator mapping', () => { beforeEach(() => { fillIndexAndIndicatorIndexPattern( - newThreatIndicatorRule.index, - newThreatIndicatorRule.indicatorIndexPattern + getNewThreatIndicatorRule().index, + getNewThreatIndicatorRule().indicatorIndexPattern ); }); @@ -221,8 +221,8 @@ describe('indicator match', () => { it('Does NOT show invalidation text when there is a valid "index field" and a valid "indicator index field"', () => { fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMappingField, - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + indexField: getNewThreatIndicatorRule().indicatorMappingField, + indicatorIndexField: getNewThreatIndicatorRule().indicatorIndexField, }); getDefineContinueButton().click(); getIndicatorInvalidationText().should('not.exist'); @@ -231,7 +231,7 @@ describe('indicator match', () => { it('Shows invalidation text when there is an invalid "index field" and a valid "indicator index field"', () => { fillIndicatorMatchRow({ indexField: 'non-existent-value', - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + indicatorIndexField: getNewThreatIndicatorRule().indicatorIndexField, validColumns: 'indicatorField', }); getDefineContinueButton().click(); @@ -240,7 +240,7 @@ describe('indicator match', () => { it('Shows invalidation text when there is a valid "index field" and an invalid "indicator index field"', () => { fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMappingField, + indexField: getNewThreatIndicatorRule().indicatorMappingField, indicatorIndexField: 'non-existent-value', validColumns: 'indexField', }); @@ -250,21 +250,21 @@ describe('indicator match', () => { it('Deletes the first row when you have two rows. Both rows valid rows of "index fields" and valid "indicator index fields". The second row should become the first row', () => { fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMappingField, - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + indexField: getNewThreatIndicatorRule().indicatorMappingField, + indicatorIndexField: getNewThreatIndicatorRule().indicatorIndexField, }); getIndicatorAndButton().click(); fillIndicatorMatchRow({ rowNumber: 2, indexField: 'agent.name', - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + indicatorIndexField: getNewThreatIndicatorRule().indicatorIndexField, validColumns: 'indicatorField', }); getIndicatorDeleteButton().click(); getIndicatorIndexComboField().should('have.text', 'agent.name'); getIndicatorMappingComboField().should( 'have.text', - newThreatIndicatorRule.indicatorIndexField + getNewThreatIndicatorRule().indicatorIndexField ); getIndicatorIndexComboField(2).should('not.exist'); getIndicatorMappingComboField(2).should('not.exist'); @@ -272,14 +272,14 @@ describe('indicator match', () => { it('Deletes the first row when you have two rows. Both rows have valid "index fields" and invalid "indicator index fields". The second row should become the first row', () => { fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMappingField, + indexField: getNewThreatIndicatorRule().indicatorMappingField, indicatorIndexField: 'non-existent-value', validColumns: 'indexField', }); getIndicatorAndButton().click(); fillIndicatorMatchRow({ rowNumber: 2, - indexField: newThreatIndicatorRule.indicatorMappingField, + indexField: getNewThreatIndicatorRule().indicatorMappingField, indicatorIndexField: 'second-non-existent-value', validColumns: 'indexField', }); @@ -292,14 +292,14 @@ describe('indicator match', () => { it('Deletes the first row when you have two rows. Both rows have valid "indicator index fields" and invalid "index fields". The second row should become the first row', () => { fillIndicatorMatchRow({ indexField: 'non-existent-value', - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + indicatorIndexField: getNewThreatIndicatorRule().indicatorIndexField, validColumns: 'indicatorField', }); getIndicatorAndButton().click(); fillIndicatorMatchRow({ rowNumber: 2, indexField: 'second-non-existent-value', - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + indicatorIndexField: getNewThreatIndicatorRule().indicatorIndexField, validColumns: 'indicatorField', }); getIndicatorDeleteButton().click(); @@ -310,8 +310,8 @@ describe('indicator match', () => { it('Deletes the first row of data but not the UI elements and the text defaults back to the placeholder of Search', () => { fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMappingField, - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + indexField: getNewThreatIndicatorRule().indicatorMappingField, + indicatorIndexField: getNewThreatIndicatorRule().indicatorIndexField, }); getIndicatorDeleteButton().click(); getIndicatorIndexComboField().should('text', 'Search'); @@ -322,8 +322,8 @@ describe('indicator match', () => { it('Deletes the second row when you have three rows. The first row is valid data, the second row is invalid data, and the third row is valid data. Third row should shift up correctly', () => { fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMappingField, - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + indexField: getNewThreatIndicatorRule().indicatorMappingField, + indicatorIndexField: getNewThreatIndicatorRule().indicatorIndexField, }); getIndicatorAndButton().click(); fillIndicatorMatchRow({ @@ -335,25 +335,25 @@ describe('indicator match', () => { getIndicatorAndButton().click(); fillIndicatorMatchRow({ rowNumber: 3, - indexField: newThreatIndicatorRule.indicatorMappingField, - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + indexField: getNewThreatIndicatorRule().indicatorMappingField, + indicatorIndexField: getNewThreatIndicatorRule().indicatorIndexField, }); getIndicatorDeleteButton(2).click(); getIndicatorIndexComboField(1).should( 'text', - newThreatIndicatorRule.indicatorMappingField + getNewThreatIndicatorRule().indicatorMappingField ); getIndicatorMappingComboField(1).should( 'text', - newThreatIndicatorRule.indicatorIndexField + getNewThreatIndicatorRule().indicatorIndexField ); getIndicatorIndexComboField(2).should( 'text', - newThreatIndicatorRule.indicatorMappingField + getNewThreatIndicatorRule().indicatorMappingField ); getIndicatorMappingComboField(2).should( 'text', - newThreatIndicatorRule.indicatorIndexField + getNewThreatIndicatorRule().indicatorIndexField ); getIndicatorIndexComboField(3).should('not.exist'); getIndicatorMappingComboField(3).should('not.exist'); @@ -368,17 +368,17 @@ describe('indicator match', () => { getIndicatorOrButton().click(); fillIndicatorMatchRow({ rowNumber: 2, - indexField: newThreatIndicatorRule.indicatorMappingField, - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + indexField: getNewThreatIndicatorRule().indicatorMappingField, + indicatorIndexField: getNewThreatIndicatorRule().indicatorIndexField, }); getIndicatorDeleteButton().click(); getIndicatorIndexComboField().should( 'text', - newThreatIndicatorRule.indicatorMappingField + getNewThreatIndicatorRule().indicatorMappingField ); getIndicatorMappingComboField().should( 'text', - newThreatIndicatorRule.indicatorIndexField + getNewThreatIndicatorRule().indicatorIndexField ); getIndicatorIndexComboField(2).should('not.exist'); getIndicatorMappingComboField(2).should('not.exist'); @@ -399,9 +399,9 @@ describe('indicator match', () => { waitForRulesTableToBeLoaded(); goToCreateNewRule(); selectIndicatorMatchType(); - fillDefineIndicatorMatchRuleAndContinue(newThreatIndicatorRule); - fillAboutRuleAndContinue(newThreatIndicatorRule); - fillScheduleRuleAndContinue(newThreatIndicatorRule); + fillDefineIndicatorMatchRuleAndContinue(getNewThreatIndicatorRule()); + fillAboutRuleAndContinue(getNewThreatIndicatorRule()); + fillScheduleRuleAndContinue(getNewThreatIndicatorRule()); createAndActivateRule(); cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); @@ -417,18 +417,18 @@ describe('indicator match', () => { cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', 1); }); - cy.get(RULE_NAME).should('have.text', newThreatIndicatorRule.name); - cy.get(RISK_SCORE).should('have.text', newThreatIndicatorRule.riskScore); - cy.get(SEVERITY).should('have.text', newThreatIndicatorRule.severity); + cy.get(RULE_NAME).should('have.text', getNewThreatIndicatorRule().name); + cy.get(RISK_SCORE).should('have.text', getNewThreatIndicatorRule().riskScore); + cy.get(SEVERITY).should('have.text', getNewThreatIndicatorRule().severity); cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); goToRuleDetails(); - cy.get(RULE_NAME_HEADER).should('contain', `${newThreatIndicatorRule.name}`); - cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newThreatIndicatorRule.description); + cy.get(RULE_NAME_HEADER).should('contain', `${getNewThreatIndicatorRule().name}`); + cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', getNewThreatIndicatorRule().description); cy.get(ABOUT_DETAILS).within(() => { - getDetails(SEVERITY_DETAILS).should('have.text', newThreatIndicatorRule.severity); - getDetails(RISK_SCORE_DETAILS).should('have.text', newThreatIndicatorRule.riskScore); + getDetails(SEVERITY_DETAILS).should('have.text', getNewThreatIndicatorRule().severity); + getDetails(RISK_SCORE_DETAILS).should('have.text', getNewThreatIndicatorRule().riskScore); getDetails(REFERENCE_URLS_DETAILS).should((details) => { expect(removeExternalLinkText(details.text())).equal(expectedUrls); }); @@ -444,18 +444,20 @@ describe('indicator match', () => { cy.get(DEFINITION_DETAILS).within(() => { getDetails(INDEX_PATTERNS_DETAILS).should( 'have.text', - newThreatIndicatorRule.index.join('') + getNewThreatIndicatorRule().index.join('') ); getDetails(CUSTOM_QUERY_DETAILS).should('have.text', '*:*'); getDetails(RULE_TYPE_DETAILS).should('have.text', 'Indicator Match'); getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); getDetails(INDICATOR_INDEX_PATTERNS).should( 'have.text', - newThreatIndicatorRule.indicatorIndexPattern.join('') + getNewThreatIndicatorRule().indicatorIndexPattern.join('') ); getDetails(INDICATOR_MAPPING).should( 'have.text', - `${newThreatIndicatorRule.indicatorMappingField} MATCHES ${newThreatIndicatorRule.indicatorIndexField}` + `${getNewThreatIndicatorRule().indicatorMappingField} MATCHES ${ + getNewThreatIndicatorRule().indicatorIndexField + }` ); getDetails(INDICATOR_INDEX_QUERY).should('have.text', '*:*'); }); @@ -463,11 +465,15 @@ describe('indicator match', () => { cy.get(SCHEDULE_DETAILS).within(() => { getDetails(RUNS_EVERY_DETAILS).should( 'have.text', - `${newThreatIndicatorRule.runsEvery.interval}${newThreatIndicatorRule.runsEvery.type}` + `${getNewThreatIndicatorRule().runsEvery.interval}${ + getNewThreatIndicatorRule().runsEvery.type + }` ); getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should( 'have.text', - `${newThreatIndicatorRule.lookBack.interval}${newThreatIndicatorRule.lookBack.type}` + `${getNewThreatIndicatorRule().lookBack.interval}${ + getNewThreatIndicatorRule().lookBack.type + }` ); }); @@ -475,13 +481,15 @@ describe('indicator match', () => { waitForAlertsToPopulate(); cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts); - cy.get(ALERT_RULE_NAME).first().should('have.text', newThreatIndicatorRule.name); + cy.get(ALERT_RULE_NAME).first().should('have.text', getNewThreatIndicatorRule().name); cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); cy.get(ALERT_RULE_METHOD).first().should('have.text', 'threat_match'); cy.get(ALERT_RULE_SEVERITY) .first() - .should('have.text', newThreatIndicatorRule.severity.toLowerCase()); - cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', newThreatIndicatorRule.riskScore); + .should('have.text', getNewThreatIndicatorRule().severity.toLowerCase()); + cy.get(ALERT_RULE_RISK_SCORE) + .first() + .should('have.text', getNewThreatIndicatorRule().riskScore); }); it('Investigate alert in timeline', () => { @@ -492,7 +500,7 @@ describe('indicator match', () => { loadPrepackagedTimelineTemplates(); goToManageAlertsDetectionRules(); - createCustomIndicatorRule(newThreatIndicatorRule); + createCustomIndicatorRule(getNewThreatIndicatorRule()); reload(); goToRuleDetails(); @@ -502,13 +510,25 @@ describe('indicator match', () => { cy.get(PROVIDER_BADGE).should('have.length', 3); cy.get(PROVIDER_BADGE).should( 'have.text', - `threat.indicator.matched.atomic: "${newThreatIndicatorRule.atomic}"threat.indicator.matched.type: "indicator_match_rule"threat.indicator.matched.field: "${newThreatIndicatorRule.indicatorMappingField}"` + `threat.indicator.matched.atomic: "${ + getNewThreatIndicatorRule().atomic + }"threat.indicator.matched.type: "indicator_match_rule"threat.indicator.matched.field: "${ + getNewThreatIndicatorRule().indicatorMappingField + }"` ); cy.readFile(threatIndicatorPath).then((threatIndicator) => { cy.get(INDICATOR_MATCH_ROW_RENDER).should( 'have.text', - `threat.indicator.matched.field${newThreatIndicatorRule.indicatorMappingField}${accessibilityText}matched${newThreatIndicatorRule.indicatorMappingField}${newThreatIndicatorRule.atomic}${accessibilityText}threat.indicator.matched.typeindicator_match_rule${accessibilityText}fromthreat.indicator.event.dataset${threatIndicator.value.source.event.dataset}${accessibilityText}:threat.indicator.event.reference${threatIndicator.value.source.event.reference}(opens in a new tab or window)${accessibilityText}` + `threat.indicator.matched.field${ + getNewThreatIndicatorRule().indicatorMappingField + }${accessibilityText}matched${getNewThreatIndicatorRule().indicatorMappingField}${ + getNewThreatIndicatorRule().atomic + }${accessibilityText}threat.indicator.matched.typeindicator_match_rule${accessibilityText}fromthreat.indicator.event.dataset${ + threatIndicator.value.source.event.dataset + }${accessibilityText}:threat.indicator.event.reference${ + threatIndicator.value.source.event.reference + }(opens in a new tab or window)${accessibilityText}` ); }); }); @@ -519,7 +539,7 @@ describe('indicator match', () => { cleanKibana(); loginAndWaitForPageWithoutDateRange(ALERTS_URL); goToManageAlertsDetectionRules(); - createCustomIndicatorRule(newThreatIndicatorRule); + createCustomIndicatorRule(getNewThreatIndicatorRule()); reload(); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/links.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/links.spec.ts index fdc4bce677f74..85eb68a6cdfa9 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/links.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/links.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { newRule } from '../../objects/rule'; +import { getNewRule } from '../../objects/rule'; import { RULES_MONIROTING_TABLE, RULE_NAME } from '../../screens/alerts_detection_rules'; import { goToManageAlertsDetectionRules, waitForAlertsIndexToBeCreated } from '../../tasks/alerts'; import { createCustomRuleActivated } from '../../tasks/api_calls/rules'; @@ -19,7 +19,7 @@ describe('Rules talbes links', () => { loginAndWaitForPageWithoutDateRange(ALERTS_URL); goToManageAlertsDetectionRules(); waitForAlertsIndexToBeCreated(); - createCustomRuleActivated(newRule, 'rule1'); + createCustomRuleActivated(getNewRule(), 'rule1'); reload(); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/machine_learning_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/machine_learning_rule.spec.ts index 2d869b314b67c..e66f8f55be986 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/machine_learning_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/machine_learning_rule.spec.ts @@ -6,7 +6,7 @@ */ import { formatMitreAttackDescription } from '../../helpers/rules'; -import { machineLearningRule } from '../../objects/rule'; +import { getMachineLearningRule } from '../../objects/rule'; import { CUSTOM_RULES_BTN, @@ -65,10 +65,10 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { ALERTS_URL } from '../../urls/navigation'; describe('Detection rules, machine learning', () => { - const expectedUrls = machineLearningRule.referenceUrls.join(''); - const expectedFalsePositives = machineLearningRule.falsePositivesExamples.join(''); - const expectedTags = machineLearningRule.tags.join(''); - const expectedMitre = formatMitreAttackDescription(machineLearningRule.mitre); + const expectedUrls = getMachineLearningRule().referenceUrls.join(''); + const expectedFalsePositives = getMachineLearningRule().falsePositivesExamples.join(''); + const expectedTags = getMachineLearningRule().tags.join(''); + const expectedMitre = formatMitreAttackDescription(getMachineLearningRule().mitre); const expectedNumberOfRules = 1; beforeEach(() => { @@ -83,9 +83,9 @@ describe('Detection rules, machine learning', () => { waitForRulesTableToBeLoaded(); goToCreateNewRule(); selectMachineLearningRuleType(); - fillDefineMachineLearningRuleAndContinue(machineLearningRule); - fillAboutRuleAndContinue(machineLearningRule); - fillScheduleRuleAndContinue(machineLearningRule); + fillDefineMachineLearningRuleAndContinue(getMachineLearningRule()); + fillAboutRuleAndContinue(getMachineLearningRule()); + fillScheduleRuleAndContinue(getMachineLearningRule()); createAndActivateRule(); cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); @@ -101,18 +101,18 @@ describe('Detection rules, machine learning', () => { cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', 1); }); - cy.get(RULE_NAME).should('have.text', machineLearningRule.name); - cy.get(RISK_SCORE).should('have.text', machineLearningRule.riskScore); - cy.get(SEVERITY).should('have.text', machineLearningRule.severity); + cy.get(RULE_NAME).should('have.text', getMachineLearningRule().name); + cy.get(RISK_SCORE).should('have.text', getMachineLearningRule().riskScore); + cy.get(SEVERITY).should('have.text', getMachineLearningRule().severity); cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); goToRuleDetails(); - cy.get(RULE_NAME_HEADER).should('contain', `${machineLearningRule.name}`); - cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', machineLearningRule.description); + cy.get(RULE_NAME_HEADER).should('contain', `${getMachineLearningRule().name}`); + cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', getMachineLearningRule().description); cy.get(ABOUT_DETAILS).within(() => { - getDetails(SEVERITY_DETAILS).should('have.text', machineLearningRule.severity); - getDetails(RISK_SCORE_DETAILS).should('have.text', machineLearningRule.riskScore); + getDetails(SEVERITY_DETAILS).should('have.text', getMachineLearningRule().severity); + getDetails(RISK_SCORE_DETAILS).should('have.text', getMachineLearningRule().riskScore); getDetails(REFERENCE_URLS_DETAILS).should((details) => { expect(removeExternalLinkText(details.text())).equal(expectedUrls); }); @@ -125,11 +125,11 @@ describe('Detection rules, machine learning', () => { cy.get(DEFINITION_DETAILS).within(() => { getDetails(ANOMALY_SCORE_DETAILS).should( 'have.text', - machineLearningRule.anomalyScoreThreshold + getMachineLearningRule().anomalyScoreThreshold ); getDetails(RULE_TYPE_DETAILS).should('have.text', 'Machine Learning'); getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); - machineLearningRule.machineLearningJobs.forEach((machineLearningJob, jobIndex) => { + getMachineLearningRule().machineLearningJobs.forEach((machineLearningJob, jobIndex) => { cy.get(MACHINE_LEARNING_JOB_STATUS).eq(jobIndex).should('have.text', 'Stopped'); cy.get(MACHINE_LEARNING_JOB_ID).eq(jobIndex).should('have.text', machineLearningJob); }); @@ -137,11 +137,11 @@ describe('Detection rules, machine learning', () => { cy.get(SCHEDULE_DETAILS).within(() => { getDetails(RUNS_EVERY_DETAILS).should( 'have.text', - `${machineLearningRule.runsEvery.interval}${machineLearningRule.runsEvery.type}` + `${getMachineLearningRule().runsEvery.interval}${getMachineLearningRule().runsEvery.type}` ); getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should( 'have.text', - `${machineLearningRule.lookBack.interval}${machineLearningRule.lookBack.type}` + `${getMachineLearningRule().lookBack.interval}${getMachineLearningRule().lookBack.type}` ); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts index a791cc293c1f0..24a56dd563e17 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts @@ -7,9 +7,9 @@ import { formatMitreAttackDescription } from '../../helpers/rules'; import { - indexPatterns, - newOverrideRule, - severitiesOverride, + getIndexPatterns, + getNewOverrideRule, + getSeveritiesOverride, OverrideRule, } from '../../objects/rule'; @@ -89,18 +89,18 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { ALERTS_URL } from '../../urls/navigation'; describe('Detection rules, override', () => { - const expectedUrls = newOverrideRule.referenceUrls.join(''); - const expectedFalsePositives = newOverrideRule.falsePositivesExamples.join(''); - const expectedTags = newOverrideRule.tags.join(''); - const expectedMitre = formatMitreAttackDescription(newOverrideRule.mitre); + const expectedUrls = getNewOverrideRule().referenceUrls.join(''); + const expectedFalsePositives = getNewOverrideRule().falsePositivesExamples.join(''); + const expectedTags = getNewOverrideRule().tags.join(''); + const expectedMitre = formatMitreAttackDescription(getNewOverrideRule().mitre); beforeEach(() => { cleanKibana(); - createTimeline(newOverrideRule.timeline).then((response) => { + createTimeline(getNewOverrideRule().timeline).then((response) => { cy.wrap({ - ...newOverrideRule, + ...getNewOverrideRule(), timeline: { - ...newOverrideRule.timeline, + ...getNewOverrideRule().timeline, id: response.body.data.persistTimeline.timeline.savedObjectId, }, }).as('rule'); @@ -167,7 +167,7 @@ describe('Detection rules, override', () => { .eq(severityOverrideIndex + i) .should( 'have.text', - `${severity.sourceField}:${severity.sourceValue}${severitiesOverride[i]}` + `${severity.sourceField}:${severity.sourceValue}${getSeveritiesOverride()[i]}` ); }); }); @@ -175,7 +175,7 @@ describe('Detection rules, override', () => { cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); cy.get(DEFINITION_DETAILS).within(() => { - getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); + getDetails(INDEX_PATTERNS_DETAILS).should('have.text', getIndexPatterns().join('')); getDetails(CUSTOM_QUERY_DETAILS).should('have.text', this.rule.customQuery); getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query'); getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts index 7d42ea533a9ae..ef3d3a82d40bd 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts @@ -39,7 +39,12 @@ import { DEFAULT_RULE_REFRESH_INTERVAL_VALUE } from '../../../common/constants'; import { ALERTS_URL } from '../../urls/navigation'; import { createCustomRule } from '../../tasks/api_calls/rules'; import { cleanKibana } from '../../tasks/common'; -import { existingRule, newOverrideRule, newRule, newThresholdRule } from '../../objects/rule'; +import { + getExistingRule, + getNewOverrideRule, + getNewRule, + getNewThresholdRule, +} from '../../objects/rule'; describe('Alerts detection rules', () => { beforeEach(() => { @@ -47,10 +52,10 @@ describe('Alerts detection rules', () => { loginAndWaitForPageWithoutDateRange(ALERTS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); - createCustomRule(newRule, '1'); - createCustomRule(existingRule, '2'); - createCustomRule(newOverrideRule, '3'); - createCustomRule(newThresholdRule, '4'); + createCustomRule(getNewRule(), '1'); + createCustomRule(getExistingRule(), '2'); + createCustomRule(getNewOverrideRule(), '3'); + createCustomRule(getNewThresholdRule(), '4'); }); it('Sorts by activated rules', () => { @@ -90,8 +95,8 @@ describe('Alerts detection rules', () => { }); it('Pagination updates page number and results', () => { - createCustomRule({ ...newRule, name: 'Test a rule' }, '5'); - createCustomRule({ ...newRule, name: 'Not same as first rule' }, '6'); + createCustomRule({ ...getNewRule(), name: 'Test a rule' }, '5'); + createCustomRule({ ...getNewRule(), name: 'Not same as first rule' }, '6'); goToManageAlertsDetectionRules(); waitForRulesTableToBeLoaded(); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts index ad71d54eb2a7a..dba12fb4ab95c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts @@ -6,7 +6,12 @@ */ import { formatMitreAttackDescription } from '../../helpers/rules'; -import { indexPatterns, newRule, newThresholdRule } from '../../objects/rule'; +import { + getIndexPatterns, + getNewRule, + getNewThresholdRule, + ThresholdRule, +} from '../../objects/rule'; import { ALERT_RULE_METHOD, @@ -84,16 +89,16 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { ALERTS_URL } from '../../urls/navigation'; describe('Detection rules, threshold', () => { - const expectedUrls = newThresholdRule.referenceUrls.join(''); - const expectedFalsePositives = newThresholdRule.falsePositivesExamples.join(''); - const expectedTags = newThresholdRule.tags.join(''); - const expectedMitre = formatMitreAttackDescription(newThresholdRule.mitre); - - const rule = { ...newThresholdRule }; + let rule = getNewThresholdRule(); + const expectedUrls = getNewThresholdRule().referenceUrls.join(''); + const expectedFalsePositives = getNewThresholdRule().falsePositivesExamples.join(''); + const expectedTags = getNewThresholdRule().tags.join(''); + const expectedMitre = formatMitreAttackDescription(getNewThresholdRule().mitre); beforeEach(() => { + rule = getNewThresholdRule(); cleanKibana(); - createTimeline(newThresholdRule.timeline).then((response) => { + createTimeline(getNewThresholdRule().timeline).then((response) => { rule.timeline.id = response.body.data.persistTimeline.timeline.savedObjectId; }); loginAndWaitForPageWithoutDateRange(ALERTS_URL); @@ -149,7 +154,7 @@ describe('Detection rules, threshold', () => { cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); cy.get(DEFINITION_DETAILS).within(() => { - getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); + getDetails(INDEX_PATTERNS_DETAILS).should('have.text', getIndexPatterns().join('')); getDetails(CUSTOM_QUERY_DETAILS).should('have.text', rule.customQuery); getDetails(RULE_TYPE_DETAILS).should('have.text', 'Threshold'); getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); @@ -180,18 +185,36 @@ describe('Detection rules, threshold', () => { cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', rule.riskScore); }); - it('Preview results', () => { - const previewRule = { ...newThresholdRule }; - previewRule.index!.push('.siem-signals*'); + it('Preview results of keyword using "host.name"', () => { + rule.index = [...rule.index, '.siem-signals*']; - createCustomRuleActivated(newRule); + createCustomRuleActivated(getNewRule()); goToManageAlertsDetectionRules(); waitForRulesTableToBeLoaded(); goToCreateNewRule(); selectThresholdRuleType(); - fillDefineThresholdRule(previewRule); + fillDefineThresholdRule(rule); previewResults(); cy.get(PREVIEW_HEADER_SUBTITLE).should('have.text', '3 unique hits'); }); + + it('Preview results of "ip" using "source.ip"', () => { + const previewRule: ThresholdRule = { + ...rule, + thresholdField: 'source.ip', + threshold: '1', + }; + previewRule.index = [...previewRule.index, '.siem-signals*']; + + createCustomRuleActivated(getNewRule()); + goToManageAlertsDetectionRules(); + waitForRulesTableToBeLoaded(); + goToCreateNewRule(); + selectThresholdRuleType(); + fillDefineThresholdRule(previewRule); + previewResults(); + + cy.get(PREVIEW_HEADER_SUBTITLE).should('have.text', '10 unique hits'); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_modal.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_modal.spec.ts index a4b929f7d8e1d..7eedc99652f80 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_modal.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_modal.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { newRule } from '../../objects/rule'; +import { getNewRule } from '../../objects/rule'; import { RULE_STATUS } from '../../screens/create_new_rule'; @@ -44,7 +44,7 @@ describe('Exceptions modal', () => { cleanKibana(); loginAndWaitForPageWithoutDateRange(ALERTS_URL); waitForAlertsIndexToBeCreated(); - createCustomRule(newRule); + createCustomRule(getNewRule()); goToManageAlertsDetectionRules(); goToRuleDetails(); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_table.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_table.spec.ts index 83277075b35cc..051ebbb9643f6 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_table.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_table.spec.ts @@ -5,8 +5,12 @@ * 2.0. */ -import { exception, exceptionList, expectedExportedExceptionList } from '../../objects/exception'; -import { newRule } from '../../objects/rule'; +import { + getException, + getExceptionList, + expectedExportedExceptionList, +} from '../../objects/exception'; +import { getNewRule } from '../../objects/rule'; import { RULE_STATUS } from '../../screens/create_new_rule'; @@ -46,7 +50,7 @@ describe('Exceptions Table', () => { cleanKibana(); loginAndWaitForPageWithoutDateRange(ALERTS_URL); waitForAlertsIndexToBeCreated(); - createCustomRule(newRule); + createCustomRule(getNewRule()); goToManageAlertsDetectionRules(); goToRuleDetails(); @@ -56,11 +60,11 @@ describe('Exceptions Table', () => { // Add a detections exception list goToExceptionsTab(); - addsExceptionFromRuleSettings(exception); + addsExceptionFromRuleSettings(getException()); waitForTheRuleToBeExecuted(); // Create exception list not used by any rules - createExceptionList(exceptionList).as('exceptionListResponse'); + createExceptionList(getExceptionList()).as('exceptionListResponse'); goBackToAllRulesTable(); waitForRulesTableToBeLoaded(); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/from_alert.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/from_alert.spec.ts index 4918de7488ddd..8a683aacd5f66 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/from_alert.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/from_alert.spec.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { exception } from '../../objects/exception'; -import { newRule } from '../../objects/rule'; +import { getException } from '../../objects/exception'; +import { getNewRule } from '../../objects/rule'; import { ALERTS_COUNT, NUMBER_OF_ALERTS } from '../../screens/alerts'; import { RULE_STATUS } from '../../screens/create_new_rule'; @@ -43,7 +43,7 @@ describe('From alert', () => { cleanKibana(); loginAndWaitForPageWithoutDateRange(ALERTS_URL); waitForAlertsIndexToBeCreated(); - createCustomRule(newRule, 'rule_testing', '10s'); + createCustomRule(getNewRule(), 'rule_testing', '10s'); goToManageAlertsDetectionRules(); goToRuleDetails(); @@ -66,7 +66,7 @@ describe('From alert', () => { it('Creates an exception and deletes it', () => { addExceptionFromFirstAlert(); - addsException(exception); + addsException(getException()); esArchiverLoad('auditbeat_for_exceptions2'); cy.get(ALERTS_COUNT).should('exist'); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/from_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/from_rule.spec.ts index ea8988456d8b3..8fa0050a36521 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/from_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/from_rule.spec.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { exception } from '../../objects/exception'; -import { newRule } from '../../objects/rule'; +import { getException } from '../../objects/exception'; +import { getNewRule } from '../../objects/rule'; import { ALERTS_COUNT, NUMBER_OF_ALERTS } from '../../screens/alerts'; import { RULE_STATUS } from '../../screens/create_new_rule'; @@ -41,7 +41,7 @@ describe('From rule', () => { cleanKibana(); loginAndWaitForPageWithoutDateRange(ALERTS_URL); waitForAlertsIndexToBeCreated(); - createCustomRule(newRule, 'rule_testing', '10s'); + createCustomRule(getNewRule(), 'rule_testing', '10s'); goToManageAlertsDetectionRules(); goToRuleDetails(); @@ -64,7 +64,7 @@ describe('From rule', () => { it('Creates an exception and deletes it', () => { goToExceptionsTab(); - addsExceptionFromRuleSettings(exception); + addsExceptionFromRuleSettings(getException()); esArchiverLoad('auditbeat_for_exceptions2'); waitForTheRuleToBeExecuted(); goToAlertsTab(); diff --git a/x-pack/plugins/security_solution/cypress/integration/header/search_bar.spec.ts b/x-pack/plugins/security_solution/cypress/integration/header/search_bar.spec.ts index d7bef9d67df2f..c02c2bd9ec139 100644 --- a/x-pack/plugins/security_solution/cypress/integration/header/search_bar.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/header/search_bar.spec.ts @@ -8,7 +8,7 @@ import { loginAndWaitForPage } from '../../tasks/login'; import { openAddFilterPopover, fillAddFilterForm } from '../../tasks/search_bar'; import { GLOBAL_SEARCH_BAR_FILTER_ITEM } from '../../screens/search_bar'; -import { hostIpFilter } from '../../objects/filter'; +import { getHostIpFilter } from '../../objects/filter'; import { HOSTS_URL } from '../../urls/navigation'; import { waitForAllHostsToBeLoaded } from '../../tasks/hosts/all_hosts'; @@ -23,11 +23,11 @@ describe('SearchBar', () => { it('adds correctly a filter to the global search bar', () => { openAddFilterPopover(); - fillAddFilterForm(hostIpFilter); + fillAddFilterForm(getHostIpFilter()); cy.get(GLOBAL_SEARCH_BAR_FILTER_ITEM).should( 'have.text', - `${hostIpFilter.key}: ${hostIpFilter.value}` + `${getHostIpFilter().key}: ${getHostIpFilter().value}` ); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts index 3ff036fa0107f..ca9f83183ab10 100644 --- a/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts @@ -16,7 +16,7 @@ import overviewFixture from '../../fixtures/overview_search_strategy.json'; import emptyInstance from '../../fixtures/empty_instance.json'; import { cleanKibana } from '../../tasks/common'; import { createTimeline, favoriteTimeline } from '../../tasks/api_calls/timelines'; -import { timeline } from '../../objects/timeline'; +import { getTimeline } from '../../objects/timeline'; describe('Overview Page', () => { before(() => { @@ -53,7 +53,7 @@ describe('Overview Page', () => { describe('Favorite Timelines', () => { it('should appear on overview page', () => { - createTimeline(timeline) + createTimeline(getTimeline()) .then((response) => response.body.data.persistTimeline.timeline.savedObjectId) .then((timelineId: string) => { favoriteTimeline({ timelineId, timelineType: 'default' }).then(() => { @@ -61,7 +61,7 @@ describe('Overview Page', () => { loginAndWaitForPage(OVERVIEW_URL); cy.get('[data-test-subj="overview-recent-timelines"]').should( 'contain', - timeline.title + getTimeline().title ); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts index e2c1d7eef38c3..3930088f8bfdd 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_templates/creation.spec.ts @@ -5,14 +5,13 @@ * 2.0. */ -import { timeline } from '../../objects/timeline'; +import { getTimeline } from '../../objects/timeline'; import { FAVORITE_TIMELINE, LOCKED_ICON, NOTES, NOTES_TAB_BUTTON, - // NOTES_COUNT, NOTES_TEXT_AREA, PIN_EVENT, TIMELINE_DESCRIPTION, @@ -61,7 +60,7 @@ describe('Timeline Templates', () => { openTimelineUsingToggle(); createNewTimelineTemplate(); populateTimeline(); - addFilter(timeline.filter); + addFilter(getTimeline().filter); cy.get(PIN_EVENT).should( 'have.attr', 'aria-label', @@ -69,21 +68,21 @@ describe('Timeline Templates', () => { ); cy.get(LOCKED_ICON).should('be.visible'); - addNameToTimeline(timeline.title); + addNameToTimeline(getTimeline().title); cy.wait('@timeline').then(({ response }) => { const timelineId = response!.body.data.persistTimeline.timeline.savedObjectId; - addDescriptionToTimeline(timeline.description); - addNotesToTimeline(timeline.notes); + addDescriptionToTimeline(getTimeline().description); + addNotesToTimeline(getTimeline().notes); markAsFavorite(); waitForTimelineChanges(); createNewTimelineTemplate(); closeTimeline(); openTimelineTemplateFromSettings(timelineId); - cy.contains(timeline.title).should('exist'); - cy.get(TIMELINES_DESCRIPTION).first().should('have.text', timeline.description); + cy.contains(getTimeline().title).should('exist'); + cy.get(TIMELINES_DESCRIPTION).first().should('have.text', getTimeline().description); cy.get(TIMELINES_PINNED_EVENT_COUNT).first().should('have.text', '1'); cy.get(TIMELINES_NOTES_COUNT).first().should('have.text', '1'); cy.get(TIMELINES_FAVORITE).first().should('exist'); @@ -91,30 +90,30 @@ describe('Timeline Templates', () => { openTimeline(timelineId); cy.get(FAVORITE_TIMELINE).should('exist'); - cy.get(TIMELINE_TITLE).should('have.text', timeline.title); - cy.get(TIMELINE_DESCRIPTION).should('have.text', timeline.description); - cy.get(TIMELINE_QUERY).should('have.text', timeline.query); + cy.get(TIMELINE_TITLE).should('have.text', getTimeline().title); + cy.get(TIMELINE_DESCRIPTION).should('have.text', getTimeline().description); + cy.get(TIMELINE_QUERY).should('have.text', getTimeline().query); // Comments this assertion until we agreed what to do with the filters. // cy.get(TIMELINE_FILTER(timeline.filter)).should('exist'); // cy.get(NOTES_COUNT).should('have.text', '1'); cy.get(NOTES_TAB_BUTTON).click(); cy.get(NOTES_TEXT_AREA).should('exist'); - cy.get(NOTES).should('have.text', timeline.notes); + cy.get(NOTES).should('have.text', getTimeline().notes); }); }); it('Create template from timeline', () => { waitForTimelinesPanelToBeLoaded(); - createTimeline(timeline).then(() => { + createTimeline(getTimeline()).then(() => { expandEventAction(); clickingOnCreateTemplateFromTimelineBtn(); cy.wait('@timeline', { timeout: 100000 }).then(({ request }) => { expect(request.body.timeline).to.haveOwnProperty('templateTimelineId'); - expect(request.body.timeline).to.haveOwnProperty('description', timeline.description); + expect(request.body.timeline).to.haveOwnProperty('description', getTimeline().description); expect(request.body.timeline.kqlQuery.filterQuery.kuery).to.haveOwnProperty( 'expression', - timeline.query + getTimeline().query ); cy.get(TIMELINE_FLYOUT_WRAPPER).should('have.css', 'visibility', 'visible'); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_templates/export.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_templates/export.spec.ts index aa0a6c9308a52..5c2d87c9b727f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_templates/export.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_templates/export.spec.ts @@ -9,7 +9,7 @@ import { exportTimeline } from '../../tasks/timelines'; import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { expectedExportedTimelineTemplate, - timeline as timelineTemplate, + getTimeline as getTimelineTemplate, } from '../../objects/timeline'; import { TIMELINE_TEMPLATES_URL } from '../../urls/navigation'; @@ -20,7 +20,7 @@ describe('Export timelines', () => { beforeEach(() => { cleanKibana(); cy.intercept('POST', 'api/timeline/_export?file_name=timelines_export.ndjson').as('export'); - createTimelineTemplate(timelineTemplate).then((response) => { + createTimelineTemplate(getTimelineTemplate()).then((response) => { cy.wrap(response).as('templateResponse'); cy.wrap(response.body.data.persistTimeline.timeline.savedObjectId).as('templateId'); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts index 8a90b67682cb2..4203b9125d155 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { timeline } from '../../objects/timeline'; +import { getTimeline } from '../../objects/timeline'; import { LOCKED_ICON, @@ -64,7 +64,7 @@ describe('Timelines', (): void => { before(() => { openTimelineUsingToggle(); - addNameAndDescriptionToTimeline(timeline); + addNameAndDescriptionToTimeline(getTimeline()); populateTimeline(); }); @@ -73,8 +73,8 @@ describe('Timelines', (): void => { }); it('can be added filter', () => { - addFilter(timeline.filter); - cy.get(TIMELINE_FILTER(timeline.filter)).should('exist'); + addFilter(getTimeline().filter); + cy.get(TIMELINE_FILTER(getTimeline().filter)).should('exist'); }); it('pins an event', () => { @@ -89,8 +89,8 @@ describe('Timelines', (): void => { }); it('can be added notes', () => { - addNotesToTimeline(timeline.notes); - cy.get(NOTES_TEXT).should('have.text', timeline.notes); + addNotesToTimeline(getTimeline().notes); + cy.get(NOTES_TEXT).should('have.text', getTimeline().notes); }); it('should update timeline after adding eql', () => { @@ -116,17 +116,20 @@ describe('Create a timeline from a template', () => { }); it('Should have the same query and open the timeline modal', () => { - createTimelineTemplate(timeline).then(() => { + createTimelineTemplate(getTimeline()).then(() => { expandEventAction(); cy.intercept('/api/timeline').as('timeline'); clickingOnCreateTimelineFormTemplateBtn(); cy.wait('@timeline', { timeout: 100000 }).then(({ request }) => { if (request.body && request.body.timeline) { - expect(request.body.timeline).to.haveOwnProperty('description', timeline.description); + expect(request.body.timeline).to.haveOwnProperty( + 'description', + getTimeline().description + ); expect(request.body.timeline.kqlQuery.filterQuery.kuery).to.haveOwnProperty( 'expression', - timeline.query + getTimeline().query ); cy.get(TIMELINE_FLYOUT_WRAPPER).should('have.css', 'visibility', 'visible'); } diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/export.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/export.spec.ts index c2bd31c635a70..918a554db5606 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/export.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/export.spec.ts @@ -10,14 +10,14 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { TIMELINES_URL } from '../../urls/navigation'; import { createTimeline } from '../../tasks/api_calls/timelines'; -import { expectedExportedTimeline, timeline } from '../../objects/timeline'; +import { expectedExportedTimeline, getTimeline } from '../../objects/timeline'; import { cleanKibana } from '../../tasks/common'; describe('Export timelines', () => { beforeEach(() => { cleanKibana(); cy.intercept('POST', '/api/timeline/_export?file_name=timelines_export.ndjson').as('export'); - createTimeline(timeline).then((response) => { + createTimeline(getTimeline()).then((response) => { cy.wrap(response).as('timelineResponse'); cy.wrap(response.body.data.persistTimeline.timeline.savedObjectId).as('timelineId'); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts index 24309b8fda084..0a784cf952ca6 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { timelineNonValidQuery } from '../../objects/timeline'; +import { getTimelineNonValidQuery } from '../../objects/timeline'; import { NOTES_AUTHOR, @@ -39,7 +39,7 @@ describe('Timeline notes tab', () => { loginAndWaitForPageWithoutDateRange(TIMELINES_URL); waitForTimelinesPanelToBeLoaded(); - createTimeline(timelineNonValidQuery) + createTimeline(getTimelineNonValidQuery()) .then((response) => response.body.data.persistTimeline.timeline.savedObjectId) .then((timelineId: string) => refreshTimelinesUntilTimeLinePresent(timelineId) @@ -56,16 +56,16 @@ describe('Timeline notes tab', () => { }); it('should render mockdown', () => { cy.intercept('/api/note').as(`updateNote`); - addNotesToTimeline(timelineNonValidQuery.notes); + addNotesToTimeline(getTimelineNonValidQuery().notes); cy.wait('@updateNote').its('response.statusCode').should('eq', 200); cy.get(NOTES_TEXT_AREA).should('exist'); }); it('should contain notes', () => { cy.intercept('/api/note').as(`updateNote`); - addNotesToTimeline(timelineNonValidQuery.notes); + addNotesToTimeline(getTimelineNonValidQuery().notes); cy.wait('@updateNote').its('response.statusCode').should('eq', 200); - cy.get(NOTES_TEXT).first().should('have.text', timelineNonValidQuery.notes); + cy.get(NOTES_TEXT).first().should('have.text', getTimelineNonValidQuery().notes); }); it('should be able to render font in bold', () => { @@ -91,7 +91,7 @@ describe('Timeline notes tab', () => { it('should render the right author', () => { cy.intercept('/api/note').as(`updateNote`); - addNotesToTimeline(timelineNonValidQuery.notes); + addNotesToTimeline(getTimelineNonValidQuery().notes); cy.wait('@updateNote').its('response.statusCode').should('eq', 200); cy.get(NOTES_AUTHOR).first().should('have.text', text); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/open_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/open_timeline.spec.ts index 814631b2af636..5c620a983b2b3 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/open_timeline.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/open_timeline.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { timeline } from '../../objects/timeline'; +import { getTimeline } from '../../objects/timeline'; import { TIMELINE_DESCRIPTION, TIMELINE_TITLE, OPEN_TIMELINE_MODAL } from '../../screens/timeline'; import { @@ -39,7 +39,7 @@ describe('Open timeline', () => { loginAndWaitForPageWithoutDateRange(TIMELINES_URL); waitForTimelinesPanelToBeLoaded(); - createTimeline(timeline) + createTimeline(getTimeline()) .then((response) => response.body.data.persistTimeline.timeline.savedObjectId) .then((timelineId: string) => { refreshTimelinesUntilTimeLinePresent(timelineId) @@ -47,7 +47,7 @@ describe('Open timeline', () => { // request responses and indeterminism since on clicks to activates URL's. .then(() => cy.wait(1000)) .then(() => - addNoteToTimeline(timeline.notes, timelineId).should((response) => + addNoteToTimeline(getTimeline().notes, timelineId).should((response) => expect(response.status).to.equal(200) ) ) @@ -71,11 +71,11 @@ describe('Open timeline', () => { }); it('should display timeline info - title', () => { - cy.contains(timeline.title).should('exist'); + cy.contains(getTimeline().title).should('exist'); }); it('should display timeline info - description', () => { - cy.get(TIMELINES_DESCRIPTION).first().should('have.text', timeline.description); + cy.get(TIMELINES_DESCRIPTION).first().should('have.text', getTimeline().description); }); it('should display timeline info - pinned event count', () => { @@ -91,11 +91,11 @@ describe('Open timeline', () => { }); it('should display timeline content - title', () => { - cy.get(TIMELINE_TITLE).should('have.text', timeline.title); + cy.get(TIMELINE_TITLE).should('have.text', getTimeline().title); }); it('should display timeline content - description', () => { - cy.get(TIMELINE_DESCRIPTION).should('have.text', timeline.description); + cy.get(TIMELINE_DESCRIPTION).should('have.text', getTimeline().description); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts index f37a66ac048fb..06891121d6354 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { timeline } from '../../objects/timeline'; +import { getTimeline } from '../../objects/timeline'; import { UNLOCKED_ICON, @@ -38,7 +38,7 @@ describe('Timeline query tab', () => { loginAndWaitForPageWithoutDateRange(TIMELINES_URL); waitForTimelinesPanelToBeLoaded(); - createTimeline(timeline) + createTimeline(getTimeline()) .then((response) => response.body.data.persistTimeline.timeline.savedObjectId) .then((timelineId: string) => { refreshTimelinesUntilTimeLinePresent(timelineId) @@ -46,14 +46,14 @@ describe('Timeline query tab', () => { // request responses and indeterminism since on clicks to activates URL's. .then(() => cy.wait(1000)) .then(() => - addNoteToTimeline(timeline.notes, timelineId).should((response) => + addNoteToTimeline(getTimeline().notes, timelineId).should((response) => expect(response.status).to.equal(200) ) ) .then(() => openTimelineById(timelineId)) .then(() => pinFirstEvent()) .then(() => persistNoteToFirstEvent('event note')) - .then(() => addFilter(timeline.filter)); + .then(() => addFilter(getTimeline().filter)); }); }); @@ -63,7 +63,7 @@ describe('Timeline query tab', () => { }); it('should contain the right query', () => { - cy.get(TIMELINE_QUERY).should('have.text', `${timeline.query}`); + cy.get(TIMELINE_QUERY).should('have.text', `${getTimeline().query}`); }); it('should be able to add event note', () => { @@ -71,7 +71,7 @@ describe('Timeline query tab', () => { }); it('should display timeline filter', () => { - cy.get(TIMELINE_FILTER(timeline.filter)).should('exist'); + cy.get(TIMELINE_FILTER(getTimeline().filter)).should('exist'); }); it('should display pinned events', () => { diff --git a/x-pack/plugins/security_solution/cypress/integration/urls/state.spec.ts b/x-pack/plugins/security_solution/cypress/integration/urls/state.spec.ts index 842dd85b42ef8..a72657d78b70d 100644 --- a/x-pack/plugins/security_solution/cypress/integration/urls/state.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/urls/state.spec.ts @@ -37,7 +37,7 @@ import { addNameToTimeline, closeTimeline, populateTimeline } from '../../tasks/ import { HOSTS_URL } from '../../urls/navigation'; import { ABSOLUTE_DATE_RANGE } from '../../urls/state'; -import { timeline } from '../../objects/timeline'; +import { getTimeline } from '../../objects/timeline'; import { TIMELINE } from '../../screens/create_new_case'; import { cleanKibana } from '../../tasks/common'; @@ -244,7 +244,7 @@ describe('url state', () => { cy.intercept('PATCH', '/api/timeline').as('timeline'); - addNameToTimeline(timeline.title); + addNameToTimeline(getTimeline().title); cy.wait('@timeline').then(({ response }) => { closeTimeline(); @@ -256,7 +256,7 @@ describe('url state', () => { cy.get(DATE_PICKER_APPLY_BUTTON_TIMELINE).should('not.have.text', 'Updating'); cy.get(TIMELINE).should('be.visible'); cy.get(TIMELINE_TITLE).should('be.visible'); - cy.get(TIMELINE_TITLE).should('have.text', timeline.title); + cy.get(TIMELINE_TITLE).should('have.text', getTimeline().title); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/objects/case.ts b/x-pack/plugins/security_solution/cypress/objects/case.ts index 847236688dee7..8bc90c5fa2a3b 100644 --- a/x-pack/plugins/security_solution/cypress/objects/case.ts +++ b/x-pack/plugins/security_solution/cypress/objects/case.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CompleteTimeline, timeline } from './timeline'; +import { CompleteTimeline, getTimeline } from './timeline'; export interface TestCase extends TestCaseWithoutTimeline { timeline: CompleteTimeline; @@ -43,49 +43,50 @@ export interface IbmResilientConnectorOptions { incidentTypes: string[]; } -export const case1: TestCase = { +export const getCase1 = (): TestCase => ({ name: 'This is the title of the case', tags: ['Tag1', 'Tag2'], description: 'This is the case description', - timeline, + timeline: getTimeline(), reporter: 'elastic', owner: 'securitySolution', -}; +}); -export const serviceNowConnector: Connector = { +export const getServiceNowConnector = (): Connector => ({ connectorName: 'New connector', URL: 'https://www.test.service-now.com', username: 'Username Name', password: 'password', -}; +}); -export const jiraConnectorOptions: JiraConnectorOptions = { +export const getJiraConnectorOptions = (): JiraConnectorOptions => ({ issueType: '10006', priority: 'High', -}; +}); -export const serviceNowConnectorOpions: ServiceNowconnectorOptions = { +export const getServiceNowConnectorOptions = (): ServiceNowconnectorOptions => ({ urgency: '2', severity: '1', impact: '3', -}; +}); -export const ibmResilientConnectorOptions: IbmResilientConnectorOptions = { +export const getIbmResilientConnectorOptions = (): IbmResilientConnectorOptions => ({ title: 'Resilient', severity: 'Medium', incidentTypes: ['Communication error (fax; email)', 'Denial of Service'], -}; +}); export const TIMELINE_CASE_ID = '68248e00-f689-11ea-9ab2-59238b522856'; -export const connectorIds = { + +export const getConnectorIds = () => ({ jira: '000e5f86-08b0-4882-adfd-6df981d45c1b', sn: '93a69ba3-3c31-4b4c-bf86-cc79a090f437', resilient: 'a6a8dd7f-7e88-48fe-9b9f-70b668da8cbc', -}; +}); -export const mockConnectorsResponse = [ +export const getMockConnectorsResponse = () => [ { - id: connectorIds.jira, + id: getConnectorIds().jira, actionTypeId: '.jira', name: 'Jira', config: { @@ -96,7 +97,7 @@ export const mockConnectorsResponse = [ referencedByCount: 0, }, { - id: connectorIds.resilient, + id: getConnectorIds().resilient, actionTypeId: '.resilient', name: 'Resilient', config: { @@ -107,7 +108,7 @@ export const mockConnectorsResponse = [ referencedByCount: 0, }, { - id: connectorIds.sn, + id: getConnectorIds().sn, actionTypeId: '.servicenow', name: 'ServiceNow', config: { @@ -117,7 +118,8 @@ export const mockConnectorsResponse = [ referencedByCount: 0, }, ]; -export const executeResponses = { + +export const getExecuteResponses = () => ({ servicenow: { choices: { status: 'ok', @@ -208,7 +210,7 @@ export const executeResponses = { { id: '10006', name: 'Task' }, { id: '10007', name: 'Sub-task' }, ], - actionId: connectorIds.jira, + actionId: getConnectorIds().jira, }, fieldsByIssueType: { status: 'ok', @@ -299,7 +301,7 @@ export const executeResponses = { timetracking: { allowedValues: [], defaultValue: {} }, labels: { allowedValues: [], defaultValue: {} }, }, - actionId: connectorIds.jira, + actionId: getConnectorIds().jira, }, }, resilient: { @@ -309,7 +311,7 @@ export const executeResponses = { { id: 17, name: 'Communication error (fax; email)' }, { id: 21, name: 'Denial of Service' }, ], - actionId: connectorIds.resilient, + actionId: getConnectorIds().resilient, }, severity: { status: 'ok', @@ -318,7 +320,7 @@ export const executeResponses = { { id: 5, name: 'Medium' }, { id: 6, name: 'High' }, ], - actionId: connectorIds.resilient, + actionId: getConnectorIds().resilient, }, }, -}; +}); diff --git a/x-pack/plugins/security_solution/cypress/objects/connector.ts b/x-pack/plugins/security_solution/cypress/objects/connector.ts new file mode 100644 index 0000000000000..a5244583bf494 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/objects/connector.ts @@ -0,0 +1,24 @@ +/* + * 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 EmailConnector { + name: string; + from: string; + host: string; + port: string; + user: string; + password: string; +} + +export const getEmailConnector = (): EmailConnector => ({ + name: 'Test connector', + from: 'test@example.com', + host: 'example.com', + port: '80', + user: 'username', + password: 'password', +}); diff --git a/x-pack/plugins/security_solution/cypress/objects/exception.ts b/x-pack/plugins/security_solution/cypress/objects/exception.ts index 73457f10ccec6..6a934e1ec4651 100644 --- a/x-pack/plugins/security_solution/cypress/objects/exception.ts +++ b/x-pack/plugins/security_solution/cypress/objects/exception.ts @@ -20,22 +20,22 @@ export interface ExceptionList { type: 'detection' | 'endpoint'; } -export const exceptionList: ExceptionList = { +export const getExceptionList = (): ExceptionList => ({ description: 'Test exception list description', list_id: 'test_exception_list', name: 'Test exception list', namespace_type: 'single', tags: ['test tag'], type: 'detection', -}; +}); -export const exception: Exception = { +export const getException = (): Exception => ({ field: 'host.name', operator: 'is', values: ['suricata-iowa'], -}; +}); -export const expectedExportedExceptionList = (exceptionListResponse: Cypress.Response) => { +export const expectedExportedExceptionList = (exceptionListResponse: Cypress.Response): string => { const jsonrule = exceptionListResponse.body; return `{"_version":"${jsonrule._version}","created_at":"${jsonrule.created_at}","created_by":"elastic","description":"${jsonrule.description}","id":"${jsonrule.id}","immutable":false,"list_id":"test_exception_list","name":"Test exception list","namespace_type":"single","os_types":[],"tags":[],"tie_breaker_id":"${jsonrule.tie_breaker_id}","type":"detection","updated_at":"${jsonrule.updated_at}","updated_by":"elastic","version":1}\n"\n""\n{"exception_list_items_details":"{"exported_count":0}\n"}`; diff --git a/x-pack/plugins/security_solution/cypress/objects/filter.ts b/x-pack/plugins/security_solution/cypress/objects/filter.ts index b00954de17422..5a69100a4b38a 100644 --- a/x-pack/plugins/security_solution/cypress/objects/filter.ts +++ b/x-pack/plugins/security_solution/cypress/objects/filter.ts @@ -10,7 +10,7 @@ export interface SearchBarFilter { value: string; } -export const hostIpFilter: SearchBarFilter = { +export const getHostIpFilter = (): SearchBarFilter => ({ key: 'host.ip', value: '1.1.1.1', -}; +}); diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index 3383ef4996ead..a10fa5b0eda78 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -7,8 +7,8 @@ /* eslint-disable @kbn/eslint/no-restricted-paths */ import { rawRules } from '../../server/lib/detection_engine/rules/prepackaged_rules/index'; -import { mockThreatData } from '../../public/detections/mitre/mitre_tactics_techniques'; -import { timeline, CompleteTimeline, indicatorMatchTimelineTemplate } from './timeline'; +import { getMockThreatData } from '../../public/detections/mitre/mitre_tactics_techniques'; +import { getTimeline, CompleteTimeline, getIndicatorMatchTimelineTemplate } from './timeline'; export const totalNumberOfPrebuiltRules = rawRules.length; @@ -96,7 +96,7 @@ export interface MachineLearningRule { lookBack: Interval; } -export const indexPatterns = [ +export const getIndexPatterns = (): string[] => [ 'apm-*-transaction*', 'auditbeat-*', 'endgame-*', @@ -106,67 +106,69 @@ export const indexPatterns = [ 'winlogbeat-*', ]; -const { tactic, technique, subtechnique } = mockThreatData; - -const mitre1: Mitre = { - tactic: `${tactic.name} (${tactic.id})`, +const getMitre1 = (): Mitre => ({ + tactic: `${getMockThreatData().tactic.name} (${getMockThreatData().tactic.id})`, techniques: [ { - name: `${technique.name} (${technique.id})`, - subtechniques: [`${subtechnique.name} (${subtechnique.id})`], + name: `${getMockThreatData().technique.name} (${getMockThreatData().technique.id})`, + subtechniques: [ + `${getMockThreatData().subtechnique.name} (${getMockThreatData().subtechnique.id})`, + ], }, { - name: `${technique.name} (${technique.id})`, + name: `${getMockThreatData().technique.name} (${getMockThreatData().technique.id})`, subtechniques: [], }, ], -}; +}); -const mitre2: Mitre = { - tactic: `${tactic.name} (${tactic.id})`, +const getMitre2 = (): Mitre => ({ + tactic: `${getMockThreatData().tactic.name} (${getMockThreatData().tactic.id})`, techniques: [ { - name: `${technique.name} (${technique.id})`, - subtechniques: [`${subtechnique.name} (${subtechnique.id})`], + name: `${getMockThreatData().technique.name} (${getMockThreatData().technique.id})`, + subtechniques: [ + `${getMockThreatData().subtechnique.name} (${getMockThreatData().subtechnique.id})`, + ], }, ], -}; +}); -const severityOverride1: SeverityOverride = { +const getSeverityOverride1 = (): SeverityOverride => ({ sourceField: 'host.name', sourceValue: 'host', -}; +}); -const severityOverride2: SeverityOverride = { +const getSeverityOverride2 = (): SeverityOverride => ({ sourceField: '@timestamp', sourceValue: '10/02/2020', -}; +}); -const severityOverride3: SeverityOverride = { +const getSeverityOverride3 = (): SeverityOverride => ({ sourceField: 'host.geo.name', sourceValue: 'atack', -}; +}); -const severityOverride4: SeverityOverride = { +const getSeverityOverride4 = (): SeverityOverride => ({ sourceField: 'agent.type', sourceValue: 'auditbeat', -}; +}); -const runsEvery: Interval = { +const getRunsEvery = (): Interval => ({ interval: '1', timeType: 'Seconds', type: 's', -}; +}); -const lookBack: Interval = { +const getLookBack = (): Interval => ({ interval: '17520', timeType: 'Hours', type: 'h', -}; +}); -export const newRule: CustomRule = { +export const getNewRule = (): CustomRule => ({ customQuery: 'host.name: *', - index: indexPatterns, + index: getIndexPatterns(), name: 'New Rule Test', description: 'The new rule description.', severity: 'High', @@ -174,15 +176,15 @@ export const newRule: CustomRule = { tags: ['test', 'newRule'], referenceUrls: ['http://example.com/', 'https://example.com/'], falsePositivesExamples: ['False1', 'False2'], - mitre: [mitre1, mitre2], + mitre: [getMitre1(), getMitre2()], note: '# test markdown', - runsEvery, - lookBack, - timeline, + runsEvery: getRunsEvery(), + lookBack: getLookBack(), + timeline: getTimeline(), maxSignals: 100, -}; +}); -export const unmappedRule: CustomRule = { +export const getUnmappedRule = (): CustomRule => ({ customQuery: '*:*', index: ['unmapped*'], name: 'Rule with unmapped fields', @@ -192,15 +194,15 @@ export const unmappedRule: CustomRule = { tags: ['test', 'newRule'], referenceUrls: ['http://example.com/', 'https://example.com/'], falsePositivesExamples: ['False1', 'False2'], - mitre: [mitre1, mitre2], + mitre: [getMitre1(), getMitre2()], note: '# test markdown', - runsEvery, - lookBack, - timeline, + runsEvery: getRunsEvery(), + lookBack: getLookBack(), + timeline: getTimeline(), maxSignals: 100, -}; +}); -export const unmappedCCSRule: CustomRule = { +export const getUnmappedCCSRule = (): CustomRule => ({ customQuery: '*:*', index: [`${ccsRemoteName}:unmapped*`], name: 'Rule with unmapped fields', @@ -210,15 +212,15 @@ export const unmappedCCSRule: CustomRule = { tags: ['test', 'newRule'], referenceUrls: ['http://example.com/', 'https://example.com/'], falsePositivesExamples: ['False1', 'False2'], - mitre: [mitre1, mitre2], + mitre: [getMitre1(), getMitre2()], note: '# test markdown', - runsEvery, - lookBack, - timeline, + runsEvery: getRunsEvery(), + lookBack: getLookBack(), + timeline: getTimeline(), maxSignals: 100, -}; +}); -export const existingRule: CustomRule = { +export const getExistingRule = (): CustomRule => ({ customQuery: 'host.name: *', name: 'Rule 1', description: 'Description for Rule 1', @@ -231,17 +233,17 @@ export const existingRule: CustomRule = { falsePositivesExamples: [], mitre: [], note: 'This is my note', - runsEvery, - lookBack, - timeline, + runsEvery: getRunsEvery(), + lookBack: getLookBack(), + timeline: getTimeline(), // Please do not change, or if you do, needs // to be any number other than default value maxSignals: 500, -}; +}); -export const newOverrideRule: OverrideRule = { +export const getNewOverrideRule = (): OverrideRule => ({ customQuery: 'host.name: *', - index: indexPatterns, + index: getIndexPatterns(), name: 'Override Rule', description: 'The new rule description.', severity: 'High', @@ -249,21 +251,26 @@ export const newOverrideRule: OverrideRule = { tags: ['test', 'newRule'], referenceUrls: ['http://example.com/', 'https://example.com/'], falsePositivesExamples: ['False1', 'False2'], - mitre: [mitre1, mitre2], + mitre: [getMitre1(), getMitre2()], note: '# test markdown', - severityOverride: [severityOverride1, severityOverride2, severityOverride3, severityOverride4], + severityOverride: [ + getSeverityOverride1(), + getSeverityOverride2(), + getSeverityOverride3(), + getSeverityOverride4(), + ], riskOverride: 'destination.port', nameOverride: 'agent.type', timestampOverride: '@timestamp', - runsEvery, - lookBack, - timeline, + runsEvery: getRunsEvery(), + lookBack: getLookBack(), + timeline: getTimeline(), maxSignals: 100, -}; +}); -export const newThresholdRule: ThresholdRule = { +export const getNewThresholdRule = (): ThresholdRule => ({ customQuery: 'host.name: *', - index: indexPatterns, + index: getIndexPatterns(), name: 'Threshold Rule', description: 'The new rule description.', severity: 'High', @@ -271,17 +278,17 @@ export const newThresholdRule: ThresholdRule = { tags: ['test', 'newRule'], referenceUrls: ['http://example.com/', 'https://example.com/'], falsePositivesExamples: ['False1', 'False2'], - mitre: [mitre1, mitre2], + mitre: [getMitre1(), getMitre2()], note: '# test markdown', thresholdField: 'host.name', threshold: '10', - runsEvery, - lookBack, - timeline, + runsEvery: getRunsEvery(), + lookBack: getLookBack(), + timeline: getTimeline(), maxSignals: 100, -}; +}); -export const machineLearningRule: MachineLearningRule = { +export const getMachineLearningRule = (): MachineLearningRule => ({ machineLearningJobs: ['linux_anomalous_network_service', 'linux_anomalous_network_activity_ecs'], anomalyScoreThreshold: '20', name: 'New ML Rule Test', @@ -291,52 +298,52 @@ export const machineLearningRule: MachineLearningRule = { tags: ['ML'], referenceUrls: ['https://elastic.co/'], falsePositivesExamples: ['False1'], - mitre: [mitre1], + mitre: [getMitre1()], note: '# test markdown', - runsEvery, - lookBack, -}; + runsEvery: getRunsEvery(), + lookBack: getLookBack(), +}); -export const eqlRule: CustomRule = { +export const getEqlRule = (): CustomRule => ({ customQuery: 'any where process.name == "which"', name: 'New EQL Rule', - index: indexPatterns, + index: getIndexPatterns(), description: 'New EQL rule description.', severity: 'High', riskScore: '17', tags: ['test', 'newRule'], referenceUrls: ['http://example.com/', 'https://example.com/'], falsePositivesExamples: ['False1', 'False2'], - mitre: [mitre1, mitre2], + mitre: [getMitre1(), getMitre2()], note: '# test markdown', - runsEvery, - lookBack, - timeline, + runsEvery: getRunsEvery(), + lookBack: getLookBack(), + timeline: getTimeline(), maxSignals: 100, -}; +}); -export const eqlSequenceRule: CustomRule = { +export const getEqlSequenceRule = (): CustomRule => ({ customQuery: 'sequence with maxspan=30s\ [any where process.name == "which"]\ [any where process.name == "xargs"]', name: 'New EQL Sequence Rule', - index: indexPatterns, + index: getIndexPatterns(), description: 'New EQL rule description.', severity: 'High', riskScore: '17', tags: ['test', 'newRule'], referenceUrls: ['http://example.com/', 'https://example.com/'], falsePositivesExamples: ['False1', 'False2'], - mitre: [mitre1, mitre2], + mitre: [getMitre1(), getMitre2()], note: '# test markdown', - runsEvery, - lookBack, - timeline, + runsEvery: getRunsEvery(), + lookBack: getLookBack(), + timeline: getTimeline(), maxSignals: 100, -}; +}); -export const newThreatIndicatorRule: ThreatIndicatorRule = { +export const getNewThreatIndicatorRule = (): ThreatIndicatorRule => ({ name: 'Threat Indicator Rule Test', description: 'The threat indicator rule description.', index: ['suspicious-*'], @@ -345,31 +352,31 @@ export const newThreatIndicatorRule: ThreatIndicatorRule = { tags: ['test', 'threat'], referenceUrls: ['http://example.com/', 'https://example.com/'], falsePositivesExamples: ['False1', 'False2'], - mitre: [mitre1, mitre2], + mitre: [getMitre1(), getMitre2()], note: '# test markdown', - runsEvery, - lookBack, + runsEvery: getRunsEvery(), + lookBack: getLookBack(), indicatorIndexPattern: ['filebeat-*'], indicatorMappingField: 'myhash.mysha256', indicatorIndexField: 'threatintel.indicator.file.hash.sha256', type: 'file', atomic: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3', - timeline: indicatorMatchTimelineTemplate, + timeline: getIndicatorMatchTimelineTemplate(), maxSignals: 100, -}; +}); -export const duplicatedRuleName = `${newThreatIndicatorRule.name} [Duplicate]`; +export const duplicatedRuleName = `${getNewThreatIndicatorRule().name} [Duplicate]`; -export const severitiesOverride = ['Low', 'Medium', 'High', 'Critical']; +export const getSeveritiesOverride = (): string[] => ['Low', 'Medium', 'High', 'Critical']; -export const editedRule = { - ...existingRule, +export const getEditedRule = (): CustomRule => ({ + ...getExistingRule(), severity: 'Medium', description: 'Edited Rule description', - tags: [...existingRule.tags, 'edited'], -}; + tags: [...getExistingRule().tags, 'edited'], +}); -export const expectedExportedRule = (ruleResponse: Cypress.Response) => { +export const expectedExportedRule = (ruleResponse: Cypress.Response): string => { const jsonrule = ruleResponse.body; return `{"id":"${jsonrule.id}","updated_at":"${jsonrule.updated_at}","updated_by":"elastic","created_at":"${jsonrule.created_at}","created_by":"elastic","name":"${jsonrule.name}","tags":[],"interval":"100m","enabled":false,"description":"${jsonrule.description}","risk_score":${jsonrule.risk_score},"severity":"${jsonrule.severity}","output_index":".siem-signals-default","author":[],"false_positives":[],"from":"now-17520h","rule_id":"rule_testing","max_signals":100,"risk_score_mapping":[],"severity_mapping":[],"threat":[],"to":"now","references":[],"version":1,"exceptions_list":[],"immutable":false,"type":"query","language":"kuery","index":["exceptions-*"],"query":"${jsonrule.query}","throttle":"no_actions","actions":[]}\n{"exported_count":1,"missing_rules":[],"missing_rules_count":0}\n`; diff --git a/x-pack/plugins/security_solution/cypress/objects/timeline.ts b/x-pack/plugins/security_solution/cypress/objects/timeline.ts index 1b66b50605508..c13c1b01ef0ed 100644 --- a/x-pack/plugins/security_solution/cypress/objects/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/objects/timeline.ts @@ -24,51 +24,51 @@ export interface TimelineFilter { value?: string; } -export const filter: TimelineFilter = { +export const getFilter = (): TimelineFilter => ({ field: 'host.name', operator: 'exists', value: 'exists', -}; +}); -export const timeline: CompleteTimeline = { +export const getTimeline = (): CompleteTimeline => ({ title: 'Security Timeline', description: 'This is the best timeline', query: 'host.name: *', notes: 'Yes, the best timeline', - filter, -}; + filter: getFilter(), +}); -export const indicatorMatchTimelineTemplate: CompleteTimeline = { - ...timeline, +export const getIndicatorMatchTimelineTemplate = (): CompleteTimeline => ({ + ...getTimeline(), title: 'Generic Threat Match Timeline', templateTimelineId: '495ad7a7-316e-4544-8a0f-9c098daee76e', -}; +}); /** * Timeline query that finds no valid data to cut down on test failures * or other issues for when we want to test one specific thing and not also * test the queries happening */ -export const timelineNonValidQuery: CompleteTimeline = { - ...timeline, +export const getTimelineNonValidQuery = (): CompleteTimeline => ({ + ...getTimeline(), query: 'query_to_intentionally_find_nothing: *', -}; +}); -export const caseTimeline: Timeline = { +export const caseTimeline = (): Timeline => ({ title: 'SIEM test', description: 'description', query: 'host.name: *', id: '0162c130-78be-11ea-9718-118a926974a4', -}; +}); -export const expectedExportedTimelineTemplate = (templateResponse: Cypress.Response) => { +export const expectedExportedTimelineTemplate = (templateResponse: Cypress.Response): string => { const timelineTemplateBody = templateResponse.body.data.persistTimeline.timeline; return `{"savedObjectId":"${timelineTemplateBody.savedObjectId}","version":"${timelineTemplateBody.version}","columns":[{"id":"@timestamp"},{"id":"user.name"},{"id":"event.category"},{"id":"event.action"},{"id":"host.name"}],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"expression":"${timelineTemplateBody.kqlQuery.filterQuery.kuery.expression}","kind":"kuery"}}},"dateRange":{"start":"${timelineTemplateBody.dateRange.start}","end":"${timelineTemplateBody.dateRange.end}"},"description":"${timelineTemplateBody.description}","title":"${timelineTemplateBody.title}","templateTimelineVersion":1,"timelineType":"template","created":${timelineTemplateBody.created},"createdBy":"elastic","updated":${timelineTemplateBody.updated},"updatedBy":"elastic","sort":[],"eventNotes":[],"globalNotes":[],"pinnedEventIds":[]} `; }; -export const expectedExportedTimeline = (timelineResponse: Cypress.Response) => { +export const expectedExportedTimeline = (timelineResponse: Cypress.Response): string => { const timelineBody = timelineResponse.body.data.persistTimeline.timeline; return `{"savedObjectId":"${timelineBody.savedObjectId}","version":"${timelineBody.version}","columns":[{"id":"@timestamp"},{"id":"user.name"},{"id":"event.category"},{"id":"event.action"},{"id":"host.name"}],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"expression":"${timelineBody.kqlQuery.filterQuery.kuery.expression}","kind":"kuery"}}},"dateRange":{"start":"${timelineBody.dateRange.start}","end":"${timelineBody.dateRange.end}"},"description":"${timelineBody.description}","title":"${timelineBody.title}","created":${timelineBody.created},"createdBy":"elastic","updated":${timelineBody.updated},"updatedBy":"elastic","timelineType":"default","sort":[],"eventNotes":[],"globalNotes":[],"pinnedEventIds":[]}\n`; diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts index a580068b636e4..551857ca3bfca 100644 --- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts @@ -16,6 +16,30 @@ export const ACTIONS_EDIT_TAB = '[data-test-subj="edit-rule-actions-tab"]'; export const ACTIONS_THROTTLE_INPUT = '[data-test-subj="stepRuleActions"] [data-test-subj="select"]'; +export const EMAIL_ACTION_BTN = '[data-test-subj=".email-ActionTypeSelectOption"]'; + +export const CREATE_ACTION_CONNECTOR_BTN = '[data-test-subj="createActionConnectorButton-0"]'; + +export const SAVE_ACTION_CONNECTOR_BTN = '[data-test-subj="saveActionButtonModal"]'; + +export const EMAIL_ACTION_TO_INPUT = '[data-test-subj="toEmailAddressInput"]'; + +export const EMAIL_ACTION_SUBJECT_INPUT = '[data-test-subj="subjectInput"]'; + +export const FROM_VALIDATION_ERROR = '.euiFormErrorText'; + +export const CONNECTOR_NAME_INPUT = '[data-test-subj="nameInput"]'; + +export const EMAIL_CONNECTOR_FROM_INPUT = '[data-test-subj="emailFromInput"]'; + +export const EMAIL_CONNECTOR_HOST_INPUT = '[data-test-subj="emailHostInput"]'; + +export const EMAIL_CONNECTOR_PORT_INPUT = '[data-test-subj="emailPortInput"]'; + +export const EMAIL_CONNECTOR_USER_INPUT = '[data-test-subj="emailUserInput"]'; + +export const EMAIL_CONNECTOR_PASSWORD_INPUT = '[data-test-subj="emailPasswordInput"]'; + export const ADD_FALSE_POSITIVE_BTN = '[data-test-subj="detectionEngineStepAboutRuleFalsePositives"] .euiButtonEmpty__text'; diff --git a/x-pack/plugins/security_solution/cypress/screens/edit_connector.ts b/x-pack/plugins/security_solution/cypress/screens/edit_connector.ts index 5b353983e5a92..598485b167c9f 100644 --- a/x-pack/plugins/security_solution/cypress/screens/edit_connector.ts +++ b/x-pack/plugins/security_solution/cypress/screens/edit_connector.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { connectorIds } from '../objects/case'; +import { getConnectorIds } from '../objects/case'; export const CONNECTOR_RESILIENT = `[data-test-subj="connector-fields-resilient"]`; @@ -17,14 +17,16 @@ export const SELECT_INCIDENT_TYPE = `[data-test-subj="incidentTypeComboBox"] inp export const SELECT_ISSUE_TYPE = `[data-test-subj="issueTypeSelect"]`; -export const SELECT_JIRA = `[data-test-subj="dropdown-connector-${connectorIds.jira}"]`; +export const SELECT_JIRA = `[data-test-subj="dropdown-connector-${getConnectorIds().jira}"]`; export const SELECT_PRIORITY = `[data-test-subj="prioritySelect"]`; -export const SELECT_RESILIENT = `[data-test-subj="dropdown-connector-${connectorIds.resilient}"]`; +export const SELECT_RESILIENT = `[data-test-subj="dropdown-connector-${ + getConnectorIds().resilient +}"]`; export const SELECT_SEVERITY = `[data-test-subj="severitySelect"]`; -export const SELECT_SN = `[data-test-subj="dropdown-connector-${connectorIds.sn}"]`; +export const SELECT_SN = `[data-test-subj="dropdown-connector-${getConnectorIds().sn}"]`; export const SELECT_URGENCY = `[data-test-subj="urgencySelect"]`; diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index 9c15b1f03932d..d8d91dc9ca624 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { getEmailConnector, EmailConnector } from '../objects/connector'; import { CustomRule, MachineLearningRule, - machineLearningRule, + getMachineLearningRule, OverrideRule, ThreatIndicatorRule, ThresholdRule, @@ -85,6 +86,12 @@ import { THRESHOLD_FIELD_SELECTION, THRESHOLD_INPUT_AREA, THRESHOLD_TYPE, + CONNECTOR_NAME_INPUT, + EMAIL_CONNECTOR_FROM_INPUT, + EMAIL_CONNECTOR_HOST_INPUT, + EMAIL_CONNECTOR_PORT_INPUT, + EMAIL_CONNECTOR_USER_INPUT, + EMAIL_CONNECTOR_PASSWORD_INPUT, } from '../screens/create_new_rule'; import { TOAST_ERROR } from '../screens/shared'; import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline'; @@ -268,7 +275,7 @@ export const fillDefineThresholdRule = (rule: ThresholdRule) => { cy.get(TIMELINE(rule.timeline.id!)).click(); cy.get(COMBO_BOX_CLEAR_BTN).click(); - rule.index!.forEach((index) => { + rule.index.forEach((index) => { cy.get(COMBO_BOX_INPUT).first().type(`${index}{enter}`); }); @@ -390,6 +397,15 @@ export const fillIndexAndIndicatorIndexPattern = ( getIndicatorIndicatorIndex().type(`${indicatorIndex}{enter}`); }; +export const fillEmailConnectorForm = (connector: EmailConnector = getEmailConnector()) => { + cy.get(CONNECTOR_NAME_INPUT).type(connector.name); + cy.get(EMAIL_CONNECTOR_FROM_INPUT).type(connector.from); + cy.get(EMAIL_CONNECTOR_HOST_INPUT).type(connector.host); + cy.get(EMAIL_CONNECTOR_PORT_INPUT).type(connector.port); + cy.get(EMAIL_CONNECTOR_USER_INPUT).type(connector.user); + cy.get(EMAIL_CONNECTOR_PASSWORD_INPUT).type(connector.password); +}; + /** Returns the indicator index drop down field. Pass in row number, default is 1 */ export const getIndicatorIndexComboField = (row = 1) => cy.get(THREAT_COMBO_BOX_INPUT).eq(row * 2 - 2); @@ -462,9 +478,12 @@ export const fillDefineMachineLearningRuleAndContinue = (rule: MachineLearningRu cy.get(MACHINE_LEARNING_DROPDOWN_INPUT).type(`${machineLearningJob}{enter}`); cy.get(MACHINE_LEARNING_DROPDOWN_INPUT).type('{esc}'); }); - cy.get(ANOMALY_THRESHOLD_INPUT).type(`{selectall}${machineLearningRule.anomalyScoreThreshold}`, { - force: true, - }); + cy.get(ANOMALY_THRESHOLD_INPUT).type( + `{selectall}${getMachineLearningRule().anomalyScoreThreshold}`, + { + force: true, + } + ); getDefineContinueButton().should('exist').click({ force: true }); cy.get(MACHINE_LEARNING_DROPDOWN_INPUT).should('not.exist'); diff --git a/x-pack/plugins/security_solution/public/app/app.tsx b/x-pack/plugins/security_solution/public/app/app.tsx index c223570c77201..0cba9341cbce1 100644 --- a/x-pack/plugins/security_solution/public/app/app.tsx +++ b/x-pack/plugins/security_solution/public/app/app.tsx @@ -24,7 +24,7 @@ import { State } from '../common/store'; import { StartServices } from '../types'; import { PageRouter } from './routes'; import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; -import { UserPrivilegesProvider } from '../detections/components/user_privileges'; +import { UserPrivilegesProvider } from '../common/components/user_privileges'; interface StartAppComponent { children: React.ReactNode; diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts index e1c14f2a86380..f5cec592c7abf 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -165,7 +165,7 @@ const nestedDeepLinks: SecurityDeepLinks = { navLinkStatus: AppNavLinkStatus.hidden, keywords: [ i18n.translate('xpack.securitySolution.search.exceptions', { - defaultMessage: 'Exception list', + defaultMessage: 'Exceptions', }), ], searchable: true, diff --git a/x-pack/plugins/security_solution/public/app/translations.ts b/x-pack/plugins/security_solution/public/app/translations.ts index 847a9114d94bd..027789713a2ae 100644 --- a/x-pack/plugins/security_solution/public/app/translations.ts +++ b/x-pack/plugins/security_solution/public/app/translations.ts @@ -24,7 +24,7 @@ export const RULES = i18n.translate('xpack.securitySolution.navigation.rules', { }); export const EXCEPTIONS = i18n.translate('xpack.securitySolution.navigation.exceptions', { - defaultMessage: 'Exception list', + defaultMessage: 'Exceptions', }); export const ALERTS = i18n.translate('xpack.securitySolution.navigation.alerts', { diff --git a/x-pack/plugins/security_solution/public/cases/components/__mock__/form.ts b/x-pack/plugins/security_solution/public/cases/components/__mock__/form.ts index 9ec356f70f9a4..3ba7aa616f1c1 100644 --- a/x-pack/plugins/security_solution/public/cases/components/__mock__/form.ts +++ b/x-pack/plugins/security_solution/public/cases/components/__mock__/form.ts @@ -25,6 +25,7 @@ export const mockFormHook = { setFieldErrors: jest.fn(), getFields: jest.fn(), getFormData: jest.fn(), + getFieldDefaultValue: jest.fn(), /* Returns a list of all errors in the form */ getErrors: jest.fn(), reset: jest.fn(), @@ -35,7 +36,6 @@ export const mockFormHook = { __validateFields: jest.fn(), __updateFormDataAt: jest.fn(), __readFieldConfigFromSchema: jest.fn(), - __getFieldDefaultValue: jest.fn(), }; // eslint-disable-next-line @typescript-eslint/no-explicit-any export const getFormMock = (sampleData: any) => ({ diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/agent_status.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/agent_status.tsx index f93721349fdac..d709105901215 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/agent_status.tsx +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/agent_status.tsx @@ -20,7 +20,7 @@ export const AgentStatus = React.memo(({ hostStatus }: { hostStatus: HostStatus > diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx index 3a1a29b63eadf..329b8e32f057d 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx @@ -213,7 +213,7 @@ const AlertSummaryViewComponent: React.FC<{ return ( <> - + { - const mount = useMountAppended(); - const mockTheme = getMockTheme({ - eui: { - euiBreakpoints: { - l: '1200px', - }, - paddingSizes: { - m: '8px', - xl: '32px', - }, - }, - }); - - test('renders correct items', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find('[data-test-subj="empty-threat-details-view"]').exists()).toEqual(true); - }); - - test('renders link to docs', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find('a').exists()).toEqual(true); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/empty_threat_details_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/empty_threat_details_view.tsx deleted file mode 100644 index d7e1c4d7754ec..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/empty_threat_details_view.tsx +++ /dev/null @@ -1,51 +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 { EuiLink, EuiSpacer, EuiTitle } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -import { useKibana } from '../../../lib/kibana'; -import * as i18n from './translations'; - -const EmptyThreatDetailsViewContainer = styled.div` - display: flex; - flex-direction: column; - align-items: center; -`; - -const Span = styled.span` - color: ${({ theme }) => theme.eui.euiColorDarkShade}; - line-height: 1.8em; - text-align: center; - padding: ${({ theme }) => `${theme.eui.paddingSizes.m} ${theme.eui.paddingSizes.xl}`}; -`; - -const EmptyThreatDetailsViewComponent: React.FC<{}> = () => { - const threatIntelDocsUrl = `${ - useKibana().services.docLinks.links.filebeat.base - }/filebeat-module-threatintel.html`; - - return ( - - - -

{i18n.NO_ENRICHMENT_FOUND}

-
- - {i18n.IF_CTI_NOT_ENABLED} - - {i18n.CHECK_DOCS} - - -
- ); -}; - -EmptyThreatDetailsViewComponent.displayName = 'EmptyThreatDetailsView'; - -export const EmptyThreatDetailsView = React.memo(EmptyThreatDetailsViewComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/no_enrichments_panel.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/no_enrichments_panel.test.tsx new file mode 100644 index 0000000000000..819c666bd7267 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/no_enrichments_panel.test.tsx @@ -0,0 +1,57 @@ +/* + * 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 { mount } from 'enzyme'; + +import { NoEnrichmentsPanel } from './no_enrichments_panel'; +import * as i18n from './translations'; + +jest.mock('../../../lib/kibana'); + +describe('NoEnrichmentsPanelView', () => { + it('renders a qualified container', () => { + const wrapper = mount( + + ); + expect(wrapper.find('[data-test-subj="no-enrichments-panel"]').exists()).toEqual(true); + }); + + it('renders nothing when all enrichments are present', () => { + const wrapper = mount( + + ); + expect(wrapper.find('[data-test-subj="no-enrichments-panel"]').exists()).toEqual(false); + }); + + it('renders expected text when no enrichments are present', () => { + const wrapper = mount( + + ); + expect(wrapper.find('[data-test-subj="no-enrichments-panel"]').hostNodes().text()).toContain( + i18n.NO_ENRICHMENTS_FOUND_TITLE + ); + }); + + it('renders expected text when existing enrichments are absent', () => { + const wrapper = mount( + + ); + expect(wrapper.find('[data-test-subj="no-enrichments-panel"]').hostNodes().text()).toContain( + i18n.NO_INDICATOR_ENRICHMENTS_TITLE + ); + }); + + it('renders expected text when investigation enrichments are absent', () => { + const wrapper = mount( + + ); + expect(wrapper.find('[data-test-subj="no-enrichments-panel"]').hostNodes().text()).toContain( + i18n.NO_INVESTIGATION_ENRICHMENTS_TITLE + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/no_enrichments_panel.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/no_enrichments_panel.tsx new file mode 100644 index 0000000000000..b521c3ba92c4d --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/no_enrichments_panel.tsx @@ -0,0 +1,88 @@ +/* + * 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 { EuiHorizontalRule, EuiLink, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; +import { useKibana } from '../../../lib/kibana'; + +import * as i18n from './translations'; + +const Container = styled(EuiPanel)` + display: flex; + flex-direction: column; +`; + +const NoEnrichmentsPanelView: React.FC<{ + title: React.ReactNode; + description: React.ReactNode; +}> = ({ title, description }) => { + return ( + + {title} + + + {description} + + + ); +}; + +NoEnrichmentsPanelView.displayName = 'NoEnrichmentsPanelView'; + +export const NoEnrichmentsPanel: React.FC<{ + existingEnrichmentsCount: number; + investigationEnrichmentsCount: number; +}> = ({ existingEnrichmentsCount, investigationEnrichmentsCount }) => { + const threatIntelDocsUrl = `${ + useKibana().services.docLinks.links.filebeat.base + }/filebeat-module-threatintel.html`; + const noIndicatorEnrichmentsDescription = ( + <> + {i18n.IF_CTI_NOT_ENABLED} + + {i18n.CHECK_DOCS} + + + ); + + if (existingEnrichmentsCount === 0 && investigationEnrichmentsCount === 0) { + return ( + {i18n.NO_ENRICHMENTS_FOUND_TITLE}} + description={ + <> +

{noIndicatorEnrichmentsDescription}

+

{i18n.NO_INVESTIGATION_ENRICHMENTS_DESCRIPTION}

+ + } + /> + ); + } else if (existingEnrichmentsCount === 0) { + return ( + <> + + {i18n.NO_INDICATOR_ENRICHMENTS_TITLE}} + description={noIndicatorEnrichmentsDescription} + /> + + ); + } else if (investigationEnrichmentsCount === 0) { + return ( + <> + + {i18n.NO_INVESTIGATION_ENRICHMENTS_TITLE}} + description={i18n.NO_INVESTIGATION_ENRICHMENTS_DESCRIPTION} + /> + + ); + } else { + return null; + } +}; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.test.tsx index 0113dde96a4b6..c25457a5e5e88 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.test.tsx @@ -31,15 +31,6 @@ describe('ThreatDetailsView', () => { ); }); - it('renders an empty view if there are no enrichments', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find('[data-test-subj="empty-threat-details-view"]').exists()).toEqual(true); - }); - it('renders anchor links for event.url and event.reference', () => { const enrichments = [ buildEventEnrichmentMock({ diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx index d5e985c5757a6..b6b8a47c1dd8c 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx @@ -20,7 +20,6 @@ import React, { Fragment } from 'react'; import { StyledEuiInMemoryTable } from '../summary_view'; import { getSummaryColumns, SummaryRow, ThreatDetailsRow } from '../helpers'; -import { EmptyThreatDetailsView } from './empty_threat_details_view'; import { FIRSTSEEN, EVENT_URL, EVENT_REFERENCE } from '../../../../../common/cti/constants'; import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../../../common/constants'; import { getFirstElement } from '../../../../../common/utils/data_retrieval'; @@ -70,15 +69,13 @@ const ThreatDetailsHeader: React.FC<{ + {isInvestigationTimeEnrichment(type) && ( + + + + )} - {isInvestigationTimeEnrichment(type) && ( - - - - - - )} ); @@ -131,10 +128,6 @@ const buildThreatDetailsItems = (enrichment: CtiEnrichment) => const ThreatDetailsViewComponent: React.FC<{ enrichments: CtiEnrichment[]; }> = ({ enrichments }) => { - if (enrichments.length < 1) { - return ; - } - const sortedEnrichments = enrichments.sort((a, b) => getFirstSeen(b) - getFirstSeen(a)); return ( @@ -146,6 +139,7 @@ const ThreatDetailsViewComponent: React.FC<{ return ( + { ).toEqual('Summary'); }); }); + + describe('threat intel tab', () => { + it('renders a "no enrichments" panel view if there are no enrichments', () => { + alertsWrapper.find('[data-test-subj="threatIntelTab"]').first().simulate('click'); + expect(alertsWrapper.find('[data-test-subj="no-enrichments-panel"]').exists()).toEqual(true); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index 9afaaef61b17a..7074212dcdb4c 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -9,9 +9,6 @@ import { EuiTabbedContent, EuiTabbedContentTab, EuiSpacer, - EuiButton, - EuiFlexGroup, - EuiFlexItem, EuiLoadingContent, EuiLoadingSpinner, } from '@elastic/eui'; @@ -34,6 +31,7 @@ import { parseExistingEnrichments, timelineDataToEnrichment, } from './cti_details/helpers'; +import { NoEnrichmentsPanel } from './cti_details/no_enrichments_panel'; type EventViewTab = EuiTabbedContentTab; @@ -100,9 +98,6 @@ const EventDetailsComponent: React.FC = ({ (tab: EuiTabbedContentTab) => setSelectedTabId(tab.id as EventViewId), [setSelectedTabId] ); - const viewThreatIntelTab = useCallback(() => setSelectedTabId(EventsViewType.threatIntelView), [ - setSelectedTabId, - ]); const eventFields = useMemo(() => getEnrichmentFields(data), [data]); const existingEnrichments = useMemo( @@ -118,6 +113,9 @@ const EventDetailsComponent: React.FC = ({ loading: enrichmentsLoading, result: enrichmentsResponse, } = useInvestigationTimeEnrichment(eventFields); + const investigationEnrichments = useMemo(() => enrichmentsResponse?.enrichments ?? [], [ + enrichmentsResponse?.enrichments, + ]); const allEnrichments = useMemo(() => { if (enrichmentsLoading || !enrichmentsResponse?.enrichments) { return existingEnrichments; @@ -140,29 +138,20 @@ const EventDetailsComponent: React.FC = ({ eventId: id, browserFields, timelineId, - title: i18n.ALERT_SUMMARY, }} /> + {enrichmentCount > 0 && ( + + )} {enrichmentsLoading && ( <> )} - {enrichmentCount > 0 && ( - <> - - - - - {i18n.VIEW_CTI_DATA} - - - - )} ), } @@ -176,7 +165,6 @@ const EventDetailsComponent: React.FC = ({ enrichmentsLoading, enrichmentCount, allEnrichments, - viewThreatIntelTab, ] ); @@ -192,10 +180,25 @@ const EventDetailsComponent: React.FC = ({ {enrichmentsLoading ? : `(${enrichmentCount})`} ), - content: , + content: ( + <> + + + + ), } : undefined, - [allEnrichments, enrichmentCount, enrichmentsLoading, isAlert] + [ + allEnrichments, + enrichmentCount, + enrichmentsLoading, + existingEnrichments.length, + investigationEnrichments.length, + isAlert, + ] ); const tableTab = useMemo( diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx index 0e846f3f6f699..961860ed6d8b9 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx @@ -13,12 +13,13 @@ import { SummaryRow } from './helpers'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)` - .euiTableHeaderCell { - border: none; - } + .euiTableHeaderCell, .euiTableRowCell { border: none; } + .euiTableHeaderCell .euiTableCellContent { + padding: 0; + } `; const StyledEuiTitle = styled(EuiTitle)` diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts index a17ca5e434ace..c632f5d6332e0 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts @@ -11,22 +11,10 @@ export const SUMMARY = i18n.translate('xpack.securitySolution.alertDetails.summa defaultMessage: 'Summary', }); -export const ALERT_SUMMARY = i18n.translate('xpack.securitySolution.alertDetails.alertSummary', { - defaultMessage: 'Alert Summary', -}); - export const THREAT_INTEL = i18n.translate('xpack.securitySolution.alertDetails.threatIntel', { defaultMessage: 'Threat Intel', }); -export const THREAT_SUMMARY = i18n.translate('xpack.securitySolution.alertDetails.threatSummary', { - defaultMessage: 'Threat Summary', -}); - -export const VIEW_CTI_DATA = i18n.translate('xpack.securitySolution.alertDetails.threatIntelCta', { - defaultMessage: 'View threat intel data', -}); - export const INVESTIGATION_GUIDE = i18n.translate( 'xpack.securitySolution.alertDetails.summary.investigationGuide', { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_endpoint_fields.json b/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_endpoint_fields.json index b5480aac27f67..c37be60545ab2 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_endpoint_fields.json +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_endpoint_fields.json @@ -2,10 +2,12 @@ "Endpoint.policy.applied.id", "Target.process.Ext.services", "Target.process.Ext.user", + "Target.process.executable", "Target.process.hash.md5", "Target.process.hash.sha1", "Target.process.hash.sha256", "Target.process.hash.sha512", + "Target.process.name", "Target.process.parent.hash.md5", "Target.process.parent.hash.sha1", "Target.process.parent.hash.sha256", @@ -17,6 +19,14 @@ "Target.process.pe.original_file_name", "Target.process.pe.product", "Target.process.pgid", + "Target.process.thread.Ext.start_address_details.allocation_type", + "Target.process.thread.Ext.start_address_bytes_disasm_hash", + "Target.process.thread.Ext.start_address_allocation_offset", + "Target.process.thread.Ext.start_address_details.allocation_size", + "Target.process.thread.Ext.start_address_details.region_size", + "Target.process.thread.Ext.start_address_details.region_protection", + "Target.process.thread.Ext.start_address_details.memory_pe.imphash", + "Target.process.thread.Ext.start_address_bytes", "agent.id", "agent.type", "agent.version", @@ -68,10 +78,13 @@ "host.type", "process.Ext.services", "process.Ext.user", + "process.Ext.code_signature", + "process.executable", "process.hash.md5", "process.hash.sha1", "process.hash.sha256", "process.hash.sha512", + "process.name", "process.parent.hash.md5", "process.parent.hash.sha1", "process.parent.hash.sha256", @@ -88,5 +101,7 @@ "user.email", "user.hash", "user.id", - "Ransomware.feature" + "Ransomware.feature", + "Memory_protection.feature", + "Memory_protection.self_injection" ] diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 0af83e2cff3b5..32eb4baad5059 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -928,5 +928,172 @@ describe('Exception helpers', () => { }, ]); }); + + test('it should return pre-populated memory signature items for event code `memory_signature`', () => { + const defaultItems = defaultEndpointExceptionItems('list_id', 'my_rule', { + _id: '123', + process: { + name: 'some name', + executable: 'some file path', + hash: { + sha256: 'some hash', + }, + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + Memory_protection: { + feature: 'signature', + }, + event: { + code: 'memory_signature', + }, + }); + + expect(defaultItems[0].entries).toEqual([ + { + field: 'Memory_protection.feature', + operator: 'included', + type: 'match', + value: 'signature', + id: '123', + }, + { + field: 'process.executable.caseless', + operator: 'included', + type: 'match', + value: 'some file path', + id: '123', + }, + { + field: 'process.name.caseless', + operator: 'included', + type: 'match', + value: 'some name', + id: '123', + }, + { + field: 'process.hash.sha256', + operator: 'included', + type: 'match', + value: 'some hash', + id: '123', + }, + ]); + }); + + test('it should return pre-populated memory shellcode items for event code `malicious_thread`', () => { + const defaultItems = defaultEndpointExceptionItems('list_id', 'my_rule', { + _id: '123', + process: { + name: 'some name', + executable: 'some file path', + Ext: { + token: { + integrity_level_name: 'high', + }, + }, + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + Memory_protection: { + feature: 'shellcode_thread', + self_injection: true, + }, + event: { + code: 'malicious_thread', + }, + Target: { + process: { + thread: { + Ext: { + start_address_allocation_offset: 0, + start_address_bytes_disasm_hash: 'a disam hash', + start_address_details: { + allocation_type: 'PRIVATE', + allocation_size: 4000, + region_size: 4000, + region_protection: 'RWX', + memory_pe: { + imphash: 'a hash', + }, + }, + }, + }, + }, + }, + }); + + expect(defaultItems[0].entries).toEqual([ + { + field: 'Memory_protection.feature', + operator: 'included', + type: 'match', + value: 'shellcode_thread', + id: '123', + }, + { + field: 'Memory_protection.self_injection', + operator: 'included', + type: 'match', + value: 'true', + id: '123', + }, + { + field: 'process.executable.caseless', + operator: 'included', + type: 'match', + value: 'some file path', + id: '123', + }, + { + field: 'process.name.caseless', + operator: 'included', + type: 'match', + value: 'some name', + id: '123', + }, + { + field: 'process.Ext.token.integrity_level_name', + operator: 'included', + type: 'match', + value: 'high', + id: '123', + }, + { + field: 'Target.process.thread.Ext.start_address_details', + type: 'nested', + entries: [ + { + field: 'allocation_type', + operator: 'included', + type: 'match', + value: 'PRIVATE', + id: '123', + }, + { + field: 'allocation_size', + operator: 'included', + type: 'match', + value: '4000', + id: '123', + }, + { field: 'region_size', operator: 'included', type: 'match', value: '4000', id: '123' }, + { + field: 'region_protection', + operator: 'included', + type: 'match', + value: 'RWX', + id: '123', + }, + { + field: 'memory_pe.imphash', + operator: 'included', + type: 'match', + value: 'a hash', + id: '123', + }, + ], + id: '123', + }, + ]); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index bfb5c7298f330..3c8652637a997 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -496,6 +496,139 @@ export const getPrepopulatedRansomwareException = ({ }; }; +export const getPrepopulatedMemorySignatureException = ({ + listId, + ruleName, + eventCode, + listNamespace = 'agnostic', + alertEcsData, +}: { + listId: string; + listNamespace?: NamespaceType; + ruleName: string; + eventCode: string; + alertEcsData: Flattened; +}): ExceptionsBuilderExceptionItem => { + const { process } = alertEcsData; + return { + ...getNewExceptionItem({ listId, namespaceType: listNamespace, ruleName }), + entries: addIdToEntries([ + { + field: 'Memory_protection.feature', + operator: 'included', + type: 'match', + value: alertEcsData.Memory_protection?.feature ?? '', + }, + { + field: 'process.executable.caseless', + operator: 'included', + type: 'match', + value: process?.executable ?? '', + }, + { + field: 'process.name.caseless', + operator: 'included', + type: 'match', + value: process?.name ?? '', + }, + { + field: 'process.hash.sha256', + operator: 'included', + type: 'match', + value: process?.hash?.sha256 ?? '', + }, + ]), + }; +}; +export const getPrepopulatedMemoryShellcodeException = ({ + listId, + ruleName, + eventCode, + listNamespace = 'agnostic', + alertEcsData, +}: { + listId: string; + listNamespace?: NamespaceType; + ruleName: string; + eventCode: string; + alertEcsData: Flattened; +}): ExceptionsBuilderExceptionItem => { + const { process, Target } = alertEcsData; + return { + ...getNewExceptionItem({ listId, namespaceType: listNamespace, ruleName }), + entries: addIdToEntries([ + { + field: 'Memory_protection.feature', + operator: 'included', + type: 'match', + value: alertEcsData.Memory_protection?.feature ?? '', + }, + { + field: 'Memory_protection.self_injection', + operator: 'included', + type: 'match', + value: String(alertEcsData.Memory_protection?.self_injection) ?? '', + }, + { + field: 'process.executable.caseless', + operator: 'included', + type: 'match', + value: process?.executable ?? '', + }, + { + field: 'process.name.caseless', + operator: 'included', + type: 'match', + value: process?.name ?? '', + }, + { + field: 'process.Ext.token.integrity_level_name', + operator: 'included', + type: 'match', + value: process?.Ext?.token?.integrity_level_name ?? '', + }, + { + field: 'Target.process.thread.Ext.start_address_details', + type: 'nested', + entries: [ + { + field: 'allocation_type', + operator: 'included', + type: 'match', + value: Target?.process?.thread?.Ext?.start_address_details?.allocation_type ?? '', + }, + { + field: 'allocation_size', + operator: 'included', + type: 'match', + value: + String(Target?.process?.thread?.Ext?.start_address_details?.allocation_size) ?? '', + }, + { + field: 'region_size', + operator: 'included', + type: 'match', + value: String(Target?.process?.thread?.Ext?.start_address_details?.region_size) ?? '', + }, + { + field: 'region_protection', + operator: 'included', + type: 'match', + value: + String(Target?.process?.thread?.Ext?.start_address_details?.region_protection) ?? '', + }, + { + field: 'memory_pe.imphash', + operator: 'included', + type: 'match', + value: + String(Target?.process?.thread?.Ext?.start_address_details?.memory_pe?.imphash) ?? '', + }, + ], + }, + ]), + }; +}; /** * Determines whether or not any entries within the given exceptionItems contain values not in the specified ECS mapping */ @@ -537,26 +670,45 @@ export const defaultEndpointExceptionItems = ( const { event: alertEvent } = alertEcsData; const eventCode = alertEvent?.code ?? ''; - if (eventCode === 'ransomware') { - return getProcessCodeSignature(alertEcsData).map((codeSignature) => - getPrepopulatedRansomwareException({ - listId, - ruleName, - eventCode, - codeSignature, - alertEcsData, - }) - ); + switch (eventCode) { + case 'memory_signature': + return [ + getPrepopulatedMemorySignatureException({ + listId, + ruleName, + eventCode, + alertEcsData, + }), + ]; + case 'malicious_thread': + return [ + getPrepopulatedMemoryShellcodeException({ + listId, + ruleName, + eventCode, + alertEcsData, + }), + ]; + case 'ransomware': + return getProcessCodeSignature(alertEcsData).map((codeSignature) => + getPrepopulatedRansomwareException({ + listId, + ruleName, + eventCode, + codeSignature, + alertEcsData, + }) + ); + default: + // By default return the standard prepopulated Endpoint Exception fields + return getFileCodeSignature(alertEcsData).map((codeSignature) => + getPrepopulatedEndpointException({ + listId, + ruleName, + eventCode, + codeSignature, + alertEcsData, + }) + ); } - - // By default return the standard prepopulated Endpoint Exception fields - return getFileCodeSignature(alertEcsData).map((codeSignature) => - getPrepopulatedEndpointException({ - listId, - ruleName, - eventCode, - codeSignature, - alertEcsData, - }) - ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap index 22805d34d2ee1..410fb7f3ae793 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap @@ -10,6 +10,7 @@ exports[`GroupsFilterPopover renders correctly against snapshot 1`] = ` iconType="arrowDown" isSelected={false} numActiveFilters={0} + numFilters={3} onClick={[Function]} > Groups diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx index b7425a62f6773..249dc0dfccdbb 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx @@ -59,6 +59,7 @@ export const GroupsFilterPopoverComponent = ({ iconType="arrowDown" onClick={() => setIsGroupPopoverOpen(!isGroupPopoverOpen)} isSelected={isGroupPopoverOpen} + numFilters={uniqueGroups.length} hasActiveFilters={selectedGroups.length > 0} numActiveFilters={selectedGroups.length} > diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx index e3549aa6ec047..af88aacb7602a 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx @@ -141,7 +141,7 @@ describe('useSecuritySolutionNavigation', () => { "href": "securitySolution/exceptions?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "id": "exceptions", "isSelected": false, - "name": "Exception list", + "name": "Exceptions", "onClick": [Function], }, ], diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/use_endpoint_privileges.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/use_endpoint_privileges.ts new file mode 100644 index 0000000000000..80cf11fecd847 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/use_endpoint_privileges.ts @@ -0,0 +1,17 @@ +/* + * 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 { EndpointPrivileges } from '../use_endpoint_privileges'; + +export const useEndpointPrivileges = jest.fn(() => { + const endpointPrivilegesMock: EndpointPrivileges = { + loading: false, + canAccessFleet: true, + canAccessEndpointManagement: true, + }; + return endpointPrivilegesMock; +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/user_privileges/index.tsx b/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx similarity index 59% rename from x-pack/plugins/security_solution/public/detections/components/user_privileges/index.tsx rename to x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx index bd6ff11b27f96..5a33297f04f9a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/user_privileges/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx @@ -6,19 +6,25 @@ */ import React, { createContext, useContext } from 'react'; -import { useFetchDetectionEnginePrivileges } from './use_fetch_detection_engine_privileges'; -import { useFetchListPrivileges } from './use_fetch_list_privileges'; +import { DeepReadonly } from 'utility-types'; +import { useFetchDetectionEnginePrivileges } from '../../../detections/components/user_privileges/use_fetch_detection_engine_privileges'; +import { useFetchListPrivileges } from '../../../detections/components/user_privileges/use_fetch_list_privileges'; +import { EndpointPrivileges, useEndpointPrivileges } from './use_endpoint_privileges'; export interface UserPrivilegesState { listPrivileges: ReturnType; detectionEnginePrivileges: ReturnType; + endpointPrivileges: EndpointPrivileges; } -const UserPrivilegesContext = createContext({ +export const initialUserPrivilegesState = (): UserPrivilegesState => ({ listPrivileges: { loading: false, error: undefined, result: undefined }, detectionEnginePrivileges: { loading: false, error: undefined, result: undefined }, + endpointPrivileges: { loading: true, canAccessEndpointManagement: false, canAccessFleet: false }, }); +const UserPrivilegesContext = createContext(initialUserPrivilegesState()); + interface UserPrivilegesProviderProps { children: React.ReactNode; } @@ -26,12 +32,14 @@ interface UserPrivilegesProviderProps { export const UserPrivilegesProvider = ({ children }: UserPrivilegesProviderProps) => { const listPrivileges = useFetchListPrivileges(); const detectionEnginePrivileges = useFetchDetectionEnginePrivileges(); + const endpointPrivileges = useEndpointPrivileges(); return ( {children} @@ -39,4 +47,5 @@ export const UserPrivilegesProvider = ({ children }: UserPrivilegesProviderProps ); }; -export const useUserPrivileges = () => useContext(UserPrivilegesContext); +export const useUserPrivileges = (): DeepReadonly => + useContext(UserPrivilegesContext); diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.test.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.test.ts new file mode 100644 index 0000000000000..8e9dae9f12ad5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.test.ts @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook, RenderHookResult, RenderResult } from '@testing-library/react-hooks'; +import { useHttp, useCurrentUser } from '../../lib/kibana'; +import { EndpointPrivileges, useEndpointPrivileges } from './use_endpoint_privileges'; +import { fleetGetCheckPermissionsHttpMock } from '../../../management/pages/endpoint_hosts/mocks'; +import { securityMock } from '../../../../../security/public/mocks'; +import { appRoutesService } from '../../../../../fleet/common'; +import { AuthenticatedUser } from '../../../../../security/common'; + +jest.mock('../../lib/kibana'); + +describe('When using useEndpointPrivileges hook', () => { + let authenticatedUser: AuthenticatedUser; + let fleetApiMock: ReturnType; + let result: RenderResult; + let unmount: ReturnType['unmount']; + let waitForNextUpdate: ReturnType['waitForNextUpdate']; + let render: () => RenderHookResult; + + beforeEach(() => { + authenticatedUser = securityMock.createMockAuthenticatedUser({ + roles: ['superuser'], + }); + + (useCurrentUser as jest.Mock).mockReturnValue(authenticatedUser); + + fleetApiMock = fleetGetCheckPermissionsHttpMock( + useHttp() as Parameters[0] + ); + + render = () => { + const hookRenderResponse = renderHook(() => useEndpointPrivileges()); + ({ result, unmount, waitForNextUpdate } = hookRenderResponse); + return hookRenderResponse; + }; + }); + + afterEach(() => { + unmount(); + }); + + it('should return `loading: true` while retrieving privileges', async () => { + // Add a daly to the API response that we can control from the test + let releaseApiResponse: () => void; + fleetApiMock.responseProvider.checkPermissions.mockDelay.mockReturnValue( + new Promise((resolve) => { + releaseApiResponse = () => resolve(); + }) + ); + (useCurrentUser as jest.Mock).mockReturnValue(null); + + const { rerender } = render(); + expect(result.current).toEqual({ + canAccessEndpointManagement: false, + canAccessFleet: false, + loading: true, + }); + + // Make user service available + (useCurrentUser as jest.Mock).mockReturnValue(authenticatedUser); + rerender(); + expect(result.current).toEqual({ + canAccessEndpointManagement: false, + canAccessFleet: false, + loading: true, + }); + + // Release the API response + releaseApiResponse!(); + await fleetApiMock.waitForApi(); + expect(result.current).toEqual({ + canAccessEndpointManagement: true, + canAccessFleet: true, + loading: false, + }); + }); + + it('should call Fleet permissions api to determine user privilege to fleet', async () => { + render(); + await waitForNextUpdate(); + await fleetApiMock.waitForApi(); + expect(useHttp().get as jest.Mock).toHaveBeenCalledWith( + appRoutesService.getCheckPermissionsPath() + ); + }); + + it('should set privileges to false if user does not have superuser role', async () => { + authenticatedUser.roles = []; + render(); + await waitForNextUpdate(); + await fleetApiMock.waitForApi(); + expect(result.current).toEqual({ + canAccessEndpointManagement: false, + canAccessFleet: true, // this is only true here because I did not adjust the API mock + loading: false, + }); + }); + + it('should set privileges to false if fleet api check returns failure', async () => { + fleetApiMock.responseProvider.checkPermissions.mockReturnValue({ + error: 'MISSING_SECURITY', + success: false, + }); + + render(); + await waitForNextUpdate(); + await fleetApiMock.waitForApi(); + expect(result.current).toEqual({ + canAccessEndpointManagement: false, + canAccessFleet: false, + loading: false, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.ts new file mode 100644 index 0000000000000..b8db0c5c0fbc9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useMemo, useRef, useState } from 'react'; +import { useCurrentUser, useHttp } from '../../lib/kibana'; +import { appRoutesService, CheckPermissionsResponse } from '../../../../../fleet/common'; + +export interface EndpointPrivileges { + loading: boolean; + /** If user has permissions to access Fleet */ + canAccessFleet: boolean; + /** If user has permissions to access Endpoint management (includes check to ensure they also have access to fleet) */ + canAccessEndpointManagement: boolean; +} + +/** + * Retrieve the endpoint privileges for the current user. + * + * **NOTE:** Consider using `usePrivileges().endpointPrivileges` instead of this hook in order + * to keep API calls to a minimum. + */ +export const useEndpointPrivileges = (): EndpointPrivileges => { + const http = useHttp(); + const user = useCurrentUser(); + const isMounted = useRef(true); + const [canAccessFleet, setCanAccessFleet] = useState(false); + const [fleetCheckDone, setFleetCheckDone] = useState(false); + + // Check if user can access fleet + useEffect(() => { + (async () => { + try { + const fleetPermissionsResponse = await http.get( + appRoutesService.getCheckPermissionsPath() + ); + + if (isMounted.current) { + setCanAccessFleet(fleetPermissionsResponse.success); + } + } finally { + if (isMounted.current) { + setFleetCheckDone(true); + } + } + })(); + }, [http]); + + // Check if user has `superuser` role + const isSuperUser = useMemo(() => { + if (user?.roles) { + return user.roles.includes('superuser'); + } + return false; + }, [user?.roles]); + + const privileges = useMemo(() => { + return { + loading: !fleetCheckDone || !user, + canAccessFleet, + canAccessEndpointManagement: canAccessFleet && isSuperUser, + }; + }, [canAccessFleet, fleetCheckDone, isSuperUser, user]); + + // Capture if component is unmounted + useEffect( + () => () => { + isMounted.current = false; + }, + [] + ); + + return privileges; +}; diff --git a/x-pack/plugins/security_solution/public/common/hooks/endpoint/ingest_enabled.ts b/x-pack/plugins/security_solution/public/common/hooks/endpoint/ingest_enabled.ts deleted file mode 100644 index 18582e7064a7b..0000000000000 --- a/x-pack/plugins/security_solution/public/common/hooks/endpoint/ingest_enabled.ts +++ /dev/null @@ -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 { ApplicationStart } from 'src/core/public'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -/** - * Returns an object which fleet permissions are allowed - */ -export const useIngestEnabledCheck = (): { - allEnabled: boolean; - show: boolean; - write: boolean; - read: boolean; -} => { - const { services } = useKibana<{ application: ApplicationStart }>(); - - // Check if Fleet is present in the configuration - const show = Boolean(services.application.capabilities.fleet?.show); - const write = Boolean(services.application.capabilities.fleet?.write); - const read = Boolean(services.application.capabilities.fleet?.read); - - // Check if all Fleet permissions are enabled - const allEnabled = show && read && write ? true : false; - - return { - allEnabled, - show, - write, - read, - }; -}; diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_security_packages.ts b/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_security_packages.ts index 6a3afccd8794d..ef1e658d349bf 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_security_packages.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_security_packages.ts @@ -8,14 +8,9 @@ import { useEffect } from 'react'; import { HttpFetchOptions, HttpStart } from 'kibana/public'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { - epmRouteService, - appRoutesService, - CheckPermissionsResponse, - BulkInstallPackagesResponse, -} from '../../../../fleet/common'; +import { epmRouteService, BulkInstallPackagesResponse } from '../../../../fleet/common'; import { StartServices } from '../../types'; -import { useIngestEnabledCheck } from './endpoint/ingest_enabled'; +import { useUserPrivileges } from '../components/user_privileges'; /** * Requests that the endpoint and security_detection_engine package be upgraded to the latest version @@ -35,25 +30,9 @@ const sendUpgradeSecurityPackages = async ( }); }; -/** - * Checks with the ingest manager if the current user making these requests has the right permissions - * to install the endpoint package. - * - * @param http an http client for sending the request - * @param options an object containing options for the request - */ -const sendCheckPermissions = async ( - http: HttpStart, - options: HttpFetchOptions = {} -): Promise => { - return http.get(appRoutesService.getCheckPermissionsPath(), { - ...options, - }); -}; - export const useUpgradeSecurityPackages = () => { const context = useKibana(); - const { allEnabled: ingestEnabled } = useIngestEnabledCheck(); + const canAccessFleet = useUserPrivileges().endpointPrivileges.canAccessFleet; useEffect(() => { const abortController = new AbortController(); @@ -63,21 +42,11 @@ export const useUpgradeSecurityPackages = () => { abortController.abort(); }; - if (ingestEnabled) { + if (canAccessFleet) { const signal = abortController.signal; (async () => { try { - // make sure we're a privileged user before trying to install the package - const { success: hasPermissions } = await sendCheckPermissions(context.services.http, { - signal, - }); - - // if we're not a privileged user then return and don't try to check the status of the endpoint package - if (!hasPermissions) { - return abortRequests; - } - // ignore the response for now since we aren't notifying the user await sendUpgradeSecurityPackages(context.services.http, { signal }); } catch (error) { @@ -93,5 +62,5 @@ export const useUpgradeSecurityPackages = () => { return abortRequests; })(); } - }, [ingestEnabled, context.services.http]); + }, [canAccessFleet, context.services.http]); }; diff --git a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx index 9ac7ae0f24322..d0755d05bdb5f 100644 --- a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx @@ -24,7 +24,7 @@ import { import { FieldHook } from '../../shared_imports'; import { SUB_PLUGINS_REDUCER } from './utils'; import { createSecuritySolutionStorageMock, localStorageMock } from './mock_local_storage'; -import { UserPrivilegesProvider } from '../../detections/components/user_privileges'; +import { UserPrivilegesProvider } from '../components/user_privileges'; const state: State = mockGlobalState; diff --git a/x-pack/plugins/security_solution/public/common/utils/shorten_count_into_string.test.ts b/x-pack/plugins/security_solution/public/common/utils/shorten_count_into_string.test.ts new file mode 100644 index 0000000000000..13699f5dc3060 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/utils/shorten_count_into_string.test.ts @@ -0,0 +1,38 @@ +/* + * 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 { shortenCountIntoString } from './shorten_count_into_string'; + +describe('utils', () => { + describe('shortenCountIntoString', () => { + it('should not change small numbers', () => { + expect(shortenCountIntoString(0)).toBe('0'); + expect(shortenCountIntoString(9999)).toBe('9999'); + }); + + it('should add K when appropriate', () => { + expect(shortenCountIntoString(10000)).toBe('10K'); + expect(shortenCountIntoString(109000)).toBe('109K'); + expect(shortenCountIntoString(109800)).toBe('109.8K'); + expect(shortenCountIntoString(109897)).toBe('109.8K'); + }); + + it('should add M when appropriate', () => { + expect(shortenCountIntoString(10000000)).toBe('10M'); + expect(shortenCountIntoString(109000000)).toBe('109M'); + expect(shortenCountIntoString(109800000)).toBe('109.8M'); + expect(shortenCountIntoString(109890000)).toBe('109.8M'); + }); + + it('should add B when appropriate', () => { + expect(shortenCountIntoString(10000000000)).toBe('10B'); + expect(shortenCountIntoString(109000000000)).toBe('109B'); + expect(shortenCountIntoString(109800000000)).toBe('109.8B'); + expect(shortenCountIntoString(109890000000)).toBe('109.8B'); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/utils/shorten_count_into_string.ts b/x-pack/plugins/security_solution/public/common/utils/shorten_count_into_string.ts new file mode 100644 index 0000000000000..63d2c8f597911 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/utils/shorten_count_into_string.ts @@ -0,0 +1,35 @@ +/* + * 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 shortenCountIntoString = (count: number): string => { + if (count < 10000) { + return count.toString(); + } + const abbreviations = [ + { magnitude: 1e18, unit: 'E' }, + { magnitude: 1e15, unit: 'P' }, + { magnitude: 1e12, unit: 'T' }, + { magnitude: 1e9, unit: 'B' }, + { magnitude: 1e6, unit: 'M' }, + { magnitude: 1e3, unit: 'K' }, + ]; + const { magnitude, unit } = abbreviations.find( + (abbreviation) => count >= abbreviation.magnitude + ) ?? { + magnitude: 1, + unit: '', + }; + + return ( + toFixedWithoutRounding(count / magnitude, 1).replace(/\.0+$|(\.[0-9]*[1-9])0+$/, '$1') + unit + ); +}; + +const toFixedWithoutRounding = (n: number, p: number) => { + const result = n.toFixed(p); + return +result <= n ? result : (+result - Math.pow(0.1, p)).toFixed(p); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/use_missing_privileges.ts b/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/use_missing_privileges.ts index dd139421e9ddd..73aa922251ee6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/use_missing_privileges.ts +++ b/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/use_missing_privileges.ts @@ -9,7 +9,7 @@ import { useMemo } from 'react'; import { SAVED_OBJECTS_MANAGEMENT_FEATURE_ID } from '../../../../../common/constants'; import { Privilege } from '../../../containers/detection_engine/alerts/types'; import { useUserData } from '../../user_info'; -import { useUserPrivileges } from '../../user_privileges'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; const REQUIRED_INDEX_PRIVILIGES = ['read', 'write', 'view_index_metadata', 'maintenance'] as const; diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/take_action_dropdown.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/take_action_dropdown.tsx index a10ad901441ea..1404f7927d6ec 100644 --- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/take_action_dropdown.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/take_action_dropdown.tsx @@ -10,6 +10,7 @@ import { EuiContextMenuItem, EuiContextMenuPanel, EuiButton, EuiPopover } from ' import { ISOLATE_HOST, UNISOLATE_HOST } from './translations'; import { TAKE_ACTION } from '../alerts_table/alerts_utility_bar/translations'; import { useHostIsolationStatus } from '../../containers/detection_engine/alerts/use_host_isolation_status'; +import { HostStatus } from '../../../../common/endpoint/types'; export const TakeActionDropdown = React.memo( ({ @@ -19,7 +20,9 @@ export const TakeActionDropdown = React.memo( onChange: (action: 'isolateHost' | 'unisolateHost') => void; agentId: string; }) => { - const { loading, isIsolated: isolationStatus } = useHostIsolationStatus({ agentId }); + const { loading, isIsolated: isolationStatus, agentStatus } = useHostIsolationStatus({ + agentId, + }); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const closePopoverHandler = useCallback(() => { @@ -41,7 +44,7 @@ export const TakeActionDropdown = React.memo( iconSide="right" fill iconType="arrowDown" - disabled={loading} + disabled={loading || agentStatus === HostStatus.UNENROLLED} onClick={() => { setIsPopoverOpen(!isPopoverOpen); }} @@ -49,7 +52,7 @@ export const TakeActionDropdown = React.memo( {TAKE_ACTION} ); - }, [isPopoverOpen, loading]); + }, [isPopoverOpen, loading, agentStatus]); return ( = ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any (key: string, value: any, index: number) => { const updatedActions = [...actions]; - updatedActions[index].params[key] = value; + updatedActions[index] = { + ...updatedActions[index], + params: { + ...updatedActions[index].params, + [key]: value, + }, + }; field.setValue(updatedActions); }, // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx index 3467b34d47135..1dd59d49e4ff5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx @@ -13,6 +13,7 @@ import { FormSchema, ValidationFunc, ERROR_CODE, + VALIDATION_TYPES, } from '../../../../shared_imports'; import { AboutStepRule } from '../../../pages/detection_engine/rules/types'; import { OptionalFieldLabel } from '../optional_field_label'; @@ -38,6 +39,20 @@ export const schema: FormSchema = { } ), labelAppend: OptionalFieldLabel, + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.authorFieldEmptyError', + { + defaultMessage: 'An author must not be empty', + } + ) + ), + type: VALIDATION_TYPES.ARRAY_ITEM, + isBlocking: false, + }, + ], }, name: { type: FIELD_TYPES.TEXT, @@ -243,6 +258,20 @@ export const schema: FormSchema = { } ), labelAppend: OptionalFieldLabel, + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.tagFieldEmptyError', + { + defaultMessage: 'A tag must not be empty', + } + ) + ), + type: VALIDATION_TYPES.ARRAY_ITEM, + isBlocking: false, + }, + ], }, note: { type: FIELD_TYPES.TEXTAREA, diff --git a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx index 83ca0026b8934..bb9ec01399f8d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx @@ -12,10 +12,11 @@ import { useUserInfo, ManageUserInfo } from './index'; import { useKibana } from '../../../common/lib/kibana'; import * as api from '../../containers/detection_engine/alerts/api'; import { TestProviders } from '../../../common/mock/test_providers'; -import { UserPrivilegesProvider } from '../user_privileges'; +import { UserPrivilegesProvider } from '../../../common/components/user_privileges'; jest.mock('../../../common/lib/kibana'); jest.mock('../../containers/detection_engine/alerts/api'); +jest.mock('../../../common/components/user_privileges/use_endpoint_privileges'); describe('useUserInfo', () => { beforeAll(() => { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/translations.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/translations.ts index d5234e719b869..ed6a22375a776 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/translations.ts @@ -37,13 +37,3 @@ export const CASES_FROM_ALERTS_FAILURE = i18n.translate( 'xpack.securitySolution.endpoint.hostIsolation.casesFromAlerts.title', { defaultMessage: 'Failed to find associated cases' } ); - -export const ISOLATION_STATUS_FAILURE = i18n.translate( - 'xpack.securitySolution.endpoint.hostIsolation.isolationStatus.title', - { defaultMessage: 'Failed to retrieve current isolation status' } -); - -export const ISOLATION_PENDING_FAILURE = i18n.translate( - 'xpack.securitySolution.endpoint.hostIsolation.isolationPending.title', - { defaultMessage: 'Failed to retrieve isolation pending statuses' } -); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx index 651d80f3165ab..f3afe83365286 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx @@ -9,13 +9,13 @@ import { act, renderHook } from '@testing-library/react-hooks'; import produce from 'immer'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock'; -import { useUserPrivileges } from '../../../components/user_privileges'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; import { Privilege } from './types'; import { UseAlertsPrivelegesReturn, useAlertsPrivileges } from './use_alerts_privileges'; jest.mock('./api'); jest.mock('../../../../common/hooks/use_app_toasts'); -jest.mock('../../../components/user_privileges'); +jest.mock('../../../../common/components/user_privileges'); const useUserPrivilegesMock = useUserPrivileges as jest.Mock>; @@ -86,6 +86,7 @@ const userPrivilegesInitial: ReturnType = { result: undefined, error: undefined, }, + endpointPrivileges: { loading: true, canAccessEndpointManagement: false, canAccessFleet: false }, }; describe('usePrivilegeUser', () => { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.tsx index 08e28521e1473..005224a80c189 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.tsx @@ -6,7 +6,7 @@ */ import { useEffect, useState } from 'react'; -import { useUserPrivileges } from '../../../components/user_privileges'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; export interface UseAlertsPrivelegesReturn extends AlertsPrivelegesState { loading: boolean; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_isolation_status.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_isolation_status.tsx index 259a377b10b79..6a40898d0a109 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_isolation_status.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_isolation_status.tsx @@ -7,9 +7,7 @@ import { isEmpty } from 'lodash'; import { useEffect, useState } from 'react'; -import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { getHostMetadata } from './api'; -import { ISOLATION_STATUS_FAILURE, ISOLATION_PENDING_FAILURE } from './translations'; import { fetchPendingActionsByAgentId } from '../../../../common/lib/endpoint_pending_actions'; import { isEndpointHostIsolated } from '../../../../common/utils/validators'; import { HostStatus } from '../../../../../common/endpoint/types'; @@ -17,7 +15,7 @@ import { HostStatus } from '../../../../../common/endpoint/types'; interface HostIsolationStatusResponse { loading: boolean; isIsolated: boolean; - agentStatus: HostStatus; + agentStatus: HostStatus | undefined; pendingIsolation: number; pendingUnisolation: number; } @@ -30,13 +28,11 @@ export const useHostIsolationStatus = ({ agentId: string; }): HostIsolationStatusResponse => { const [isIsolated, setIsIsolated] = useState(false); - const [agentStatus, setAgentStatus] = useState(HostStatus.UNHEALTHY); + const [agentStatus, setAgentStatus] = useState(); const [pendingIsolation, setPendingIsolation] = useState(0); const [pendingUnisolation, setPendingUnisolation] = useState(0); const [loading, setLoading] = useState(false); - const { addError } = useAppToasts(); - useEffect(() => { const abortCtrl = new AbortController(); // isMounted tracks if a component is mounted before changing state @@ -55,7 +51,10 @@ export const useHostIsolationStatus = ({ if (error.name === 'AbortError') { return; } - addError(error.message, { title: ISOLATION_STATUS_FAILURE }); + + if (isMounted && error.body.statusCode === 404) { + setAgentStatus(HostStatus.UNENROLLED); + } } try { @@ -65,7 +64,8 @@ export const useHostIsolationStatus = ({ setPendingUnisolation(data[0].pending_actions?.unisolate ?? 0); } } catch (error) { - addError(error.message, { title: ISOLATION_PENDING_FAILURE }); + // silently catch non-user initiated error + return; } if (isMounted) { @@ -87,6 +87,6 @@ export const useHostIsolationStatus = ({ isMounted = false; abortCtrl.abort(); }; - }, [addError, agentId]); + }, [agentId]); return { loading, isIsolated, agentStatus, pendingIsolation, pendingUnisolation }; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx index ce262ce4f9a2e..ade83fed4fd6b 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx @@ -13,6 +13,7 @@ import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; jest.mock('./api'); jest.mock('../../../../common/hooks/use_app_toasts'); +jest.mock('../../../../common/components/user_privileges/use_endpoint_privileges'); describe('useSignalIndex', () => { let appToastsMock: jest.Mocked>; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_privileges.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_privileges.tsx index 0b7cd673c49f4..5f21f0287d7ea 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_privileges.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_privileges.tsx @@ -6,7 +6,7 @@ */ import { useEffect, useState } from 'react'; -import { useUserPrivileges } from '../../../components/user_privileges'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; import { Privilege } from '../alerts/types'; export interface UseListsPrivilegesState { diff --git a/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts b/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts index a5da747787ba6..f28311d9c96e7 100644 --- a/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts +++ b/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts @@ -10143,7 +10143,7 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ * * Is built alongside and sampled from the data in the file so to always be valid with the most up to date MITRE ATT&CK data */ -export const mockThreatData = { +export const getMockThreatData = () => ({ tactic: { name: 'Privilege Escalation', id: 'TA0004', @@ -10162,4 +10162,4 @@ export const mockThreatData = { tactics: ['privilege-escalation', 'persistence'], techniqueId: 'T1546', }, -}; +}); diff --git a/x-pack/plugins/security_solution/public/detections/mitre/valid_threat_mock.ts b/x-pack/plugins/security_solution/public/detections/mitre/valid_threat_mock.ts index a7de7494e1116..743b143213c22 100644 --- a/x-pack/plugins/security_solution/public/detections/mitre/valid_threat_mock.ts +++ b/x-pack/plugins/security_solution/public/detections/mitre/valid_threat_mock.ts @@ -6,9 +6,9 @@ */ import { Threats } from '@kbn/securitysolution-io-ts-alerting-types'; -import { mockThreatData } from './mitre_tactics_techniques'; +import { getMockThreatData } from './mitre_tactics_techniques'; -const { tactic, technique, subtechnique } = mockThreatData; +const { tactic, technique, subtechnique } = getMockThreatData(); const { tactics, ...mockTechnique } = technique; const { tactics: subtechniqueTactics, ...mockSubtechnique } = subtechnique; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx index b4f5efe2348bb..206976e6c0c1a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx @@ -336,7 +336,8 @@ export const ExceptionListsTable = React.memo(() => { <> <> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/translations.ts index 0dd016425f4e6..912f5bec4de35 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/translations.ts @@ -73,7 +73,14 @@ export const EXCEPTIONS_LISTS_SEARCH_PLACEHOLDER = i18n.translate( export const ALL_EXCEPTIONS = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allExceptions.tableTitle', { - defaultMessage: 'Exception Lists', + defaultMessage: 'Exceptions', + } +); + +export const ALL_EXCEPTIONS_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allExceptions.tableTitleDescription', + { + defaultMessage: 'Exceptions are automatically grouped into exception lists.', } ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx index 45ce5bc18361c..c5262caf6c776 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx @@ -102,9 +102,11 @@ const TagsFilterPopoverComponent = ({ ownFocus button={ setIsTagPopoverOpen(!isTagPopoverOpen)} + numFilters={tags.length} isSelected={isTagPopoverOpen} hasActiveFilters={selectedTags.length > 0} numActiveFilters={selectedTags.length} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/translations.ts index 5a49ab349c094..f4292f6c663cc 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/translations.ts @@ -15,9 +15,9 @@ export const PAGE_TITLE = i18n.translate( ); export const BACK_TO_RULES = i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.backToRulesDescription', + 'xpack.securitySolution.detectionEngine.createRule.backToRulesButton', { - defaultMessage: 'Back to detection rules', + defaultMessage: 'Rules', } ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts index e42fde569bd67..ca3e5a4587a09 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts @@ -15,9 +15,9 @@ export const PAGE_TITLE = i18n.translate( ); export const BACK_TO_RULES = i18n.translate( - 'xpack.securitySolution.detectionEngine.ruleDetails.backToRulesDescription', + 'xpack.securitySolution.detectionEngine.ruleDetails.backToRulesButton', { - defaultMessage: 'Back to detection rules', + defaultMessage: 'Rules', } ); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts index 5498d139fd6e1..76de52222bbd3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts @@ -15,7 +15,6 @@ import { HostPolicyResponse, HostResultList, HostStatus, - MetadataQueryStrategyVersions, } from '../../../../common/endpoint/types'; import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; import { @@ -25,6 +24,8 @@ import { } from '../../../../common/endpoint/constants'; import { AGENT_POLICY_API_ROUTES, + appRoutesService, + CheckPermissionsResponse, EPM_API_ROUTES, GetAgentPoliciesResponse, GetPackagesResponse, @@ -52,7 +53,6 @@ export const endpointMetadataHttpMocks = httpHandlerMockFactory GetAgentPoliciesResponse; }>; -export const fleetGetAgentPolicyListHttpMock = httpHandlerMockFactory([ - { - id: 'agentPolicy', - path: AGENT_POLICY_API_ROUTES.LIST_PATTERN, - method: 'get', - handler: () => { - const generator = new EndpointDocGenerator('seed'); - const endpointMetadata = generator.generateHostMetadata(); - const agentPolicy = generator.generateAgentPolicy(); - - // Make sure that the Agent policy returned from the API has the Integration Policy ID that - // the endpoint metadata is using. This is needed especially when testing the Endpoint Details - // flyout where certain actions might be disabled if we know the endpoint integration policy no - // longer exists. - (agentPolicy.package_policies as string[]).push(endpointMetadata.Endpoint.policy.applied.id); - - return { - items: [agentPolicy], - perPage: 10, - total: 1, - page: 1, - }; +export const fleetGetAgentPolicyListHttpMock = httpHandlerMockFactory( + [ + { + id: 'agentPolicy', + path: AGENT_POLICY_API_ROUTES.LIST_PATTERN, + method: 'get', + handler: () => { + const generator = new EndpointDocGenerator('seed'); + const endpointMetadata = generator.generateHostMetadata(); + const agentPolicy = generator.generateAgentPolicy(); + + // Make sure that the Agent policy returned from the API has the Integration Policy ID that + // the endpoint metadata is using. This is needed especially when testing the Endpoint Details + // flyout where certain actions might be disabled if we know the endpoint integration policy no + // longer exists. + (agentPolicy.package_policies as string[]).push( + endpointMetadata.Endpoint.policy.applied.id + ); + + return { + items: [agentPolicy], + perPage: 10, + total: 1, + page: 1, + }; + }, }, - }, -]); + ] +); + +export type FleetGetCheckPermissionsInterface = ResponseProvidersInterface<{ + checkPermissions: () => CheckPermissionsResponse; +}>; + +export const fleetGetCheckPermissionsHttpMock = httpHandlerMockFactory( + [ + { + id: 'checkPermissions', + path: appRoutesService.getCheckPermissionsPath(), + method: 'get', + handler: () => { + return { + error: undefined, + success: true, + }; + }, + }, + ] +); type FleetApisHttpMockInterface = FleetGetPackageListHttpMockInterface & - FleetGetAgentPolicyListHttpMockInterface; + FleetGetAgentPolicyListHttpMockInterface & + FleetGetCheckPermissionsInterface; +/** + * Mocks all Fleet apis needed to render the Endpoint List/Details pages + */ export const fleetApisHttpMock = composeHttpHandlerMocks([ fleetGetPackageListHttpMock, fleetGetAgentPolicyListHttpMock, + fleetGetCheckPermissionsHttpMock, ]); type EndpointPageHttpMockInterface = EndpointMetadataHttpMocksInterface & diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts index 42c16e151c45d..3fe6821abbcbe 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts @@ -146,13 +146,6 @@ export type EndpointIsolationRequestStateChange = Action<'endpointIsolationReque payload: EndpointState['isolationRequestState']; }; -export interface AppRequestedEndpointActivityLog { - type: 'appRequestedEndpointActivityLog'; - payload: { - page: number; - pageSize: number; - }; -} export type EndpointDetailsActivityLogChanged = Action<'endpointDetailsActivityLogChanged'> & { payload: EndpointState['endpointDetails']['activityLog']['logData']; }; @@ -165,9 +158,18 @@ export interface EndpointDetailsActivityLogUpdatePaging { type: 'endpointDetailsActivityLogUpdatePaging'; payload: { // disable paging when no more data after paging - disabled: boolean; + disabled?: boolean; page: number; pageSize: number; + startDate?: string; + endDate?: string; + }; +} + +export interface EndpointDetailsActivityLogUpdateIsInvalidDateRange { + type: 'endpointDetailsActivityLogUpdateIsInvalidDateRange'; + payload: { + isInvalidDateRange?: boolean; }; } @@ -181,8 +183,8 @@ export type EndpointAction = | ServerFailedToReturnEndpointList | ServerReturnedEndpointDetails | ServerFailedToReturnEndpointDetails - | AppRequestedEndpointActivityLog | EndpointDetailsActivityLogUpdatePaging + | EndpointDetailsActivityLogUpdateIsInvalidDateRange | EndpointDetailsFlyoutTabChanged | EndpointDetailsActivityLogChanged | ServerReturnedEndpointPolicyResponse diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts index 5db861d18cd69..384a7b999d826 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts @@ -25,6 +25,9 @@ export const initialEndpointPageState = (): Immutable => { disabled: false, page: 1, pageSize: 50, + startDate: undefined, + endDate: undefined, + isInvalidDateRange: false, }, logData: createUninitialisedResourceState(), }, @@ -53,7 +56,6 @@ export const initialEndpointPageState = (): Immutable => { agentsWithEndpointsTotalError: undefined, endpointsTotal: 0, endpointsTotalError: undefined, - queryStrategyVersion: undefined, policyVersionInfo: undefined, hostStatus: undefined, isolationRequestState: createUninitialisedResourceState(), diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts index 3bf625d726e5f..a9c65c74015c6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts @@ -48,6 +48,7 @@ describe('EndpointList store concerns', () => { disabled: false, page: 1, pageSize: 50, + isInvalidDateRange: false, }, logData: { type: 'UninitialisedResourceState' }, }, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index e34e9cf5a83f3..922f10cee2f8b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -28,7 +28,6 @@ import { nonExistingPolicies, patterns, searchBarQuery, - isTransformEnabled, getIsIsolationRequestPending, getCurrentIsolationRequestState, getActivityLogData, @@ -65,6 +64,7 @@ import { resolvePathVariables } from '../../../../common/utils/resolve_path_vari import { EndpointPackageInfoStateChanged } from './action'; import { fetchPendingActionsByAgentId } from '../../../../common/lib/endpoint_pending_actions'; import { EndpointDetailsTabsTypes } from '../view/details/components/endpoint_details_tabs'; +import { getIsInvalidDateRange } from '../utils'; type EndpointPageStore = ImmutableMiddlewareAPI; @@ -179,7 +179,7 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory(getActivityLogData(getState())), - }); - + if ( + action.type === 'endpointDetailsActivityLogUpdatePaging' && + hasSelectedEndpoint(getState()) + ) { try { - const { page, pageSize } = getActivityLogDataPaging(getState()); + const { disabled, page, pageSize, startDate, endDate } = getActivityLogDataPaging( + getState() + ); + // don't page when paging is disabled or when date ranges are invalid + if (disabled) { + return; + } + if (getIsInvalidDateRange({ startDate, endDate })) { + dispatch({ + type: 'endpointDetailsActivityLogUpdateIsInvalidDateRange', + payload: { + isInvalidDateRange: true, + }, + }); + return; + } + + dispatch({ + type: 'endpointDetailsActivityLogUpdateIsInvalidDateRange', + payload: { + isInvalidDateRange: false, + }, + }); + dispatch({ + type: 'endpointDetailsActivityLogChanged', + // ts error to be fixed when AsyncResourceState is refactored (#830) + // @ts-expect-error + payload: createLoadingResourceState(getActivityLogData(getState())), + }); const route = resolvePathVariables(ENDPOINT_ACTION_LOG_ROUTE, { agent_id: selectedAgent(getState()), }); const activityLog = await coreStart.http.get(route, { - query: { page, page_size: pageSize }, + query: { + page, + page_size: pageSize, + start_date: startDate, + end_date: endDate, + }, }); const lastLoadedLogData = getLastLoadedActivityLogData(getState()); @@ -428,6 +457,8 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory 1 ? activityLog.page - 1 : 1, pageSize: activityLog.pageSize, + startDate: activityLog.startDate, + endDate: activityLog.endDate, }, }); } diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts index 8d257678a425d..e0590a80a66aa 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts @@ -12,7 +12,6 @@ import { HostPolicyResponse, HostResultList, HostStatus, - MetadataQueryStrategyVersions, PendingActionsResponse, } from '../../../../../common/endpoint/types'; import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; @@ -38,13 +37,11 @@ export const mockEndpointResultList: (options?: { total?: number; request_page_size?: number; request_page_index?: number; - query_strategy_version?: MetadataQueryStrategyVersions; }) => HostResultList = (options = {}) => { const { total = 1, request_page_size: requestPageSize = 10, request_page_index: requestPageIndex = 0, - query_strategy_version: queryStrategyVersion = MetadataQueryStrategyVersions.VERSION_2, } = options; // Skip any that are before the page we're on @@ -58,7 +55,6 @@ export const mockEndpointResultList: (options?: { hosts.push({ metadata: generator.generateHostMetadata(), host_status: HostStatus.UNHEALTHY, - query_strategy_version: queryStrategyVersion, }); } const mock: HostResultList = { @@ -66,7 +62,6 @@ export const mockEndpointResultList: (options?: { total, request_page_size: requestPageSize, request_page_index: requestPageIndex, - query_strategy_version: queryStrategyVersion, }; return mock; }; @@ -78,7 +73,6 @@ export const mockEndpointDetailsApiResult = (): HostInfo => { return { metadata: generator.generateHostMetadata(), host_status: HostStatus.UNHEALTHY, - query_strategy_version: MetadataQueryStrategyVersions.VERSION_2, }; }; @@ -92,7 +86,6 @@ const endpointListApiPathHandlerMocks = ({ endpointPackagePolicies = [], policyResponse = generator.generatePolicyResponse(), agentPolicy = generator.generateAgentPolicy(), - queryStrategyVersion = MetadataQueryStrategyVersions.VERSION_2, totalAgentsUsingEndpoint = 0, }: { /** route handlers will be setup for each individual host in this array */ @@ -101,7 +94,6 @@ const endpointListApiPathHandlerMocks = ({ endpointPackagePolicies?: GetPolicyListResponse['items']; policyResponse?: HostPolicyResponse; agentPolicy?: GetAgentPoliciesResponseItem; - queryStrategyVersion?: MetadataQueryStrategyVersions; totalAgentsUsingEndpoint?: number; } = {}) => { const apiHandlers = { @@ -119,7 +111,6 @@ const endpointListApiPathHandlerMocks = ({ request_page_size: 10, request_page_index: 0, total: endpointsResults?.length || 0, - query_strategy_version: queryStrategyVersion, }; }, @@ -192,16 +183,11 @@ export const setEndpointListApiMockImplementation: ( apiResponses?: Parameters[0] ) => void = ( mockedHttpService, - { - endpointsResults = mockEndpointResultList({ total: 3 }).hosts, - queryStrategyVersion = MetadataQueryStrategyVersions.VERSION_2, - ...pathHandlersOptions - } = {} + { endpointsResults = mockEndpointResultList({ total: 3 }).hosts, ...pathHandlersOptions } = {} ) => { const apiHandlers = endpointListApiPathHandlerMocks({ ...pathHandlersOptions, endpointsResults, - queryStrategyVersion, }); mockedHttpService.post diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts index 0981d621f26f3..c6bf13a3b5715 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts @@ -41,6 +41,8 @@ const handleEndpointDetailsActivityLogChanged: CaseReducer) = state.agentsWithEndpointsTotalError; export const endpointsTotalError = (state: Immutable) => state.endpointsTotalError; -const queryStrategyVersion = (state: Immutable) => state.queryStrategyVersion; export const endpointPackageVersion = createSelector(endpointPackageInfo, (info) => isLoadedResourceState(info) ? info.data.version : undefined ); -export const isTransformEnabled = createSelector( - queryStrategyVersion, - (version) => version !== MetadataQueryStrategyVersions.VERSION_1 -); - /** * Returns the index patterns for the SearchBar to use for autosuggest */ diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts index de213b3dbccc3..875841cb55b73 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts @@ -13,7 +13,6 @@ import { HostPolicyResponse, AppLocation, PolicyData, - MetadataQueryStrategyVersions, HostStatus, HostIsolationResponse, EndpointPendingActions, @@ -40,9 +39,12 @@ export interface EndpointState { flyoutView: EndpointIndexUIQueryParams['show']; activityLog: { paging: { - disabled: boolean; + disabled?: boolean; page: number; pageSize: number; + startDate?: string; + endDate?: string; + isInvalidDateRange: boolean; }; logData: AsyncResourceState; }; @@ -93,8 +95,6 @@ export interface EndpointState { endpointsTotal: number; /** api error for total, actual Endpoints */ endpointsTotalError?: ServerApiError; - /** The query strategy version that informs whether the transform for KQL is enabled or not */ - queryStrategyVersion?: MetadataQueryStrategyVersions; /** The policy IDs and revision number of the corresponding agent, and endpoint. May be more recent than what's running */ policyVersionInfo?: HostInfo['policy_info']; /** The status of the host, which is mapped to the Elastic Agent status in Fleet */ diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.test.ts new file mode 100644 index 0000000000000..fa2aaaa16ae37 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.test.ts @@ -0,0 +1,30 @@ +/* + * 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 moment from 'moment'; +import { getIsInvalidDateRange } from './utils'; + +describe('utils', () => { + describe('getIsInvalidDateRange', () => { + it('should return FALSE when either dates are undefined', () => { + expect(getIsInvalidDateRange({})).toBe(false); + expect(getIsInvalidDateRange({ startDate: moment().subtract(1, 'd').toISOString() })).toBe( + false + ); + expect(getIsInvalidDateRange({ endDate: moment().toISOString() })).toBe(false); + }); + + it('should return TRUE when startDate is after endDate', () => { + expect( + getIsInvalidDateRange({ + startDate: moment().toISOString(), + endDate: moment().subtract(1, 'd').toISOString(), + }) + ).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.ts index 3e17992dd975f..e2d619743c83b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/utils.ts @@ -5,6 +5,7 @@ * 2.0. */ +import moment from 'moment'; import { HostInfo, HostMetadata } from '../../../../common/endpoint/types'; export const isPolicyOutOfDate = ( @@ -23,3 +24,18 @@ export const isPolicyOutOfDate = ( reported.endpoint_policy_version >= current.endpoint.revision ); }; + +export const getIsInvalidDateRange = ({ + startDate, + endDate, +}: { + startDate?: string; + endDate?: string; +}) => { + if (startDate && endDate) { + const start = moment(startDate); + const end = moment(endDate); + return start.isAfter(end); + } + return false; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx new file mode 100644 index 0000000000000..f11d2872e3d26 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx @@ -0,0 +1,127 @@ +/* + * 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 { useDispatch } from 'react-redux'; +import React, { memo, useCallback } from 'react'; +import styled from 'styled-components'; +import moment from 'moment'; +import { EuiFlexGroup, EuiFlexItem, EuiDatePicker, EuiDatePickerRange } from '@elastic/eui'; + +import * as i18 from '../../../translations'; +import { useEndpointSelector } from '../../../hooks'; +import { getActivityLogDataPaging } from '../../../../store/selectors'; + +const DatePickerWrapper = styled.div` + width: ${(props) => props.theme.eui.fractions.single.percentage}; + background: white; +`; +const StickyFlexItem = styled(EuiFlexItem)` + position: sticky; + top: ${(props) => props.theme.eui.euiSizeM}; + z-index: 1; +`; + +export const DateRangePicker = memo(() => { + const dispatch = useDispatch(); + const { page, pageSize, startDate, endDate, isInvalidDateRange } = useEndpointSelector( + getActivityLogDataPaging + ); + + const onClear = useCallback( + ({ clearStart = false, clearEnd = false }: { clearStart?: boolean; clearEnd?: boolean }) => { + dispatch({ + type: 'endpointDetailsActivityLogUpdatePaging', + payload: { + disabled: false, + page, + pageSize, + startDate: clearStart ? undefined : startDate, + endDate: clearEnd ? undefined : endDate, + }, + }); + }, + [dispatch, endDate, startDate, page, pageSize] + ); + + const onChangeStartDate = useCallback( + (date) => { + dispatch({ + type: 'endpointDetailsActivityLogUpdatePaging', + payload: { + disabled: false, + page, + pageSize, + startDate: date ? date?.toISOString() : undefined, + endDate: endDate ? endDate : undefined, + }, + }); + }, + [dispatch, endDate, page, pageSize] + ); + + const onChangeEndDate = useCallback( + (date) => { + dispatch({ + type: 'endpointDetailsActivityLogUpdatePaging', + payload: { + disabled: false, + page, + pageSize, + startDate: startDate ? startDate : undefined, + endDate: date ? date.toISOString() : undefined, + }, + }); + }, + [dispatch, startDate, page, pageSize] + ); + + return ( + + + + + onClear({ clearStart: true })} + placeholderText={i18.ACTIVITY_LOG.datePicker.startDate} + selected={startDate ? moment(startDate) : undefined} + showTimeSelect + startDate={startDate ? moment(startDate) : undefined} + /> + } + endDateControl={ + onClear({ clearEnd: true })} + placeholderText={i18.ACTIVITY_LOG.datePicker.endDate} + selected={endDate ? moment(endDate) : undefined} + showTimeSelect + startDate={startDate ? moment(startDate) : undefined} + /> + } + /> + + + + + ); +}); + +DateRangePicker.displayName = 'DateRangePicker'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx index aa1f56529657e..73a3734e4ca88 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx @@ -56,19 +56,14 @@ export const EndpointDetailsFlyoutTabs = memo( }, }); if (tab.id === EndpointDetailsTabsTypes.activityLog) { - const paging = { - page: 1, - pageSize, - }; - dispatch({ - type: 'appRequestedEndpointActivityLog', - payload: paging, - }); dispatch({ type: 'endpointDetailsActivityLogUpdatePaging', payload: { disabled: false, - ...paging, + page: 1, + pageSize, + startDate: undefined, + endDate: undefined, }, }); } diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx index 360d6e3842816..121f23fdb3a9e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { memo, useCallback, useEffect, useRef } from 'react'; +import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react'; import styled from 'styled-components'; import { @@ -17,6 +17,7 @@ import { } from '@elastic/eui'; import { useDispatch } from 'react-redux'; import { LogEntry } from './components/log_entry'; +import { DateRangePicker } from './components/activity_log_date_range_picker'; import * as i18 from '../translations'; import { Immutable, ActivityLog } from '../../../../../../common/endpoint/types'; import { AsyncResourceState } from '../../../../state'; @@ -31,12 +32,12 @@ import { getActivityLogRequestLoading, } from '../../store/selectors'; -const StyledEuiFlexGroup = styled(EuiFlexGroup)` - height: 85vh; +const StyledEuiFlexGroup = styled(EuiFlexGroup)<{ isShorter: boolean }>` + height: ${({ isShorter }) => (isShorter ? '25vh' : '85vh')}; `; const LoadMoreTrigger = styled.div` - height: 6px; - width: 100%; + height: ${(props) => props.theme.eui.euiSizeXS}; + width: ${(props) => props.theme.eui.fractions.single.percentage}; `; export const EndpointActivityLog = memo( @@ -48,25 +49,37 @@ export const EndpointActivityLog = memo( const activityLogSize = activityLogData.length; const activityLogError = useEndpointSelector(getActivityLogError); const dispatch = useDispatch<(action: EndpointAction) => void>(); - const { page, pageSize, disabled: isPagingDisabled } = useEndpointSelector( + const { page, pageSize, startDate, endDate, disabled: isPagingDisabled } = useEndpointSelector( getActivityLogDataPaging ); + const hasActiveDateRange = useMemo(() => !!startDate || !!endDate, [startDate, endDate]); + const showEmptyState = useMemo( + () => (activityLogLoaded && !activityLogSize && !hasActiveDateRange) || activityLogError, + [activityLogLoaded, activityLogSize, hasActiveDateRange, activityLogError] + ); + const isShorter = useMemo( + () => !!(hasActiveDateRange && isPagingDisabled && !activityLogLoading && !activityLogSize), + [hasActiveDateRange, isPagingDisabled, activityLogLoading, activityLogSize] + ); + const loadMoreTrigger = useRef(null); const getActivityLog = useCallback( (entries: IntersectionObserverEntry[]) => { const isTargetIntersecting = entries.some((entry) => entry.isIntersecting); if (isTargetIntersecting && activityLogLoaded && !isPagingDisabled) { dispatch({ - type: 'appRequestedEndpointActivityLog', + type: 'endpointDetailsActivityLogUpdatePaging', payload: { page: page + 1, pageSize, + startDate, + endDate, }, }); } }, - [activityLogLoaded, dispatch, isPagingDisabled, page, pageSize] + [activityLogLoaded, dispatch, isPagingDisabled, page, pageSize, startDate, endDate] ); useEffect(() => { @@ -82,8 +95,8 @@ export const EndpointActivityLog = memo( return ( <> - - {(activityLogLoaded && !activityLogSize) || activityLogError ? ( + + {showEmptyState ? ( ) : ( <> + {activityLogLoaded && activityLogData.map((logEntry) => ( diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/host_constants.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/host_constants.ts index fb9661b509a33..45cf7d725443d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/host_constants.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/host_constants.ts @@ -18,6 +18,7 @@ export const HOST_STATUS_TO_BADGE_COLOR = Object.freeze< [HostStatus.UPDATING]: 'primary', [HostStatus.OFFLINE]: 'default', [HostStatus.INACTIVE]: 'default', + [HostStatus.UNENROLLED]: 'default', }); export const POLICY_STATUS_TO_HEALTH_COLOR = Object.freeze< diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index ee5ef52d00f18..26d0d53e39982 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -23,7 +23,6 @@ import { HostPolicyResponseActionStatus, HostPolicyResponseAppliedAction, HostStatus, - MetadataQueryStrategyVersions, } from '../../../../../common/endpoint/types'; import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; import { POLICY_STATUS_TO_TEXT } from './host_constants'; @@ -167,31 +166,6 @@ describe('when on the endpoint list page', () => { }); }); - describe('when loading data with the query_strategy_version is `v1`', () => { - beforeEach(() => { - reactTestingLibrary.act(() => { - const mockedEndpointListData = mockEndpointResultList({ - total: 4, - query_strategy_version: MetadataQueryStrategyVersions.VERSION_1, - }); - setEndpointListApiMockImplementation(coreStart.http, { - endpointsResults: mockedEndpointListData.hosts, - queryStrategyVersion: mockedEndpointListData.query_strategy_version, - }); - }); - }); - afterEach(() => { - jest.clearAllMocks(); - }); - it('should not display the KQL bar', async () => { - const renderResult = render(); - await reactTestingLibrary.act(async () => { - await middlewareSpy.waitForAction('serverReturnedEndpointList'); - }); - expect(renderResult.queryByTestId('adminSearchBar')).toBeNull(); - }); - }); - describe('when determining when to show the enrolling message', () => { afterEach(() => { jest.clearAllMocks(); @@ -268,7 +242,6 @@ describe('when on the endpoint list page', () => { reactTestingLibrary.act(() => { const mockedEndpointData = mockEndpointResultList({ total: 5 }); const hostListData = mockedEndpointData.hosts; - const queryStrategyVersion = mockedEndpointData.query_strategy_version; firstPolicyID = hostListData[0].metadata.Endpoint.policy.applied.id; firstPolicyRev = hostListData[0].metadata.Endpoint.policy.applied.endpoint_policy_version; @@ -329,7 +302,6 @@ describe('when on the endpoint list page', () => { hostListData[index].metadata.Endpoint.policy.applied, setup.policy ), - query_strategy_version: queryStrategyVersion, }; }); hostListData.forEach((item, index) => { @@ -535,8 +507,6 @@ describe('when on the endpoint list page', () => { // eslint-disable-next-line @typescript-eslint/naming-convention host_status, metadata: { agent, Endpoint, ...details }, - // eslint-disable-next-line @typescript-eslint/naming-convention - query_strategy_version, } = mockEndpointDetailsApiResult(); hostDetails = { @@ -555,7 +525,6 @@ describe('when on the endpoint list page', () => { id: '1', }, }, - query_strategy_version, }; const policy = docGenerator.generatePolicyPackagePolicy(); @@ -889,6 +858,27 @@ describe('when on the endpoint list page', () => { const emptyState = await renderResult.queryByTestId('activityLogEmpty'); expect(emptyState).not.toBe(null); }); + + it('should not display empty state with no log data while date range filter is active', async () => { + const activityLogTab = await renderResult.findByTestId('activity_log'); + reactTestingLibrary.act(() => { + reactTestingLibrary.fireEvent.click(activityLogTab); + }); + await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged'); + reactTestingLibrary.act(() => { + dispatchEndpointDetailsActivityLogChanged('success', { + page: 1, + pageSize: 50, + startDate: new Date().toISOString(), + data: [], + }); + }); + + const emptyState = await renderResult.queryByTestId('activityLogEmpty'); + const dateRangePicker = await renderResult.queryByTestId('activityLogDateRangePicker'); + expect(emptyState).toBe(null); + expect(dateRangePicker).not.toBe(null); + }); }); describe('when showing host Policy Response panel', () => { @@ -1177,7 +1167,7 @@ describe('when on the endpoint list page', () => { let renderResult: ReturnType; const mockEndpointListApi = () => { - const { hosts, query_strategy_version: queryStrategyVersion } = mockEndpointResultList(); + const { hosts } = mockEndpointResultList(); hostInfo = { host_status: hosts[0].host_status, metadata: { @@ -1201,7 +1191,6 @@ describe('when on the endpoint list page', () => { version: '7.14.0', }, }, - query_strategy_version: queryStrategyVersion, }; const packagePolicy = docGenerator.generatePolicyPackagePolicy(); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 0ee345431055b..c78d4ca6af634 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -120,7 +120,6 @@ export const EndpointList = () => { areEndpointsEnrolling, agentsWithEndpointsTotalError, endpointsTotalError, - isTransformEnabled, } = useEndpointSelector(selector); const { search } = useFormatUrl(SecurityPageName.administration); const { getAppUrl } = useAppUrl(); @@ -476,8 +475,8 @@ export const EndpointList = () => { const hasListData = listData && listData.length > 0; const refreshStyle = useMemo(() => { - return { display: endpointsExist && isTransformEnabled ? 'flex' : 'none', maxWidth: 200 }; - }, [endpointsExist, isTransformEnabled]); + return { display: endpointsExist ? 'flex' : 'none', maxWidth: 200 }; + }, [endpointsExist]); const refreshIsPaused = useMemo(() => { return !endpointsExist ? false : hasSelectedEndpoint ? true : !isAutoRefreshEnabled; @@ -492,8 +491,8 @@ export const EndpointList = () => { }, [endpointsTotalError, agentsWithEndpointsTotalError]); const shouldShowKQLBar = useMemo(() => { - return endpointsExist && !patternsError && isTransformEnabled; - }, [endpointsExist, patternsError, isTransformEnabled]); + return endpointsExist && !patternsError; + }, [endpointsExist, patternsError]); return ( ExceptionListItemSchema; }>; +export const esResponseData = () => ({ + rawResponse: { + took: 0, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: 1, + max_score: 1, + hits: [ + { + _index: '.ds-logs-endpoint.events.process-default-2021.07.06-000001', + _id: 'ZihXfHoBP7UhLrksX9-B', + _score: 1, + _source: { + agent: { + id: '9b5fad11-6cd9-401b-afc1-1c2b0c8a2603', + type: 'endpoint', + version: '7.12.2', + }, + process: { + args: '"C:\\lsass.exe" \\d6e', + Ext: { + ancestry: ['wm6pfs8yo3', 'd0zpkp91jx'], + }, + parent: { + pid: 2356, + entity_id: 'wm6pfs8yo3', + }, + code_signature: { + subject_name: 'Microsoft', + status: 'trusted', + }, + name: 'lsass.exe', + pid: 2522, + entity_id: 'hmmlst1ewe', + executable: 'C:\\lsass.exe', + hash: { + md5: 'de8c03a1-099f-4d9b-9a5e-1961c18af19f', + }, + }, + network: { + forwarded_ip: '10.105.19.209', + direction: 'inbound', + }, + '@timestamp': 1625694621727, + ecs: { + version: '1.4.0', + }, + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'endpoint.events.process', + }, + host: { + hostname: 'Host-15ofk0qkwk', + os: { + Ext: { + variant: 'Windows Pro', + }, + name: 'Linux', + family: 'Debian OS', + version: '10.0', + platform: 'Windows', + full: 'Windows 10', + }, + ip: ['10.133.4.77', '10.135.101.75', '10.137.102.119'], + name: 'Host-15ofk0qkwk', + id: 'bae7a849-1ce9-421a-a879-5fee5dcd1fb9', + mac: ['ad-65-2d-17-aa-95', '63-4-33-c5-c6-90'], + architecture: 'uwp8xmxk1f', + }, + event: { + agent_id_status: 'auth_metadata_missing', + sequence: 36, + ingested: '2021-07-06T15:02:18.746828Z', + kind: 'event', + id: '02057ac0-0ae5-442c-9082-c5a7489dde09', + category: 'network', + type: 'start', + }, + user: { + domain: '22bk8yptgw', + name: 'dlkfiz43rh', + }, + }, + }, + ], + }, + }, + isPartial: false, + isRunning: false, + total: 1, + loaded: 1, + isRestored: false, +}); + /** * Mock `core.http` methods used by Event Filters List page */ diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.tsx index 9f81d25520524..48523ce45c3f9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/flyout/index.tsx @@ -70,6 +70,15 @@ export const EventFiltersFlyout: React.FC = memo( payload: { entry: getInitialExceptionFromEvent() }, }); } + + return () => { + dispatch({ + type: 'eventFiltersFormStateChanged', + payload: { + type: 'UninitialisedResourceState', + }, + }); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/modal/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/modal/index.test.tsx index 178b774e91635..c77188694f507 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/modal/index.test.tsx @@ -6,18 +6,24 @@ */ import React from 'react'; import { EventFiltersModal } from '.'; -import { RenderResult, act, render } from '@testing-library/react'; +import { RenderResult, act } from '@testing-library/react'; import { fireEvent } from '@testing-library/dom'; -import { Provider } from 'react-redux'; -import { ThemeProvider } from 'styled-components'; -import { createGlobalNoMiddlewareStore, ecsEventMock } from '../../../test_utils'; -import { getMockTheme } from '../../../../../../common/lib/kibana/kibana_react.mock'; +import { ecsEventMock, esResponseData } from '../../../test_utils'; +import { + AppContextTestRender, + createAppRootMockRenderer, +} from '../../../../../../common/mock/endpoint'; +import { MiddlewareActionSpyHelper } from '../../../../../../common/store/test_utils'; + import { MODAL_TITLE, MODAL_SUBTITLE, ACTIONS_CONFIRM, ACTIONS_CANCEL } from './translations'; import type { CreateExceptionListItemSchema, ExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { useKibana } from '../../../../../../common/lib/kibana'; +import { EventFiltersListPageState } from '../../../types'; +jest.mock('../../../../../../common/lib/kibana'); jest.mock('../form'); jest.mock('../../hooks', () => { const originalModule = jest.requireActual('../../hooks'); @@ -29,67 +35,88 @@ jest.mock('../../hooks', () => { }; }); -const mockTheme = getMockTheme({ - eui: { - paddingSizes: { m: '2' }, - euiBreakpoints: { l: '2' }, - }, -}); - describe('Event filter modal', () => { let component: RenderResult; - let store: ReturnType; let onCancelMock: jest.Mock; - - const renderForm = () => { - const Wrapper: React.FC = ({ children }) => ( - - {children} - - ); - - return render(, { - wrapper: Wrapper, - }); - }; + let mockedContext: AppContextTestRender; + let waitForAction: MiddlewareActionSpyHelper['waitForAction']; + let render: () => ReturnType; + let getState: () => EventFiltersListPageState; beforeEach(() => { - store = createGlobalNoMiddlewareStore(); + mockedContext = createAppRootMockRenderer(); + waitForAction = mockedContext.middlewareSpy.waitForAction; onCancelMock = jest.fn(); + getState = () => mockedContext.store.getState().management.eventFilters; + render = () => + mockedContext.render(); + (useKibana as jest.Mock).mockReturnValue({ + services: { + http: {}, + data: { + search: { + search: jest.fn().mockImplementation(() => ({ toPromise: () => esResponseData() })), + }, + }, + notifications: {}, + }, + }); }); - it('should renders correctly', () => { - component = renderForm(); + it('should renders correctly', async () => { + await act(async () => { + component = render(); + await waitForAction('eventFiltersInitForm'); + }); + expect(component.getAllByText(MODAL_TITLE)).not.toBeNull(); expect(component.getByText(MODAL_SUBTITLE)).not.toBeNull(); expect(component.getAllByText(ACTIONS_CONFIRM)).not.toBeNull(); expect(component.getByText(ACTIONS_CANCEL)).not.toBeNull(); }); - it('should dispatch action to init form store on mount', () => { - component = renderForm(); - expect(store.getState()!.management!.eventFilters!.form!.entry).not.toBeNull(); + it('should dispatch action to init form store on mount', async () => { + await act(async () => { + component = render(); + await waitForAction('eventFiltersInitForm'); + }); + + expect(getState().form!.entry).not.toBeUndefined(); + }); + + it('should set OS with the enriched data', async () => { + await act(async () => { + component = render(); + await waitForAction('eventFiltersInitForm'); + }); + + expect(getState().form!.entry?.os_types).toContain('linux'); }); - it('should confirm form when button is disabled', () => { - component = renderForm(); + it('should confirm form when button is disabled', async () => { + await act(async () => { + component = render(); + await waitForAction('eventFiltersInitForm'); + }); + const confirmButton = component.getByTestId('add-exception-confirm-button'); act(() => { fireEvent.click(confirmButton); }); - expect(store.getState()!.management!.eventFilters!.form!.submissionResourceState.type).toBe( - 'UninitialisedResourceState' - ); + expect(getState().form!.submissionResourceState.type).toBe('UninitialisedResourceState'); }); - it('should confirm form when button is enabled', () => { - component = renderForm(); - store.dispatch({ + it('should confirm form when button is enabled', async () => { + await act(async () => { + component = render(); + await waitForAction('eventFiltersInitForm'); + }); + + mockedContext.store.dispatch({ type: 'eventFiltersChangeForm', payload: { entry: { - ...(store.getState()!.management!.eventFilters!.form! - .entry as CreateExceptionListItemSchema), + ...(getState().form!.entry as CreateExceptionListItemSchema), name: 'test', }, hasNameError: false, @@ -99,22 +126,24 @@ describe('Event filter modal', () => { act(() => { fireEvent.click(confirmButton); }); - expect(store.getState()!.management!.eventFilters!.form!.submissionResourceState.type).toBe( - 'UninitialisedResourceState' - ); - expect(confirmButton.hasAttribute('disabled')).toBeFalsy(); + expect(getState().form!.submissionResourceState.type).toBe('LoadingResourceState'); + expect(confirmButton.hasAttribute('disabled')).toBeTruthy(); }); - it('should close when exception has been submitted correctly', () => { - component = renderForm(); + it('should close when exception has been submitted correctly', async () => { + await act(async () => { + component = render(); + await waitForAction('eventFiltersInitForm'); + }); + expect(onCancelMock).toHaveBeenCalledTimes(0); act(() => { - store.dispatch({ + mockedContext.store.dispatch({ type: 'eventFiltersFormStateChanged', payload: { type: 'LoadedResourceState', - data: store.getState()!.management!.eventFilters!.form!.entry as ExceptionListItemSchema, + data: getState().form!.entry as ExceptionListItemSchema, }, }); }); @@ -122,8 +151,12 @@ describe('Event filter modal', () => { expect(onCancelMock).toHaveBeenCalledTimes(1); }); - it('should close when click on cancel button', () => { - component = renderForm(); + it('should close when click on cancel button', async () => { + await act(async () => { + component = render(); + await waitForAction('eventFiltersInitForm'); + }); + const cancelButton = component.getByText(ACTIONS_CANCEL); expect(onCancelMock).toHaveBeenCalledTimes(0); @@ -134,8 +167,12 @@ describe('Event filter modal', () => { expect(onCancelMock).toHaveBeenCalledTimes(1); }); - it('should close when close modal', () => { - component = renderForm(); + it('should close when close modal', async () => { + await act(async () => { + component = render(); + await waitForAction('eventFiltersInitForm'); + }); + const modalCloseButton = component.getByLabelText('Closes this modal window'); expect(onCancelMock).toHaveBeenCalledTimes(0); @@ -146,10 +183,14 @@ describe('Event filter modal', () => { expect(onCancelMock).toHaveBeenCalledTimes(1); }); - it('should prevent close when is loading action', () => { - component = renderForm(); + it('should prevent close when is loading action', async () => { + await act(async () => { + component = render(); + await waitForAction('eventFiltersInitForm'); + }); + act(() => { - store.dispatch({ + mockedContext.store.dispatch({ type: 'eventFiltersFormStateChanged', payload: { type: 'LoadingResourceState', diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/modal/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/modal/index.tsx index 50102d09248b1..dabf68ffed394 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/modal/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/modal/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { memo, useMemo, useEffect, useCallback } from 'react'; +import React, { memo, useMemo, useEffect, useCallback, useState, useRef } from 'react'; import { useDispatch } from 'react-redux'; import { Dispatch } from 'redux'; import styled, { css } from 'styled-components'; @@ -20,6 +20,7 @@ import { import { AppAction } from '../../../../../../common/store/actions'; import { Ecs } from '../../../../../../../common/ecs'; import { EventFiltersForm } from '../form'; +import { useKibana } from '../../../../../../common/lib/kibana'; import { useEventFiltersSelector, useEventFiltersNotification } from '../../hooks'; import { getFormHasError, @@ -61,10 +62,76 @@ const ModalBodySection = styled.section` export const EventFiltersModal: React.FC = memo(({ data, onCancel }) => { useEventFiltersNotification(); + const [enrichedData, setEnrichedData] = useState(); + const { + data: { search }, + } = useKibana().services; const dispatch = useDispatch>(); const formHasError = useEventFiltersSelector(getFormHasError); const creationInProgress = useEventFiltersSelector(isCreationInProgress); const creationSuccessful = useEventFiltersSelector(isCreationSuccessful); + const isMounted = useRef(false); + + // Enrich the event with missing ECS data from ES source + useEffect(() => { + isMounted.current = true; + + const enrichEvent = async () => { + if (!data._index) return; + const searchResponse = await search + .search({ + params: { + index: data._index, + body: { + query: { + match: { + _id: data._id, + }, + }, + }, + }, + }) + .toPromise(); + + if (!isMounted.current) return; + + setEnrichedData({ + ...data, + host: { + ...data.host, + os: { + ...(data?.host?.os || {}), + name: [searchResponse.rawResponse.hits.hits[0]._source.host.os.name], + }, + }, + }); + }; + + enrichEvent(); + + return () => { + dispatch({ + type: 'eventFiltersFormStateChanged', + payload: { + type: 'UninitialisedResourceState', + }, + }); + isMounted.current = false; + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Initialize the store with the enriched event to allow render the form + useEffect(() => { + if (enrichedData) { + dispatch({ + type: 'eventFiltersInitForm', + payload: { entry: getInitialExceptionFromEvent(enrichedData) }, + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [enrichedData]); useEffect(() => { if (creationSuccessful) { @@ -78,15 +145,6 @@ export const EventFiltersModal: React.FC = memo(({ data, } }, [creationSuccessful, onCancel, dispatch]); - // Initialize the store with the event passed as prop to allow render the form. It acts as componentDidMount - useEffect(() => { - dispatch({ - type: 'eventFiltersInitForm', - payload: { entry: getInitialExceptionFromEvent(data) }, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - const handleOnCancel = useCallback(() => { if (creationInProgress) return; onCancel(); diff --git a/x-pack/plugins/security_solution/public/management/pages/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/index.test.tsx index 9f2ed3618b06d..821e14edfda45 100644 --- a/x-pack/plugins/security_solution/public/management/pages/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/index.test.tsx @@ -10,11 +10,11 @@ import React from 'react'; import { ManagementContainer } from './index'; import '../../common/mock/match_media.ts'; import { AppContextTestRender, createAppRootMockRenderer } from '../../common/mock/endpoint'; -import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enabled'; +import { useUserPrivileges } from '../../common/components/user_privileges'; -jest.mock('../../common/hooks/endpoint/ingest_enabled'); +jest.mock('../../common/components/user_privileges'); -describe('when in the Admistration tab', () => { +describe('when in the Administration tab', () => { let render: () => ReturnType; beforeEach(() => { @@ -23,14 +23,18 @@ describe('when in the Admistration tab', () => { mockedContext.history.push('/administration/endpoints'); }); - it('should display the No Permissions view when Ingest is OFF', async () => { - (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: false }); + it('should display the No Permissions if no sufficient privileges', async () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + endpointPrivileges: { loading: false, canAccessEndpointManagement: false }, + }); expect(await render().findByTestId('noIngestPermissions')).not.toBeNull(); }); - it('should display the Management view when Ingest is ON', async () => { - (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true }); + it('should display the Management view if user has privileges', async () => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + endpointPrivileges: { loading: false, canAccessEndpointManagement: true }, + }); expect(await render().findByTestId('endpointPage')).not.toBeNull(); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/index.tsx b/x-pack/plugins/security_solution/public/management/pages/index.tsx index 327dcd4458eeb..f348be6089923 100644 --- a/x-pack/plugins/security_solution/public/management/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/index.tsx @@ -7,7 +7,7 @@ import React, { memo } from 'react'; import { Route, Switch, Redirect } from 'react-router-dom'; -import { EuiEmptyPrompt, EuiText } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiLoadingSpinner, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { MANAGEMENT_ROUTING_ENDPOINTS_PATH, @@ -22,9 +22,9 @@ import { PolicyContainer } from './policy'; import { TrustedAppsContainer } from './trusted_apps'; import { MANAGEMENT_PATH, SecurityPageName } from '../../../common/constants'; import { SpyRoute } from '../../common/utils/route/spy_routes'; -import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enabled'; import { EventFiltersContainer } from './event_filters'; import { getEndpointListPath } from '../common/routing'; +import { useUserPrivileges } from '../../common/components/user_privileges'; const NoPermissions = memo(() => { return ( @@ -80,9 +80,14 @@ const EventFilterTelemetry = () => ( ); export const ManagementContainer = memo(() => { - const { allEnabled: isIngestEnabled } = useIngestEnabledCheck(); + const { loading, canAccessEndpointManagement } = useUserPrivileges().endpointPrivileges; - if (!isIngestEnabled) { + // Lets wait until we can verify permissions + if (loading) { + return ; + } + + if (!canAccessEndpointManagement) { return ; } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts index 62d51c3630db7..db998b871cd93 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts @@ -691,4 +691,15 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ } ), }, + { + key: 'linux.advanced.malware.quarantine', + first_supported_version: '7.14', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.malware.quarantine', + { + defaultMessage: + 'Whether quarantine should be enabled when malware prevention is enabled. Default: true.', + } + ), + }, ]; diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.tsx index e22fec1861f8b..1600356882c36 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.tsx @@ -21,11 +21,11 @@ export const CtiDisabledModuleComponent = () => { const danger = useMemo( () => ( + {i18n.DANGER_BUTTON} } diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_inner_panel.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_inner_panel.tsx index 08bf0a432f9bb..ddff78608dfb0 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_inner_panel.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_inner_panel.tsx @@ -17,13 +17,9 @@ const ButtonContainer = styled(EuiFlexGroup)` padding: ${({ theme }) => theme.eui.paddingSizes.s}; `; -const Title = styled(EuiText)<{ textcolor: 'primary' | 'warning' | 'danger' }>` +const Title = styled(EuiText)<{ textcolor: 'primary' | 'warning' }>` color: ${({ theme, textcolor }) => - textcolor === 'primary' - ? theme.eui.euiColorPrimary - : textcolor === 'warning' - ? theme.eui.euiColorWarningText - : theme.eui.euiColorDangerText}; + textcolor === 'primary' ? theme.eui.euiColorPrimary : theme.eui.euiColorWarningText}; margin-bottom: ${({ theme }) => theme.eui.paddingSizes.m}; `; @@ -40,12 +36,12 @@ export const CtiInnerPanel = ({ body, button, }: { - color: 'primary' | 'warning' | 'danger'; + color: 'primary' | 'warning'; title: string; body: string; button?: JSX.Element; }) => { - const iconType = color === 'primary' ? 'iInCircle' : color === 'warning' ? 'help' : 'alert'; + const iconType = color === 'primary' ? 'iInCircle' : 'help'; return ( diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.test.tsx index 5e1697279dd4c..f00e1053e8082 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.test.tsx @@ -69,7 +69,7 @@ describe('CtiNoEvents', () => { ); expect(wrapper.find('[data-test-subj="cti-total-event-count"]').text()).toEqual( - 'Showing: 0 events' + 'Showing: 0 indicators' ); }); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.test.tsx index 3b03b9c418a1c..fac05bb72df38 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.test.tsx @@ -51,7 +51,7 @@ describe('CtiWithEvents', () => { ); expect(wrapper.find('[data-test-subj="cti-total-event-count"]').text()).toEqual( - `Showing: ${mockCtiWithEventsProps.totalCount} events` + `Showing: ${mockCtiWithEventsProps.totalCount} indicators` ); }); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.test.tsx index 59ee1e5447ba3..f0e3bcaaec6e0 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.test.tsx @@ -141,7 +141,7 @@ describe('ThreatIntelPanelView', () => { ); expect(wrapper.find('[data-test-subj="cti-total-event-count"]').text()).toEqual( - `Showing: ${mockThreatIntelPanelViewProps.totalEventCount} events` + `Showing: ${mockThreatIntelPanelViewProps.totalEventCount} indicators` ); }); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx index 4565c16bc2bf6..babbd5d13224f 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx @@ -22,10 +22,10 @@ import { InspectButtonContainer } from '../../../common/components/inspect'; import { HeaderSection } from '../../../common/components/header_section'; import { ID as CTIEventCountQueryId } from '../../containers/overview_cti_links/use_cti_event_counts'; import { CtiListItem } from '../../containers/overview_cti_links/helpers'; -import { LinkButton } from '../../../common/components/links'; import { useKibana } from '../../../common/lib/kibana'; import { CtiInnerPanel } from './cti_inner_panel'; import * as i18n from './translations'; +import { shortenCountIntoString } from '../../../common/utils/shorten_count_into_string'; const DashboardLink = styled.li` margin: 0 ${({ theme }) => theme.eui.paddingSizes.s} 0 ${({ theme }) => theme.eui.paddingSizes.m}; @@ -36,7 +36,7 @@ const DashboardLinkItems = styled(EuiFlexGroup)` `; const Title = styled(EuiFlexItem)` - min-width: 140px; + min-width: 110px; `; const List = styled.ul` @@ -45,12 +45,11 @@ const List = styled.ul` const DashboardRightSideElement = styled(EuiFlexItem)` align-items: flex-end; - max-width: 160px; `; const RightSideLink = styled(EuiLink)` text-align: right; - min-width: 140px; + min-width: 180px; `; interface ThreatIntelPanelViewProps { @@ -86,7 +85,7 @@ export const ThreatIntelPanelView: React.FC = ({ () => ( @@ -96,12 +95,12 @@ export const ThreatIntelPanelView: React.FC = ({ const button = useMemo( () => ( - + - + ), [buttonHref] ); @@ -117,7 +116,11 @@ export const ThreatIntelPanelView: React.FC = ({ color={'primary'} title={i18n.INFO_TITLE} body={i18n.INFO_BODY} - button={{i18n.INFO_BUTTON}} + button={ + + {i18n.INFO_BUTTON} + + } /> ) : null, [isDashboardPluginDisabled, threatIntelDashboardDocLink] @@ -149,9 +152,7 @@ export const ThreatIntelPanelView: React.FC = ({ gutterSize="l" justifyContent="spaceBetween" > - - {title} - + {title} = ({ alignItems="center" justifyContent="flexEnd" > - - {count} + + {shortenCountIntoString(count)} - + {path ? ( - {linkCopy} + + {linkCopy} + ) : ( {linkCopy} diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/translations.ts b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/translations.ts index 663ec3a75c902..91abd48eb2b7e 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/translations.ts +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/translations.ts @@ -51,9 +51,10 @@ export const DANGER_TITLE = i18n.translate( ); export const DANGER_BODY = i18n.translate( - 'xpack.securitySolution.overview.ctiDashboardDangerPanelBody', + 'xpack.securitySolution.overview.ctiDashboardEnableThreatIntel', { - defaultMessage: 'You need to enable module in order to view data from different sources.', + defaultMessage: + 'You need to enable the filebeat threatintel module in order to view data from different sources.', } ); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx index f0198092ec1be..7af9f84ad0875 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; import { OverviewEmpty } from '.'; -import { useIngestEnabledCheck } from '../../../common/hooks/endpoint/ingest_enabled'; +import { useUserPrivileges } from '../../../common/components/user_privileges'; const endpointPackageVersion = '0.19.1'; @@ -20,8 +20,10 @@ jest.mock('../../../management/pages/endpoint_hosts/view/hooks', () => ({ useEndpointSelector: jest.fn().mockReturnValue({ endpointPackageVersion }), })); -jest.mock('../../../common/hooks/endpoint/ingest_enabled', () => ({ - useIngestEnabledCheck: jest.fn().mockReturnValue({ allEnabled: true }), +jest.mock('../../../common/components/user_privileges', () => ({ + useUserPrivileges: jest + .fn() + .mockReturnValue({ endpointPrivileges: { loading: false, canAccessFleet: true } }), })); jest.mock('../../../common/hooks/endpoint/use_navigate_to_app_event_handler', () => ({ @@ -36,7 +38,7 @@ describe('OverviewEmpty', () => { }); afterAll(() => { - (useIngestEnabledCheck as jest.Mock).mockReset(); + (useUserPrivileges as jest.Mock).mockReset(); }); test('render with correct actions ', () => { @@ -70,7 +72,9 @@ describe('OverviewEmpty', () => { describe('When isIngestEnabled = false', () => { let wrapper: ShallowWrapper; beforeAll(() => { - (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: false }); + (useUserPrivileges as jest.Mock).mockReturnValue({ + endpointPrivileges: { loading: false, canAccessFleet: false }, + }); wrapper = shallow(); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx index 028871d7be19d..c75438e18f5d5 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx @@ -20,9 +20,9 @@ import { useIngestUrl, } from '../../../management/pages/endpoint_hosts/view/hooks'; import { useNavigateToAppEventHandler } from '../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; -import { useIngestEnabledCheck } from '../../../common/hooks/endpoint/ingest_enabled'; import { CreateStructuredSelector } from '../../../common/store'; import { endpointPackageVersion as useEndpointPackageVersion } from '../../../management/pages/endpoint_hosts/store/selectors'; +import { useUserPrivileges } from '../../../common/components/user_privileges'; const OverviewEmptyComponent: React.FC = () => { const { http, docLinks } = useKibana().services; @@ -40,7 +40,7 @@ const OverviewEmptyComponent: React.FC = () => { const handleEndpointClick = useNavigateToAppEventHandler('fleet', { path: endpointIntegrationUrl, }); - const { allEnabled: isIngestEnabled } = useIngestEnabledCheck(); + const canAccessFleet = useUserPrivileges().endpointPrivileges.canAccessFleet; const emptyPageActions: EmptyPageActionsProps = useMemo( () => ({ @@ -72,7 +72,7 @@ const OverviewEmptyComponent: React.FC = () => { [emptyPageActions] ); - return isIngestEnabled === true ? ( + return canAccessFleet === true ? ( { + const items = DashboardsSO.savedObjects + ?.reduce((acc: CtiListItem[], dashboardSO, i) => { const item = createLinkFromDashboardSO( dashboardSO, eventCountsByDataset, @@ -87,9 +87,8 @@ export const useCtiDashboardLinks = ( acc.push(item); } return acc; - }, - [] - ); + }, []) + .sort((a, b) => (a.title > b.title ? 1 : -1)); setListItems(items); } else { handleDisabledPlugin(); diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx index cc8c0bfcf7f61..d40f43c81aead 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx @@ -8,6 +8,7 @@ import { mount } from 'enzyme'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; +import { merge } from 'lodash'; import '../../common/mock/match_media'; import { TestProviders } from '../../common/mock'; @@ -16,7 +17,10 @@ import { UseMessagesStorage, } from '../../common/containers/local_storage/use_messages_storage'; import { Overview } from './index'; -import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enabled'; +import { + initialUserPrivilegesState, + useUserPrivileges, +} from '../../common/components/user_privileges'; import { useSourcererScope } from '../../common/containers/sourcerer'; import { useFetchIndex } from '../../common/containers/source'; import { useIsThreatIntelModuleEnabled } from '../containers/overview_cti_links/use_is_threat_intel_module_enabled'; @@ -26,6 +30,7 @@ import { mockCtiLinksResponse, } from '../components/overview_cti_links/mock'; import { useCtiDashboardLinks } from '../containers/overview_cti_links'; +import { EndpointPrivileges } from '../../common/components/user_privileges/use_endpoint_privileges'; jest.mock('../../common/lib/kibana'); jest.mock('../../common/containers/source'); @@ -47,12 +52,28 @@ jest.mock('../../common/components/search_bar', () => ({ jest.mock('../../common/components/query_bar', () => ({ QueryBar: () => null, })); -jest.mock('../../common/hooks/endpoint/ingest_enabled'); +jest.mock('../../common/components/user_privileges', () => { + return { + ...jest.requireActual('../../common/components/user_privileges'), + useUserPrivileges: jest.fn(() => { + return { + listPrivileges: { loading: false, error: undefined, result: undefined }, + detectionEnginePrivileges: { loading: false, error: undefined, result: undefined }, + endpointPrivileges: { + loading: false, + canAccessEndpointManagement: true, + canAccessFleet: true, + }, + }; + }), + }; +}); jest.mock('../../common/containers/local_storage/use_messages_storage'); jest.mock('../containers/overview_cti_links'); jest.mock('../containers/overview_cti_links/use_cti_event_counts'); jest.mock('../containers/overview_cti_links'); + const useCtiDashboardLinksMock = useCtiDashboardLinks as jest.Mock; useCtiDashboardLinksMock.mockReturnValue(mockCtiLinksResponse); @@ -74,12 +95,25 @@ const endpointNoticeMessage = (hasMessageValue: boolean) => { }; }; const mockUseSourcererScope = useSourcererScope as jest.Mock; -const mockUseIngestEnabledCheck = useIngestEnabledCheck as jest.Mock; +const mockUseUserPrivileges = useUserPrivileges as jest.Mock; const mockUseFetchIndex = useFetchIndex as jest.Mock; const mockUseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; describe('Overview', () => { + const loadedUserPrivilegesState = ( + endpointOverrides: Partial = {} + ): ReturnType => + merge(initialUserPrivilegesState(), { + endpointPrivileges: { + loading: false, + canAccessFleet: true, + canAccessEndpointManagement: true, + ...endpointOverrides, + }, + }); + beforeEach(() => { + mockUseUserPrivileges.mockReturnValue(loadedUserPrivilegesState()); mockUseFetchIndex.mockReturnValue([ false, { @@ -88,6 +122,10 @@ describe('Overview', () => { ]); }); + afterAll(() => { + mockUseUserPrivileges.mockReset(); + }); + describe('rendering', () => { test('it DOES NOT render the Getting started text when an index is available', () => { mockUseSourcererScope.mockReturnValue({ @@ -97,7 +135,6 @@ describe('Overview', () => { }); mockUseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); - mockUseIngestEnabledCheck.mockReturnValue({ allEnabled: true }); const wrapper = mount( @@ -125,7 +162,6 @@ describe('Overview', () => { }); mockUseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); - mockUseIngestEnabledCheck.mockReturnValue({ allEnabled: true }); const wrapper = mount( @@ -153,7 +189,6 @@ describe('Overview', () => { }); mockUseMessagesStorage.mockImplementation(() => endpointNoticeMessage(true)); - mockUseIngestEnabledCheck.mockReturnValue({ allEnabled: true }); const wrapper = mount( @@ -175,7 +210,6 @@ describe('Overview', () => { }); mockUseMessagesStorage.mockImplementation(() => endpointNoticeMessage(true)); - mockUseIngestEnabledCheck.mockReturnValue({ allEnabled: true }); const wrapper = mount( @@ -197,7 +231,6 @@ describe('Overview', () => { }); mockUseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); - mockUseIngestEnabledCheck.mockReturnValue({ allEnabled: true }); const wrapper = mount( @@ -219,7 +252,7 @@ describe('Overview', () => { }); mockUseMessagesStorage.mockImplementation(() => endpointNoticeMessage(true)); - mockUseIngestEnabledCheck.mockReturnValue({ allEnabled: false }); + mockUseUserPrivileges.mockReturnValue(loadedUserPrivilegesState({ canAccessFleet: false })); const wrapper = mount( @@ -239,7 +272,7 @@ describe('Overview', () => { selectedPatterns: [], indicesExist: false, }); - mockUseIngestEnabledCheck.mockReturnValue({ allEnabled: false }); + mockUseUserPrivileges.mockReturnValue(loadedUserPrivilegesState({ canAccessFleet: false })); mockUseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); }); @@ -266,7 +299,7 @@ describe('Overview', () => { }); it('shows Endpoint get ready button when ingest is enabled', () => { - mockUseIngestEnabledCheck.mockReturnValue({ allEnabled: true }); + mockUseUserPrivileges.mockReturnValue(loadedUserPrivilegesState({ canAccessFleet: true })); const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index 174141db9bfb1..ed12dce6db482 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -27,13 +27,13 @@ import { SecurityPageName } from '../../app/types'; import { EndpointNotice } from '../components/endpoint_notice'; import { useMessagesStorage } from '../../common/containers/local_storage/use_messages_storage'; import { ENDPOINT_METADATA_INDEX } from '../../../common/constants'; -import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enabled'; import { useSourcererScope } from '../../common/containers/sourcerer'; import { Sourcerer } from '../../common/components/sourcerer'; import { SourcererScopeName } from '../../common/store/sourcerer/model'; import { useDeepEqualSelector } from '../../common/hooks/use_selector'; import { ThreatIntelLinkPanel } from '../components/overview_cti_links'; import { useIsThreatIntelModuleEnabled } from '../containers/overview_cti_links/use_is_threat_intel_module_enabled'; +import { useUserPrivileges } from '../../common/components/user_privileges'; const SidebarFlexItem = styled(EuiFlexItem)` margin-right: 24px; @@ -70,9 +70,8 @@ const OverviewComponent = () => { setDismissMessage(true); addMessage('management', 'dismissEndpointNotice'); }, [addMessage]); - const { allEnabled: isIngestEnabled } = useIngestEnabledCheck(); + const canAccessFleet = useUserPrivileges().endpointPrivileges.canAccessFleet; const isThreatIntelModuleEnabled = useIsThreatIntelModuleEnabled(); - return ( <> {indicesExist ? ( @@ -82,7 +81,7 @@ const OverviewComponent = () => { - {!dismissMessage && !metadataIndexExists && isIngestEnabled && ( + {!dismissMessage && !metadataIndexExists && canAccessFleet && ( <> diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx index 3c9d9161a7a48..2afb2af01406d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx @@ -26,7 +26,7 @@ interface FlyoutPaneComponentProps { const StyledEuiFlyout = styled(EuiFlyout)` animation: none; min-width: 150px; - z-index: ${({ theme }) => theme.eui.euiZLevel6}; + z-index: ${({ theme }) => theme.eui.euiZLevel4}; `; const FlyoutPaneComponent: React.FC = ({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx index 2c88b305c7d05..dac10f4648784 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/agent_statuses.tsx @@ -6,11 +6,12 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { DefaultDraggable } from '../../../../../common/components/draggables'; import { EndpointHostIsolationStatus } from '../../../../../common/components/endpoint/host_isolation'; import { useHostIsolationStatus } from '../../../../../detections/containers/detection_engine/alerts/use_host_isolation_status'; import { AgentStatus } from '../../../../../common/components/endpoint/agent_status'; +import { EMPTY_STATUS } from './translations'; export const AgentStatuses = React.memo( ({ @@ -33,16 +34,22 @@ export const AgentStatuses = React.memo( const isolationFieldName = 'host.isolation'; return ( - - - - - + {agentStatus !== undefined ? ( + + + + + + ) : ( + +

{EMPTY_STATUS}

+
+ )} { wrapper.find('[data-test-subj="timelineSizeRowPopover"] button').first().simulate('click'); expect(wrapper.find('[data-test-subj="timelinePickSizeRow"]').exists()).toBeTruthy(); }); + + test('it renders last updated when updated at is > 0', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="fixed-width-last-updated"]').exists()).toBeTruthy(); + }); + + test('it does NOT render last updated when updated at is 0', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="fixed-width-last-updated"]').exists()).toBeFalsy(); + }); }); describe('Events', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx index b71cbb4c082ef..2a253087567a7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx @@ -45,11 +45,11 @@ const FixedWidthLastUpdatedContainer = React.memo isCompactFooter(width), [width]); - return ( + return updatedAt > 0 ? ( {timelines.getLastUpdated({ updatedAt, compact })} - ); + ) : null; } ); diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index d8e7a813c37c3..3ab0e6179f842 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -20,7 +20,6 @@ import { SecurityPluginStart } from '../../../security/server'; import { AgentService, FleetStartContract, - PackageService, AgentPolicyServiceInterface, PackagePolicyServiceInterface, } from '../../../fleet/server'; @@ -30,14 +29,6 @@ import { getPackagePolicyUpdateCallback, } from '../fleet_integration/fleet_integration'; import { ManifestManager } from './services/artifacts'; -import { MetadataQueryStrategy } from './types'; -import { MetadataQueryStrategyVersions } from '../../common/endpoint/types'; -import { - metadataQueryStrategyV1, - metadataQueryStrategyV2, -} from './routes/metadata/support/query_strategies'; -import { ElasticsearchAssetType } from '../../../fleet/common/types/models'; -import { metadataTransformPrefix } from '../../common/endpoint/constants'; import { AppClientFactory } from '../client'; import { ConfigType } from '../config'; import { LicenseService } from '../../common/license'; @@ -46,45 +37,6 @@ import { parseExperimentalConfigValue, } from '../../common/experimental_features'; -export interface MetadataService { - queryStrategy( - savedObjectsClient: SavedObjectsClientContract, - version?: MetadataQueryStrategyVersions - ): Promise; -} - -export const createMetadataService = (packageService: PackageService): MetadataService => { - return { - async queryStrategy( - savedObjectsClient: SavedObjectsClientContract, - version?: MetadataQueryStrategyVersions - ): Promise { - if (version === MetadataQueryStrategyVersions.VERSION_1) { - return metadataQueryStrategyV1(); - } - if (!packageService) { - throw new Error('package service is uninitialized'); - } - - if (version === MetadataQueryStrategyVersions.VERSION_2 || !version) { - const assets = - (await packageService.getInstallation({ savedObjectsClient, pkgName: 'endpoint' })) - ?.installed_es ?? []; - const expectedTransformAssets = assets.filter( - (ref) => - ref.type === ElasticsearchAssetType.transform && - ref.id.startsWith(metadataTransformPrefix) - ); - if (expectedTransformAssets && expectedTransformAssets.length === 1) { - return metadataQueryStrategyV2(); - } - return metadataQueryStrategyV1(); - } - return metadataQueryStrategyV1(); - }, - }; -}; - export type EndpointAppContextServiceStartContract = Partial< Pick< FleetStartContract, @@ -114,7 +66,6 @@ export class EndpointAppContextService { private packagePolicyService: PackagePolicyServiceInterface | undefined; private agentPolicyService: AgentPolicyServiceInterface | undefined; private savedObjectsStart: SavedObjectsServiceStart | undefined; - private metadataService: MetadataService | undefined; private config: ConfigType | undefined; private license: LicenseService | undefined; public security: SecurityPluginStart | undefined; @@ -128,7 +79,6 @@ export class EndpointAppContextService { this.agentPolicyService = dependencies.agentPolicyService; this.manifestManager = dependencies.manifestManager; this.savedObjectsStart = dependencies.savedObjectsStart; - this.metadataService = createMetadataService(dependencies.packageService!); this.config = dependencies.config; this.license = dependencies.licenseService; this.security = dependencies.security; @@ -176,10 +126,6 @@ export class EndpointAppContextService { return this.agentPolicyService; } - public getMetadataService(): MetadataService | undefined { - return this.metadataService; - } - public getManifestManager(): ManifestManager | undefined { return this.manifestManager; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts index c7f07151f8724..d9069444a10d7 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts @@ -60,6 +60,37 @@ describe('Action Log API', () => { }).not.toThrow(); }); + it('should work with all query params', () => { + expect(() => { + EndpointActionLogRequestSchema.query.validate({ + page: 10, + page_size: 100, + start_date: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), // yesterday + end_date: new Date().toISOString(), // today + }); + }).not.toThrow(); + }); + + it('should work with just startDate', () => { + expect(() => { + EndpointActionLogRequestSchema.query.validate({ + page: 1, + page_size: 100, + start_date: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), // yesterday + }); + }).not.toThrow(); + }); + + it('should work with just endDate', () => { + expect(() => { + EndpointActionLogRequestSchema.query.validate({ + page: 1, + page_size: 100, + end_date: new Date().toISOString(), // today + }); + }).not.toThrow(); + }); + it('should not work without allowed page and page_size params', () => { expect(() => { EndpointActionLogRequestSchema.query.validate({ page_size: 101 }); @@ -176,5 +207,20 @@ describe('Action Log API', () => { expect(error.message).toEqual(`Error fetching actions log for agent_id ${mockID}`); } }); + + it('should return date ranges if present in the query', async () => { + havingActionsAndResponses([], []); + const startDate = new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(); + const endDate = new Date().toISOString(); + const response = await getActivityLog({ + page: 1, + page_size: 50, + start_date: startDate, + end_date: endDate, + }); + expect(response.ok).toBeCalled(); + expect((response.ok.mock.calls[0][0]?.body as ActivityLog).startDate).toEqual(startDate); + expect((response.ok.mock.calls[0][0]?.body as ActivityLog).endDate).toEqual(endDate); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log_handler.ts index 5e9594f478b31..716c1ab833559 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log_handler.ts @@ -27,10 +27,18 @@ export const actionsLogRequestHandler = ( return async (context, req, res) => { const { params: { agent_id: elasticAgentId }, - query: { page, page_size: pageSize }, + query: { page, page_size: pageSize, start_date: startDate, end_date: endDate }, } = req; - const body = await getAuditLogResponse({ elasticAgentId, page, pageSize, context, logger }); + const body = await getAuditLogResponse({ + elasticAgentId, + page, + pageSize, + startDate, + endDate, + context, + logger, + }); return res.ok({ body, }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts index 45063ca92e2b0..fceb45b17c258 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts @@ -83,7 +83,7 @@ export const isolationRequestHandler = function ( // fetch the Agent IDs to send the commands to const endpointIDs = [...new Set(req.body.endpoint_ids)]; // dedupe - const endpointData = await getMetadataForEndpoints(endpointIDs, context, endpointContext); + const endpointData = await getMetadataForEndpoints(endpointIDs, context); const casesClient = await endpointContext.service.getCasesClient(req); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/enrichment.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/enrichment.test.ts index 960f3abda8195..39aa0bf2d8cf7 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/enrichment.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/enrichment.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { HostStatus, MetadataQueryStrategyVersions } from '../../../../common/endpoint/types'; +import { HostStatus } from '../../../../common/endpoint/types'; import { createMockMetadataRequestContext } from '../../mocks'; import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; import { enrichHostMetadata, MetadataRequestContext } from './handlers'; @@ -18,30 +18,6 @@ describe('test document enrichment', () => { metaReqCtx = createMockMetadataRequestContext(); }); - // verify query version passed through - describe('metadata query strategy enrichment', () => { - it('should match v1 strategy when directed', async () => { - const enrichedHostList = await enrichHostMetadata( - docGen.generateHostMetadata(), - metaReqCtx, - MetadataQueryStrategyVersions.VERSION_1 - ); - expect(enrichedHostList.query_strategy_version).toEqual( - MetadataQueryStrategyVersions.VERSION_1 - ); - }); - it('should match v2 strategy when directed', async () => { - const enrichedHostList = await enrichHostMetadata( - docGen.generateHostMetadata(), - metaReqCtx, - MetadataQueryStrategyVersions.VERSION_2 - ); - expect(enrichedHostList.query_strategy_version).toEqual( - MetadataQueryStrategyVersions.VERSION_2 - ); - }); - }); - describe('host status enrichment', () => { let statusFn: jest.Mock; @@ -57,77 +33,49 @@ describe('test document enrichment', () => { it('should return host healthy for online agent', async () => { statusFn.mockImplementation(() => 'online'); - const enrichedHostList = await enrichHostMetadata( - docGen.generateHostMetadata(), - metaReqCtx, - MetadataQueryStrategyVersions.VERSION_2 - ); + const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx); expect(enrichedHostList.host_status).toEqual(HostStatus.HEALTHY); }); it('should return host offline for offline agent', async () => { statusFn.mockImplementation(() => 'offline'); - const enrichedHostList = await enrichHostMetadata( - docGen.generateHostMetadata(), - metaReqCtx, - MetadataQueryStrategyVersions.VERSION_2 - ); + const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx); expect(enrichedHostList.host_status).toEqual(HostStatus.OFFLINE); }); it('should return host updating for unenrolling agent', async () => { statusFn.mockImplementation(() => 'unenrolling'); - const enrichedHostList = await enrichHostMetadata( - docGen.generateHostMetadata(), - metaReqCtx, - MetadataQueryStrategyVersions.VERSION_2 - ); + const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx); expect(enrichedHostList.host_status).toEqual(HostStatus.UPDATING); }); it('should return host unhealthy for degraded agent', async () => { statusFn.mockImplementation(() => 'degraded'); - const enrichedHostList = await enrichHostMetadata( - docGen.generateHostMetadata(), - metaReqCtx, - MetadataQueryStrategyVersions.VERSION_2 - ); + const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx); expect(enrichedHostList.host_status).toEqual(HostStatus.UNHEALTHY); }); it('should return host unhealthy for erroring agent', async () => { statusFn.mockImplementation(() => 'error'); - const enrichedHostList = await enrichHostMetadata( - docGen.generateHostMetadata(), - metaReqCtx, - MetadataQueryStrategyVersions.VERSION_2 - ); + const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx); expect(enrichedHostList.host_status).toEqual(HostStatus.UNHEALTHY); }); it('should return host unhealthy for warning agent', async () => { statusFn.mockImplementation(() => 'warning'); - const enrichedHostList = await enrichHostMetadata( - docGen.generateHostMetadata(), - metaReqCtx, - MetadataQueryStrategyVersions.VERSION_2 - ); + const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx); expect(enrichedHostList.host_status).toEqual(HostStatus.UNHEALTHY); }); it('should return host unhealthy for invalid agent', async () => { statusFn.mockImplementation(() => 'asliduasofb'); - const enrichedHostList = await enrichHostMetadata( - docGen.generateHostMetadata(), - metaReqCtx, - MetadataQueryStrategyVersions.VERSION_2 - ); + const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx); expect(enrichedHostList.host_status).toEqual(HostStatus.UNHEALTHY); }); }); @@ -164,11 +112,7 @@ describe('test document enrichment', () => { }; }); - const enrichedHostList = await enrichHostMetadata( - docGen.generateHostMetadata(), - metaReqCtx, - MetadataQueryStrategyVersions.VERSION_2 - ); + const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx); expect(enrichedHostList.policy_info).toBeDefined(); expect(enrichedHostList.policy_info!.agent.applied.id).toEqual(policyID); expect(enrichedHostList.policy_info!.agent.applied.revision).toEqual(policyRev); @@ -184,11 +128,7 @@ describe('test document enrichment', () => { }; }); - const enrichedHostList = await enrichHostMetadata( - docGen.generateHostMetadata(), - metaReqCtx, - MetadataQueryStrategyVersions.VERSION_2 - ); + const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx); expect(enrichedHostList.policy_info).toBeDefined(); expect(enrichedHostList.policy_info!.agent.configured.id).toEqual(policyID); expect(enrichedHostList.policy_info!.agent.configured.revision).toEqual(policyRev); @@ -209,11 +149,7 @@ describe('test document enrichment', () => { }; }); - const enrichedHostList = await enrichHostMetadata( - docGen.generateHostMetadata(), - metaReqCtx, - MetadataQueryStrategyVersions.VERSION_2 - ); + const enrichedHostList = await enrichHostMetadata(docGen.generateHostMetadata(), metaReqCtx); expect(enrichedHostList.policy_info).toBeDefined(); expect(enrichedHostList.policy_info!.endpoint.id).toEqual(policyID); expect(enrichedHostList.policy_info!.endpoint.revision).toEqual(policyRev); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts index 815f30e6e7426..2ceca170881e3 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts @@ -17,10 +17,8 @@ import { import { HostInfo, HostMetadata, - HostMetadataInfo, HostResultList, HostStatus, - MetadataQueryStrategyVersions, } from '../../../../common/endpoint/types'; import type { SecuritySolutionRequestHandlerContext } from '../../../types'; @@ -33,6 +31,10 @@ import { findAllUnenrolledAgentIds } from './support/unenroll'; import { findAgentIDsByStatus } from './support/agent_status'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { fleetAgentStatusToEndpointHostStatus } from '../../utils'; +import { + queryResponseToHostListResult, + queryResponseToHostResult, +} from './support/query_strategies'; export interface MetadataRequestContext { esClient?: IScopedClusterClient; @@ -58,8 +60,7 @@ export const getLogger = (endpointAppContext: EndpointAppContext): Logger => { export const getMetadataListRequestHandler = function ( endpointAppContext: EndpointAppContext, - logger: Logger, - queryStrategyVersion?: MetadataQueryStrategyVersions + logger: Logger ): RequestHandler< unknown, unknown, @@ -96,24 +97,15 @@ export const getMetadataListRequestHandler = function ( ) : undefined; - const queryStrategy = await endpointAppContext.service - ?.getMetadataService() - ?.queryStrategy(context.core.savedObjects.client, queryStrategyVersion); - - const queryParams = await kibanaRequestToMetadataListESQuery( - request, - endpointAppContext, - queryStrategy!, - { - unenrolledAgentIds: unenrolledAgentIds.concat(IGNORED_ELASTIC_AGENT_IDS), - statusAgentIDs: statusIDs, - } - ); + const queryParams = await kibanaRequestToMetadataListESQuery(request, endpointAppContext, { + unenrolledAgentIds: unenrolledAgentIds.concat(IGNORED_ELASTIC_AGENT_IDS), + statusAgentIDs: statusIDs, + }); const result = await context.core.elasticsearch.client.asCurrentUser.search( queryParams ); - const hostListQueryResult = queryStrategy!.queryResponseToHostListResult(result.body); + const hostListQueryResult = queryResponseToHostListResult(result.body); return response.ok({ body: await mapToHostResultList(queryParams, hostListQueryResult, metadataRequestContext), }); @@ -122,8 +114,7 @@ export const getMetadataListRequestHandler = function ( export const getMetadataRequestHandler = function ( endpointAppContext: EndpointAppContext, - logger: Logger, - queryStrategyVersion?: MetadataQueryStrategyVersions + logger: Logger ): RequestHandler< TypeOf, unknown, @@ -145,11 +136,7 @@ export const getMetadataRequestHandler = function ( }; try { - const doc = await getHostData( - metadataRequestContext, - request?.params?.id, - queryStrategyVersion - ); + const doc = await getHostData(metadataRequestContext, request?.params?.id); if (doc) { return response.ok({ body: doc }); } @@ -169,9 +156,8 @@ export const getMetadataRequestHandler = function ( export async function getHostMetaData( metadataRequestContext: MetadataRequestContext, - id: string, - queryStrategyVersion?: MetadataQueryStrategyVersions -): Promise { + id: string +): Promise { if ( !metadataRequestContext.esClient && !metadataRequestContext.requestHandlerContext?.core.elasticsearch.client @@ -190,32 +176,23 @@ export async function getHostMetaData( metadataRequestContext.requestHandlerContext?.core.elasticsearch .client) as IScopedClusterClient; - const esSavedObjectClient = - metadataRequestContext?.savedObjectsClient ?? - (metadataRequestContext.requestHandlerContext?.core.savedObjects - .client as SavedObjectsClientContract); - - const queryStrategy = await metadataRequestContext.endpointAppContextService - ?.getMetadataService() - ?.queryStrategy(esSavedObjectClient, queryStrategyVersion); - const query = getESQueryHostMetadataByID(id, queryStrategy!); + const query = getESQueryHostMetadataByID(id); const response = await esClient.asCurrentUser.search(query); - const hostResult = queryStrategy!.queryResponseToHostResult(response.body); + const hostResult = queryResponseToHostResult(response.body); const hostMetadata = hostResult.result; if (!hostMetadata) { return undefined; } - return { metadata: hostMetadata, query_strategy_version: hostResult.queryStrategyVersion }; + return hostMetadata; } export async function getHostData( metadataRequestContext: MetadataRequestContext, - id: string, - queryStrategyVersion?: MetadataQueryStrategyVersions + id: string ): Promise { if (!metadataRequestContext.savedObjectsClient) { throw Boom.badRequest('savedObjectsClient not found'); @@ -228,25 +205,21 @@ export async function getHostData( throw Boom.badRequest('esClient not found'); } - const hostResult = await getHostMetaData(metadataRequestContext, id, queryStrategyVersion); + const hostMetadata = await getHostMetaData(metadataRequestContext, id); - if (!hostResult) { + if (!hostMetadata) { return undefined; } - const agent = await findAgent(metadataRequestContext, hostResult.metadata); + const agent = await findAgent(metadataRequestContext, hostMetadata); if (agent && !agent.active) { throw Boom.badRequest('the requested endpoint is unenrolled'); } - const metadata = await enrichHostMetadata( - hostResult.metadata, - metadataRequestContext, - hostResult.query_strategy_version - ); + const metadata = await enrichHostMetadata(hostMetadata, metadataRequestContext); - return { ...metadata, query_strategy_version: hostResult.query_strategy_version }; + return metadata; } async function findAgent( @@ -293,15 +266,10 @@ export async function mapToHostResultList( request_page_index: queryParams.from, hosts: await Promise.all( hostListQueryResult.resultList.map(async (entry) => - enrichHostMetadata( - entry, - metadataRequestContext, - hostListQueryResult.queryStrategyVersion - ) + enrichHostMetadata(entry, metadataRequestContext) ) ), total: totalNumberOfHosts, - query_strategy_version: hostListQueryResult.queryStrategyVersion, }; } else { return { @@ -309,15 +277,13 @@ export async function mapToHostResultList( request_page_index: queryParams.from, total: totalNumberOfHosts, hosts: [], - query_strategy_version: hostListQueryResult.queryStrategyVersion, }; } } export async function enrichHostMetadata( hostMetadata: HostMetadata, - metadataRequestContext: MetadataRequestContext, - metadataQueryStrategyVersion: MetadataQueryStrategyVersions + metadataRequestContext: MetadataRequestContext ): Promise { let hostStatus = HostStatus.UNHEALTHY; let elasticAgentId = hostMetadata?.elastic?.agent?.id; @@ -413,6 +379,5 @@ export async function enrichHostMetadata( metadata: hostMetadata, host_status: hostStatus, policy_info: policyInfo, - query_strategy_version: metadataQueryStrategyVersion, }; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts index b4784c1ff5ed4..d9c3e6c195307 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts @@ -7,19 +7,15 @@ import { schema } from '@kbn/config-schema'; -import { HostStatus, MetadataQueryStrategyVersions } from '../../../../common/endpoint/types'; +import { HostStatus } from '../../../../common/endpoint/types'; import { EndpointAppContext } from '../../types'; import { getLogger, getMetadataListRequestHandler, getMetadataRequestHandler } from './handlers'; import type { SecuritySolutionPluginRouter } from '../../../types'; import { - BASE_ENDPOINT_ROUTE, HOST_METADATA_GET_ROUTE, HOST_METADATA_LIST_ROUTE, } from '../../../../common/endpoint/constants'; -export const METADATA_REQUEST_V1_ROUTE = `${BASE_ENDPOINT_ROUTE}/v1/metadata`; -export const GET_METADATA_REQUEST_V1_ROUTE = `${METADATA_REQUEST_V1_ROUTE}/{id}`; - /* Filters that can be applied to the endpoint fetch route */ export const endpointFilters = schema.object({ kql: schema.nullable(schema.string()), @@ -69,18 +65,6 @@ export function registerEndpointRoutes( endpointAppContext: EndpointAppContext ) { const logger = getLogger(endpointAppContext); - router.post( - { - path: `${METADATA_REQUEST_V1_ROUTE}`, - validate: GetMetadataListRequestSchema, - options: { authRequired: true, tags: ['access:securitySolution'] }, - }, - getMetadataListRequestHandler( - endpointAppContext, - logger, - MetadataQueryStrategyVersions.VERSION_1 - ) - ); router.post( { @@ -91,15 +75,6 @@ export function registerEndpointRoutes( getMetadataListRequestHandler(endpointAppContext, logger) ); - router.get( - { - path: `${GET_METADATA_REQUEST_V1_ROUTE}`, - validate: GetMetadataRequestSchema, - options: { authRequired: true, tags: ['access:securitySolution'] }, - }, - getMetadataRequestHandler(endpointAppContext, logger, MetadataQueryStrategyVersions.VERSION_1) - ); - router.get( { path: `${HOST_METADATA_GET_ROUTE}`, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index 5250f7c49d6ad..1e56f79aa0b32 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -19,12 +19,7 @@ import { loggingSystemMock, savedObjectsClientMock, } from '../../../../../../../src/core/server/mocks'; -import { - HostInfo, - HostResultList, - HostStatus, - MetadataQueryStrategyVersions, -} from '../../../../common/endpoint/types'; +import { HostInfo, HostResultList, HostStatus } from '../../../../common/endpoint/types'; import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; import { registerEndpointRoutes } from './index'; import { @@ -39,7 +34,7 @@ import { import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; import { Agent, ElasticsearchAssetType } from '../../../../../fleet/common/types/models'; -import { createV1SearchResponse, createV2SearchResponse } from './support/test_support'; +import { createV2SearchResponse } from './support/test_support'; import { PackageService } from '../../../../../fleet/server/services'; import { HOST_METADATA_LIST_ROUTE, @@ -98,94 +93,6 @@ describe('test endpoint route', () => { ); }); - describe('with no transform package', () => { - beforeEach(() => { - endpointAppContextService = new EndpointAppContextService(); - mockPackageService = createMockPackageService(); - mockPackageService.getInstallation.mockReturnValue(Promise.resolve(undefined)); - endpointAppContextService.start({ ...startContract, packageService: mockPackageService }); - mockAgentService = startContract.agentService!; - - registerEndpointRoutes(routerMock, { - logFactory: loggingSystemMock.create(), - service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), - }); - }); - - afterEach(() => endpointAppContextService.stop()); - - it('test find the latest of all endpoints', async () => { - const mockRequest = httpServerMock.createKibanaRequest({}); - const response = createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: response }) - ); - [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => - path.startsWith(`${HOST_METADATA_LIST_ROUTE}`) - )!; - mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); - await routeHandler( - createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), - mockRequest, - mockResponse - ); - - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); - expect(routeConfig.options).toEqual({ - authRequired: true, - tags: ['access:securitySolution'], - }); - expect(mockResponse.ok).toBeCalled(); - const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as HostResultList; - expect(endpointResultList.hosts.length).toEqual(1); - expect(endpointResultList.total).toEqual(1); - expect(endpointResultList.request_page_index).toEqual(0); - expect(endpointResultList.request_page_size).toEqual(10); - expect(endpointResultList.query_strategy_version).toEqual( - MetadataQueryStrategyVersions.VERSION_1 - ); - }); - - it('should return a single endpoint with status healthy', async () => { - const response = createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()); - const mockRequest = httpServerMock.createKibanaRequest({ - params: { id: response.hits.hits[0]._id }, - }); - - mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('online'); - mockAgentService.getAgent = jest.fn().mockReturnValue(({ - active: true, - } as unknown) as Agent); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: response }) - ); - - [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => - path.startsWith(`${HOST_METADATA_LIST_ROUTE}`) - )!; - - await routeHandler( - createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), - mockRequest, - mockResponse - ); - - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); - expect(routeConfig.options).toEqual({ - authRequired: true, - tags: ['access:securitySolution'], - }); - expect(mockResponse.ok).toBeCalled(); - const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; - expect(result).toHaveProperty('metadata.Endpoint'); - expect(result.host_status).toEqual(HostStatus.HEALTHY); - expect(result.query_strategy_version).toEqual(MetadataQueryStrategyVersions.VERSION_1); - }); - }); - describe('with new transform package', () => { beforeEach(() => { endpointAppContextService = new EndpointAppContextService(); @@ -254,9 +161,6 @@ describe('test endpoint route', () => { expect(endpointResultList.total).toEqual(1); expect(endpointResultList.request_page_index).toEqual(0); expect(endpointResultList.request_page_size).toEqual(10); - expect(endpointResultList.query_strategy_version).toEqual( - MetadataQueryStrategyVersions.VERSION_2 - ); }); it('test find the latest of all endpoints with paging properties', async () => { @@ -311,9 +215,6 @@ describe('test endpoint route', () => { expect(endpointResultList.total).toEqual(1); expect(endpointResultList.request_page_index).toEqual(10); expect(endpointResultList.request_page_size).toEqual(10); - expect(endpointResultList.query_strategy_version).toEqual( - MetadataQueryStrategyVersions.VERSION_2 - ); }); it('test find the latest of all endpoints with paging and filters properties', async () => { @@ -405,9 +306,6 @@ describe('test endpoint route', () => { expect(endpointResultList.total).toEqual(1); expect(endpointResultList.request_page_index).toEqual(10); expect(endpointResultList.request_page_size).toEqual(10); - expect(endpointResultList.query_strategy_version).toEqual( - MetadataQueryStrategyVersions.VERSION_2 - ); }); describe('Endpoint Details route', () => { @@ -475,7 +373,6 @@ describe('test endpoint route', () => { const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; expect(result).toHaveProperty('metadata.Endpoint'); expect(result.host_status).toEqual(HostStatus.HEALTHY); - expect(result.query_strategy_version).toEqual(MetadataQueryStrategyVersions.VERSION_2); }); it('should return a single endpoint with status unhealthy when AgentService throw 404', async () => { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata_v1.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata_v1.test.ts deleted file mode 100644 index 29b2c231cc4a5..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata_v1.test.ts +++ /dev/null @@ -1,456 +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 { - KibanaResponseFactory, - RequestHandler, - RouteConfig, - SavedObjectsClientContract, - SavedObjectsErrorHelpers, -} from '../../../../../../../src/core/server'; -import { - ClusterClientMock, - ScopedClusterClientMock, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../../src/core/server/elasticsearch/client/mocks'; -import { - elasticsearchServiceMock, - httpServerMock, - httpServiceMock, - loggingSystemMock, - savedObjectsClientMock, -} from '../../../../../../../src/core/server/mocks'; -import { - HostInfo, - HostResultList, - HostStatus, - MetadataQueryStrategyVersions, -} from '../../../../common/endpoint/types'; -import { registerEndpointRoutes, METADATA_REQUEST_V1_ROUTE } from './index'; -import { - createMockEndpointAppContextServiceStartContract, - createMockPackageService, - createRouteHandlerContext, -} from '../../mocks'; -import { - EndpointAppContextService, - EndpointAppContextServiceStartContract, -} from '../../endpoint_app_context_services'; -import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; -import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; -import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; -import { Agent } from '../../../../../fleet/common/types/models'; -import { createV1SearchResponse } from './support/test_support'; -import { PackageService } from '../../../../../fleet/server/services'; -import type { SecuritySolutionPluginRouter } from '../../../types'; -import { PackagePolicyServiceInterface } from '../../../../../fleet/server'; - -describe('test endpoint route v1', () => { - let routerMock: jest.Mocked; - let mockResponse: jest.Mocked; - let mockClusterClient: ClusterClientMock; - let mockScopedClient: ScopedClusterClientMock; - let mockSavedObjectClient: jest.Mocked; - let mockPackageService: jest.Mocked; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let routeHandler: RequestHandler; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let routeConfig: RouteConfig; - // tests assume that fleet is enabled, and thus agentService is available - let mockAgentService: Required< - ReturnType - >['agentService']; - let endpointAppContextService: EndpointAppContextService; - let startContract: EndpointAppContextServiceStartContract; - const noUnenrolledAgent = { - agents: [], - total: 0, - page: 1, - perPage: 1, - }; - - beforeEach(() => { - mockClusterClient = elasticsearchServiceMock.createClusterClient(); - mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); - mockSavedObjectClient = savedObjectsClientMock.create(); - mockClusterClient.asScoped.mockReturnValue(mockScopedClient); - routerMock = httpServiceMock.createRouter(); - mockResponse = httpServerMock.createResponseFactory(); - endpointAppContextService = new EndpointAppContextService(); - mockPackageService = createMockPackageService(); - mockPackageService.getInstallation.mockReturnValue(Promise.resolve(undefined)); - startContract = createMockEndpointAppContextServiceStartContract(); - endpointAppContextService.start({ ...startContract, packageService: mockPackageService }); - mockAgentService = startContract.agentService!; - - (startContract.packagePolicyService as jest.Mocked).list.mockImplementation( - () => { - return Promise.resolve({ - items: [], - total: 0, - page: 1, - perPage: 1000, - }); - } - ); - - registerEndpointRoutes(routerMock, { - logFactory: loggingSystemMock.create(), - service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), - }); - }); - - afterEach(() => endpointAppContextService.stop()); - - it('test find the latest of all endpoints', async () => { - const mockRequest = httpServerMock.createKibanaRequest({}); - const response = createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: response }) - ); - [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => - path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) - )!; - mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); - await routeHandler( - createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), - mockRequest, - mockResponse - ); - - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); - expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'] }); - expect(mockResponse.ok).toBeCalled(); - const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as HostResultList; - expect(endpointResultList.hosts.length).toEqual(1); - expect(endpointResultList.total).toEqual(1); - expect(endpointResultList.request_page_index).toEqual(0); - expect(endpointResultList.request_page_size).toEqual(10); - expect(endpointResultList.query_strategy_version).toEqual( - MetadataQueryStrategyVersions.VERSION_1 - ); - }); - - it('test find the latest of all endpoints with paging properties', async () => { - const mockRequest = httpServerMock.createKibanaRequest({ - body: { - paging_properties: [ - { - page_size: 10, - }, - { - page_index: 1, - }, - ], - }, - }); - - mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ - body: createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()), - }) - ); - [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => - path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) - )!; - - await routeHandler( - createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), - mockRequest, - mockResponse - ); - - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); - expect( - (mockScopedClient.asCurrentUser.search as jest.Mock).mock.calls[0][0]?.body?.query.bool - .must_not - ).toContainEqual({ - terms: { - 'elastic.agent.id': [ - '00000000-0000-0000-0000-000000000000', - '11111111-1111-1111-1111-111111111111', - ], - }, - }); - expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'] }); - expect(mockResponse.ok).toBeCalled(); - const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as HostResultList; - expect(endpointResultList.hosts.length).toEqual(1); - expect(endpointResultList.total).toEqual(1); - expect(endpointResultList.request_page_index).toEqual(10); - expect(endpointResultList.request_page_size).toEqual(10); - expect(endpointResultList.query_strategy_version).toEqual( - MetadataQueryStrategyVersions.VERSION_1 - ); - }); - - it('test find the latest of all endpoints with paging and filters properties', async () => { - const mockRequest = httpServerMock.createKibanaRequest({ - body: { - paging_properties: [ - { - page_size: 10, - }, - { - page_index: 1, - }, - ], - - filters: { kql: 'not host.ip:10.140.73.246' }, - }, - }); - - mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ - body: createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()), - }) - ); - [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => - path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) - )!; - - await routeHandler( - createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), - mockRequest, - mockResponse - ); - - expect(mockScopedClient.asCurrentUser.search).toBeCalled(); - // needs to have the KQL filter passed through - expect( - (mockScopedClient.asCurrentUser.search as jest.Mock).mock.calls[0][0]?.body?.query.bool.must - ).toContainEqual({ - bool: { - must_not: { - bool: { - should: [ - { - match: { - 'host.ip': '10.140.73.246', - }, - }, - ], - minimum_should_match: 1, - }, - }, - }, - }); - // and unenrolled should be filtered out. - expect( - (mockScopedClient.asCurrentUser.search as jest.Mock).mock.calls[0][0]?.body?.query.bool.must - ).toContainEqual({ - bool: { - must_not: [ - { - terms: { - 'elastic.agent.id': [ - '00000000-0000-0000-0000-000000000000', - '11111111-1111-1111-1111-111111111111', - ], - }, - }, - { - terms: { - // we actually don't care about HostDetails in v1 queries, but - // harder to set up the expectation to ignore its inclusion succinctly - 'HostDetails.elastic.agent.id': [ - '00000000-0000-0000-0000-000000000000', - '11111111-1111-1111-1111-111111111111', - ], - }, - }, - ], - }, - }); - expect(routeConfig.options).toEqual({ authRequired: true, tags: ['access:securitySolution'] }); - expect(mockResponse.ok).toBeCalled(); - const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as HostResultList; - expect(endpointResultList.hosts.length).toEqual(1); - expect(endpointResultList.total).toEqual(1); - expect(endpointResultList.request_page_index).toEqual(10); - expect(endpointResultList.request_page_size).toEqual(10); - expect(endpointResultList.query_strategy_version).toEqual( - MetadataQueryStrategyVersions.VERSION_1 - ); - }); - - describe('Endpoint Details route', () => { - it('should return 404 on no results', async () => { - const mockRequest = httpServerMock.createKibanaRequest({ params: { id: 'BADID' } }); - - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: createV1SearchResponse() }) - ); - - mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockAgentService.getAgent = jest.fn().mockReturnValue(({ - active: true, - } as unknown) as Agent); - - [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => - path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) - )!; - await routeHandler( - createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), - mockRequest, - mockResponse - ); - - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); - expect(routeConfig.options).toEqual({ - authRequired: true, - tags: ['access:securitySolution'], - }); - expect(mockResponse.notFound).toBeCalled(); - const message = mockResponse.notFound.mock.calls[0][0]?.body; - expect(message).toEqual('Endpoint Not Found'); - }); - - it('should return a single endpoint with status healthy', async () => { - const response = createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()); - const mockRequest = httpServerMock.createKibanaRequest({ - params: { id: response.hits.hits[0]._id }, - }); - - mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('online'); - mockAgentService.getAgent = jest.fn().mockReturnValue(({ - active: true, - } as unknown) as Agent); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: response }) - ); - - [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => - path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) - )!; - - await routeHandler( - createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), - mockRequest, - mockResponse - ); - - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); - expect(routeConfig.options).toEqual({ - authRequired: true, - tags: ['access:securitySolution'], - }); - expect(mockResponse.ok).toBeCalled(); - const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; - expect(result).toHaveProperty('metadata.Endpoint'); - expect(result.host_status).toEqual(HostStatus.HEALTHY); - }); - - it('should return a single endpoint with status unhealthy when AgentService throw 404', async () => { - const response = createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()); - - const mockRequest = httpServerMock.createKibanaRequest({ - params: { id: response.hits.hits[0]._id }, - }); - - mockAgentService.getAgentStatusById = jest.fn().mockImplementation(() => { - SavedObjectsErrorHelpers.createGenericNotFoundError(); - }); - - mockAgentService.getAgent = jest.fn().mockImplementation(() => { - SavedObjectsErrorHelpers.createGenericNotFoundError(); - }); - - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: response }) - ); - - [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => - path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) - )!; - - await routeHandler( - createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), - mockRequest, - mockResponse - ); - - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); - expect(routeConfig.options).toEqual({ - authRequired: true, - tags: ['access:securitySolution'], - }); - expect(mockResponse.ok).toBeCalled(); - const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; - expect(result.host_status).toEqual(HostStatus.UNHEALTHY); - }); - - it('should return a single endpoint with status unhealthy when status is not offline, online or enrolling', async () => { - const response = createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()); - - const mockRequest = httpServerMock.createKibanaRequest({ - params: { id: response.hits.hits[0]._id }, - }); - - mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('warning'); - mockAgentService.getAgent = jest.fn().mockReturnValue(({ - active: true, - } as unknown) as Agent); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: response }) - ); - - [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => - path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) - )!; - - await routeHandler( - createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), - mockRequest, - mockResponse - ); - - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); - expect(routeConfig.options).toEqual({ - authRequired: true, - tags: ['access:securitySolution'], - }); - expect(mockResponse.ok).toBeCalled(); - const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; - expect(result.host_status).toEqual(HostStatus.UNHEALTHY); - }); - - it('should throw error when endpoint agent is not active', async () => { - const response = createV1SearchResponse(new EndpointDocGenerator().generateHostMetadata()); - - const mockRequest = httpServerMock.createKibanaRequest({ - params: { id: response.hits.hits[0]._id }, - }); - (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ body: response }) - ); - mockAgentService.getAgent = jest.fn().mockReturnValue(({ - active: false, - } as unknown) as Agent); - - [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => - path.startsWith(`${METADATA_REQUEST_V1_ROUTE}`) - )!; - - await routeHandler( - createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), - mockRequest, - mockResponse - ); - - expect(mockScopedClient.asCurrentUser.search).toHaveBeenCalledTimes(1); - expect(mockResponse.customError).toBeCalled(); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts index e790c1de1a5b8..87de5a540ea99 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts @@ -11,38 +11,29 @@ import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; import { metadataCurrentIndexPattern } from '../../../../common/endpoint/constants'; import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; -import { metadataQueryStrategyV2 } from './support/query_strategies'; import { get } from 'lodash'; describe('query builder', () => { describe('MetadataListESQuery', () => { it('queries the correct index', async () => { const mockRequest = httpServerMock.createKibanaRequest({ body: {} }); - const query = await kibanaRequestToMetadataListESQuery( - mockRequest, - { - logFactory: loggingSystemMock.create(), - service: new EndpointAppContextService(), - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), - }, - metadataQueryStrategyV2() - ); + const query = await kibanaRequestToMetadataListESQuery(mockRequest, { + logFactory: loggingSystemMock.create(), + service: new EndpointAppContextService(), + config: () => Promise.resolve(createMockConfig()), + experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), + }); expect(query.index).toEqual(metadataCurrentIndexPattern); }); it('sorts using *event.created', async () => { const mockRequest = httpServerMock.createKibanaRequest({ body: {} }); - const query = await kibanaRequestToMetadataListESQuery( - mockRequest, - { - logFactory: loggingSystemMock.create(), - service: new EndpointAppContextService(), - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), - }, - metadataQueryStrategyV2() - ); + const query = await kibanaRequestToMetadataListESQuery(mockRequest, { + logFactory: loggingSystemMock.create(), + service: new EndpointAppContextService(), + config: () => Promise.resolve(createMockConfig()), + experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), + }); expect(query.body.sort).toContainEqual({ 'event.created': { order: 'desc', @@ -61,16 +52,12 @@ describe('query builder', () => { const mockRequest = httpServerMock.createKibanaRequest({ body: {}, }); - const query = await kibanaRequestToMetadataListESQuery( - mockRequest, - { - logFactory: loggingSystemMock.create(), - service: new EndpointAppContextService(), - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), - }, - metadataQueryStrategyV2() - ); + const query = await kibanaRequestToMetadataListESQuery(mockRequest, { + logFactory: loggingSystemMock.create(), + service: new EndpointAppContextService(), + config: () => Promise.resolve(createMockConfig()), + experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), + }); expect(query.body.query).toHaveProperty('match_all'); }); @@ -87,7 +74,6 @@ describe('query builder', () => { config: () => Promise.resolve(createMockConfig()), experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), }, - metadataQueryStrategyV2(), { unenrolledAgentIds: [unenrolledElasticAgentId], } @@ -111,16 +97,12 @@ describe('query builder', () => { filters: { kql: 'not host.ip:10.140.73.246' }, }, }); - const query = await kibanaRequestToMetadataListESQuery( - mockRequest, - { - logFactory: loggingSystemMock.create(), - service: new EndpointAppContextService(), - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), - }, - metadataQueryStrategyV2() - ); + const query = await kibanaRequestToMetadataListESQuery(mockRequest, { + logFactory: loggingSystemMock.create(), + service: new EndpointAppContextService(), + config: () => Promise.resolve(createMockConfig()), + experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), + }); expect(query.body.query.bool.must).toContainEqual({ bool: { @@ -160,7 +142,6 @@ describe('query builder', () => { createMockConfig().enableExperimental ), }, - metadataQueryStrategyV2(), { unenrolledAgentIds: [unenrolledElasticAgentId], } @@ -197,13 +178,13 @@ describe('query builder', () => { describe('MetadataGetQuery', () => { it('searches the correct index', () => { - const query = getESQueryHostMetadataByID('nonsense-id', metadataQueryStrategyV2()); + const query = getESQueryHostMetadataByID('nonsense-id'); expect(query.index).toEqual(metadataCurrentIndexPattern); }); it('searches for the correct ID', () => { const mockID = 'AABBCCDD-0011-2233-AA44-DEADBEEF8899'; - const query = getESQueryHostMetadataByID(mockID, metadataQueryStrategyV2()); + const query = getESQueryHostMetadataByID(mockID); expect(get(query, 'body.query.bool.filter.0.bool.should')).toContainEqual({ term: { 'agent.id': mockID }, @@ -212,7 +193,7 @@ describe('query builder', () => { it('supports HostDetails in schema for backwards compat', () => { const mockID = 'AABBCCDD-0011-2233-AA44-DEADBEEF8899'; - const query = getESQueryHostMetadataByID(mockID, metadataQueryStrategyV2()); + const query = getESQueryHostMetadataByID(mockID); expect(get(query, 'body.query.bool.filter.0.bool.should')).toContainEqual({ term: { 'HostDetails.agent.id': mockID }, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts index f0950e5fb79ba..99ec1d1022747 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts @@ -6,9 +6,10 @@ */ import type { estypes } from '@elastic/elasticsearch'; +import { metadataCurrentIndexPattern } from '../../../../common/endpoint/constants'; import { KibanaRequest } from '../../../../../../../src/core/server'; import { esKuery } from '../../../../../../../src/plugins/data/server'; -import { EndpointAppContext, MetadataQueryStrategy } from '../../types'; +import { EndpointAppContext } from '../../types'; export interface QueryBuilderOptions { unenrolledAgentIds?: string[]; @@ -39,7 +40,6 @@ export async function kibanaRequestToMetadataListESQuery( // eslint-disable-next-line @typescript-eslint/no-explicit-any request: KibanaRequest, endpointAppContext: EndpointAppContext, - metadataQueryStrategy: MetadataQueryStrategy, queryBuilderOptions?: QueryBuilderOptions // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Promise> { @@ -49,16 +49,15 @@ export async function kibanaRequestToMetadataListESQuery( body: { query: buildQueryBody( request, - metadataQueryStrategy, queryBuilderOptions?.unenrolledAgentIds!, queryBuilderOptions?.statusAgentIDs! ), - ...metadataQueryStrategy.extraBodyProperties, + track_total_hits: true, sort: MetadataSortMethod, }, from: pagingProperties.pageIndex * pagingProperties.pageSize, size: pagingProperties.pageSize, - index: metadataQueryStrategy.index, + index: metadataCurrentIndexPattern, }; } @@ -86,7 +85,6 @@ async function getPagingProperties( function buildQueryBody( // eslint-disable-next-line @typescript-eslint/no-explicit-any request: KibanaRequest, - metadataQueryStrategy: MetadataQueryStrategy, unerolledAgentIds: string[] | undefined, statusAgentIDs: string[] | undefined // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -144,10 +142,7 @@ function buildQueryBody( }; } -export function getESQueryHostMetadataByID( - agentID: string, - metadataQueryStrategy: MetadataQueryStrategy -): estypes.SearchRequest { +export function getESQueryHostMetadataByID(agentID: string): estypes.SearchRequest { return { body: { query: { @@ -167,14 +162,11 @@ export function getESQueryHostMetadataByID( sort: MetadataSortMethod, size: 1, }, - index: metadataQueryStrategy.index, + index: metadataCurrentIndexPattern, }; } -export function getESQueryHostMetadataByIDs( - agentIDs: string[], - metadataQueryStrategy: MetadataQueryStrategy -) { +export function getESQueryHostMetadataByIDs(agentIDs: string[]) { return { body: { query: { @@ -193,6 +185,6 @@ export function getESQueryHostMetadataByIDs( }, sort: MetadataSortMethod, }, - index: metadataQueryStrategy.index, + index: metadataCurrentIndexPattern, }; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders_v1.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders_v1.test.ts deleted file mode 100644 index c18c585cd3d34..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders_v1.test.ts +++ /dev/null @@ -1,188 +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 { httpServerMock, loggingSystemMock } from '../../../../../../../src/core/server/mocks'; -import { kibanaRequestToMetadataListESQuery, getESQueryHostMetadataByID } from './query_builders'; -import { EndpointAppContextService } from '../../endpoint_app_context_services'; -import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; -import { metadataIndexPattern } from '../../../../common/endpoint/constants'; -import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; -import { metadataQueryStrategyV1 } from './support/query_strategies'; -import { get } from 'lodash'; - -describe('query builder v1', () => { - describe('MetadataListESQuery', () => { - it('test default query params for all endpoints metadata when no params or body is provided', async () => { - const mockRequest = httpServerMock.createKibanaRequest({ - body: {}, - }); - const query = await kibanaRequestToMetadataListESQuery( - mockRequest, - { - logFactory: loggingSystemMock.create(), - service: new EndpointAppContextService(), - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), - }, - metadataQueryStrategyV1() - ); - - expect(query.body.query).toHaveProperty('match_all'); // no filtering - expect(query.body.collapse).toEqual({ - field: 'agent.id', - inner_hits: { - name: 'most_recent', - size: 1, - sort: [{ 'event.created': 'desc' }], - }, - }); - expect(query.body.aggs).toEqual({ - total: { - cardinality: { - field: 'agent.id', - }, - }, - }); - expect(query.index).toEqual(metadataIndexPattern); - }); - - it( - 'test default query params for all endpoints metadata when no params or body is provided ' + - 'with unenrolled host ids excluded', - async () => { - const unenrolledElasticAgentId = '1fdca33f-799f-49f4-939c-ea4383c77672'; - const mockRequest = httpServerMock.createKibanaRequest({ - body: {}, - }); - const query = await kibanaRequestToMetadataListESQuery( - mockRequest, - { - logFactory: loggingSystemMock.create(), - service: new EndpointAppContextService(), - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue( - createMockConfig().enableExperimental - ), - }, - metadataQueryStrategyV1(), - { - unenrolledAgentIds: [unenrolledElasticAgentId], - } - ); - expect(Object.keys(query.body.query.bool)).toEqual(['must_not']); // only filtering out unenrolled - expect(query.body.query.bool.must_not).toContainEqual({ - terms: { 'elastic.agent.id': [unenrolledElasticAgentId] }, - }); - } - ); - }); - - describe('test query builder with kql filter', () => { - it('test default query params for all endpoints metadata when body filter is provided', async () => { - const mockRequest = httpServerMock.createKibanaRequest({ - body: { - filters: { kql: 'not host.ip:10.140.73.246' }, - }, - }); - const query = await kibanaRequestToMetadataListESQuery( - mockRequest, - { - logFactory: loggingSystemMock.create(), - service: new EndpointAppContextService(), - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), - }, - metadataQueryStrategyV1() - ); - expect(query.body.query.bool.must).toHaveLength(1); // should not be any other filtering happening - expect(query.body.query.bool.must).toContainEqual({ - bool: { - must_not: { - bool: { - should: [ - { - match: { - 'host.ip': '10.140.73.246', - }, - }, - ], - minimum_should_match: 1, - }, - }, - }, - }); - }); - - it( - 'test default query params for all endpoints endpoint metadata excluding unerolled endpoint ' + - 'and when body filter is provided', - async () => { - const unenrolledElasticAgentId = '1fdca33f-799f-49f4-939c-ea4383c77672'; - const mockRequest = httpServerMock.createKibanaRequest({ - body: { - filters: { kql: 'not host.ip:10.140.73.246' }, - }, - }); - const query = await kibanaRequestToMetadataListESQuery( - mockRequest, - { - logFactory: loggingSystemMock.create(), - service: new EndpointAppContextService(), - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue( - createMockConfig().enableExperimental - ), - }, - metadataQueryStrategyV1(), - { - unenrolledAgentIds: [unenrolledElasticAgentId], - } - ); - - expect(query.body.query.bool.must.length).toBeGreaterThan(1); - // unenrollment filter should be there - expect(query.body.query.bool.must).toContainEqual({ - bool: { - must_not: [ - { terms: { 'elastic.agent.id': [unenrolledElasticAgentId] } }, - // below is not actually necessary behavior for v1, but hard to structure the test to ignore it - { terms: { 'HostDetails.elastic.agent.id': [unenrolledElasticAgentId] } }, - ], - }, - }); - // and KQL should also be there - expect(query.body.query.bool.must).toContainEqual({ - bool: { - must_not: { - bool: { - should: [ - { - match: { - 'host.ip': '10.140.73.246', - }, - }, - ], - minimum_should_match: 1, - }, - }, - }, - }); - } - ); - }); - - describe('MetadataGetQuery', () => { - it('searches for the correct ID', () => { - const mockID = 'AABBCCDD-0011-2233-AA44-DEADBEEF8899'; - const query = getESQueryHostMetadataByID(mockID, metadataQueryStrategyV1()); - - expect(get(query, 'body.query.bool.filter.0.bool.should')).toContainEqual({ - term: { 'agent.id': mockID }, - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/query_strategies.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/query_strategies.ts index 506c02fc2f1ec..2d7bff4a53f3f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/query_strategies.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/query_strategies.ts @@ -6,102 +6,39 @@ */ import { SearchResponse } from '@elastic/elasticsearch/api/types'; -import { - metadataCurrentIndexPattern, - metadataIndexPattern, -} from '../../../../../common/endpoint/constants'; -import { HostMetadata, MetadataQueryStrategyVersions } from '../../../../../common/endpoint/types'; -import { HostListQueryResult, HostQueryResult, MetadataQueryStrategy } from '../../../types'; +import { HostMetadata } from '../../../../../common/endpoint/types'; +import { HostListQueryResult, HostQueryResult } from '../../../types'; -export function metadataQueryStrategyV1(): MetadataQueryStrategy { - return { - index: metadataIndexPattern, - extraBodyProperties: { - collapse: { - field: 'agent.id', - inner_hits: { - name: 'most_recent', - size: 1, - sort: [{ 'event.created': 'desc' }], - }, - }, - aggs: { - total: { - cardinality: { - field: 'agent.id', - }, - }, - }, - }, - queryResponseToHostListResult: ( - searchResponse: SearchResponse - ): HostListQueryResult => { - const response = searchResponse as SearchResponse; - return { - resultLength: - ((response?.aggregations?.total as unknown) as { value?: number; relation: string }) - ?.value || 0, - resultList: response.hits.hits - .map((hit) => hit.inner_hits?.most_recent.hits.hits) - .flatMap((data) => data) - .map((entry) => (entry?._source ?? {}) as HostMetadata), - queryStrategyVersion: MetadataQueryStrategyVersions.VERSION_1, - }; - }, - queryResponseToHostResult: (searchResponse: SearchResponse): HostQueryResult => { - const response = searchResponse as SearchResponse; - return { - resultLength: response.hits.hits.length, - result: response.hits.hits.length > 0 ? response.hits.hits[0]._source : undefined, - queryStrategyVersion: MetadataQueryStrategyVersions.VERSION_1, - }; - }, - }; +// remove the top-level 'HostDetails' property if found, from previous schemas +function stripHostDetails(host: HostMetadata | { HostDetails: HostMetadata }): HostMetadata { + return 'HostDetails' in host ? host.HostDetails : host; } -export function metadataQueryStrategyV2(): MetadataQueryStrategy { +export const queryResponseToHostResult = ( + searchResponse: SearchResponse +): HostQueryResult => { + const response = searchResponse as SearchResponse; return { - index: metadataCurrentIndexPattern, - extraBodyProperties: { - track_total_hits: true, - }, - queryResponseToHostListResult: ( - searchResponse: SearchResponse - ): HostListQueryResult => { - const response = searchResponse as SearchResponse< - HostMetadata | { HostDetails: HostMetadata } - >; - const list = - response.hits.hits.length > 0 - ? response.hits.hits.map((entry) => stripHostDetails(entry?._source as HostMetadata)) - : []; - - return { - resultLength: - ((response.hits?.total as unknown) as { value: number; relation: string }).value || 0, - resultList: list, - queryStrategyVersion: MetadataQueryStrategyVersions.VERSION_2, - }; - }, - queryResponseToHostResult: ( - searchResponse: SearchResponse - ): HostQueryResult => { - const response = searchResponse as SearchResponse< - HostMetadata | { HostDetails: HostMetadata } - >; - return { - resultLength: response.hits.hits.length, - result: - response.hits.hits.length > 0 - ? stripHostDetails(response.hits.hits[0]._source as HostMetadata) - : undefined, - queryStrategyVersion: MetadataQueryStrategyVersions.VERSION_2, - }; - }, + resultLength: response.hits.hits.length, + result: + response.hits.hits.length > 0 + ? stripHostDetails(response.hits.hits[0]._source as HostMetadata) + : undefined, }; -} +}; -// remove the top-level 'HostDetails' property if found, from previous schemas -function stripHostDetails(host: HostMetadata | { HostDetails: HostMetadata }): HostMetadata { - return 'HostDetails' in host ? host.HostDetails : host; -} +export const queryResponseToHostListResult = ( + searchResponse: SearchResponse +): HostListQueryResult => { + const response = searchResponse as SearchResponse; + const list = + response.hits.hits.length > 0 + ? response.hits.hits.map((entry) => stripHostDetails(entry?._source as HostMetadata)) + : []; + + return { + resultLength: + ((response.hits?.total as unknown) as { value: number; relation: string }).value || 0, + resultList: list, + }; +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/test_support.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/test_support.ts index bc23c253c4347..a0530590f5f9f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/test_support.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/test_support.ts @@ -8,62 +8,6 @@ import { SearchResponse } from 'elasticsearch'; import { HostMetadata } from '../../../../../common/endpoint/types'; -export function createV1SearchResponse(hostMetadata?: HostMetadata): SearchResponse { - return ({ - took: 15, - timed_out: false, - _shards: { - total: 1, - successful: 1, - skipped: 0, - failed: 0, - }, - hits: { - total: { - value: 5, - relation: 'eq', - }, - max_score: null, - hits: hostMetadata - ? [ - { - _index: 'metrics-endpoint.metadata-default', - _id: '8FhM0HEBYyRTvb6lOQnw', - _score: null, - _source: hostMetadata, - sort: [1588337587997], - inner_hits: { - most_recent: { - hits: { - total: { - value: 2, - relation: 'eq', - }, - max_score: null, - hits: [ - { - _index: 'metrics-endpoint.metadata-default', - _id: 'W6Vo1G8BYQH1gtPUgYkC', - _score: null, - _source: hostMetadata, - sort: [1579816615336], - }, - ], - }, - }, - }, - }, - ] - : [], - }, - aggregations: { - total: { - value: 1, - }, - }, - } as unknown) as SearchResponse; -} - export function createV2SearchResponse(hostMetadata?: HostMetadata): SearchResponse { return ({ took: 15, diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions.ts index 9d8db5b9a2154..89f088e322ffa 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions.ts @@ -19,28 +19,37 @@ export const getAuditLogResponse = async ({ elasticAgentId, page, pageSize, + startDate, + endDate, context, logger, }: { elasticAgentId: string; page: number; pageSize: number; + startDate?: string; + endDate?: string; context: SecuritySolutionRequestHandlerContext; logger: Logger; -}): Promise<{ - page: number; - pageSize: number; - data: ActivityLog['data']; -}> => { +}): Promise => { const size = Math.floor(pageSize / 2); const from = page <= 1 ? 0 : page * size - size + 1; const esClient = context.core.elasticsearch.client.asCurrentUser; - - const data = await getActivityLog({ esClient, from, size, elasticAgentId, logger }); + const data = await getActivityLog({ + esClient, + from, + size, + startDate, + endDate, + elasticAgentId, + logger, + }); return { page, pageSize, + startDate, + endDate, data, }; }; @@ -49,6 +58,8 @@ const getActivityLog = async ({ esClient, size, from, + startDate, + endDate, elasticAgentId, logger, }: { @@ -56,6 +67,8 @@ const getActivityLog = async ({ elasticAgentId: string; size: number; from: number; + startDate?: string; + endDate?: string; logger: Logger; }) => { const options = { @@ -67,8 +80,22 @@ const getActivityLog = async ({ let actionsResult; let responsesResult; + const dateFilters = []; + if (startDate) { + dateFilters.push({ range: { '@timestamp': { gte: startDate } } }); + } + if (endDate) { + dateFilters.push({ range: { '@timestamp': { lte: endDate } } }); + } try { + // fetch actions with matching agent_id + const baseActionFilters = [ + { term: { agents: elasticAgentId } }, + { term: { input_type: 'endpoint' } }, + { term: { type: 'INPUT_ACTION' } }, + ]; + const actionsFilters = [...baseActionFilters, ...dateFilters]; actionsResult = await esClient.search( { index: AGENT_ACTIONS_INDEX, @@ -77,11 +104,8 @@ const getActivityLog = async ({ body: { query: { bool: { - filter: [ - { term: { agents: elasticAgentId } }, - { term: { input_type: 'endpoint' } }, - { term: { type: 'INPUT_ACTION' } }, - ], + // @ts-ignore + filter: actionsFilters, }, }, sort: [ @@ -99,6 +123,12 @@ const getActivityLog = async ({ (e) => (e._source as EndpointAction).action_id ); + // fetch responses with matching `action_id`s + const baseResponsesFilter = [ + { term: { agent_id: elasticAgentId } }, + { terms: { action_id: actionIds } }, + ]; + const responsesFilters = [...baseResponsesFilter, ...dateFilters]; responsesResult = await esClient.search( { index: AGENT_ACTIONS_RESULTS_INDEX, @@ -106,7 +136,7 @@ const getActivityLog = async ({ body: { query: { bool: { - filter: [{ term: { agent_id: elasticAgentId } }, { terms: { action_id: actionIds } }], + filter: responsesFilters, }, }, }, diff --git a/x-pack/plugins/security_solution/server/endpoint/services/metadata.ts b/x-pack/plugins/security_solution/server/endpoint/services/metadata.ts index 0ca1983aa68d5..1a5515d8122f1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/metadata.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/metadata.ts @@ -10,20 +10,15 @@ import { SearchResponse } from 'elasticsearch'; import { HostMetadata } from '../../../common/endpoint/types'; import { SecuritySolutionRequestHandlerContext } from '../../types'; import { getESQueryHostMetadataByIDs } from '../routes/metadata/query_builders'; -import { EndpointAppContext } from '../types'; +import { queryResponseToHostListResult } from '../routes/metadata/support/query_strategies'; export async function getMetadataForEndpoints( endpointIDs: string[], - requestHandlerContext: SecuritySolutionRequestHandlerContext, - endpointAppContext: EndpointAppContext + requestHandlerContext: SecuritySolutionRequestHandlerContext ): Promise { - const queryStrategy = await endpointAppContext.service - ?.getMetadataService() - ?.queryStrategy(requestHandlerContext.core.savedObjects.client); - - const query = getESQueryHostMetadataByIDs(endpointIDs, queryStrategy!); + const query = getESQueryHostMetadataByIDs(endpointIDs); const esClient = requestHandlerContext.core.elasticsearch.client.asCurrentUser; const { body } = await esClient.search(query as SearchRequest); - const hosts = queryStrategy!.queryResponseToHostListResult(body as SearchResponse); + const hosts = queryResponseToHostListResult(body as SearchResponse); return hosts.resultList; } diff --git a/x-pack/plugins/security_solution/server/endpoint/types.ts b/x-pack/plugins/security_solution/server/endpoint/types.ts index 6076aa9af635b..bc52b759b9f0a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/types.ts +++ b/x-pack/plugins/security_solution/server/endpoint/types.ts @@ -7,11 +7,9 @@ import { LoggerFactory } from 'kibana/server'; -import { SearchResponse } from '@elastic/elasticsearch/api/types'; -import { JsonObject } from '@kbn/common-utils'; import { ConfigType } from '../config'; import { EndpointAppContextService } from './endpoint_app_context_services'; -import { HostMetadata, MetadataQueryStrategyVersions } from '../../common/endpoint/types'; +import { HostMetadata } from '../../common/endpoint/types'; import { ExperimentalFeatures } from '../../common/experimental_features'; /** @@ -31,20 +29,9 @@ export interface EndpointAppContext { export interface HostListQueryResult { resultLength: number; resultList: HostMetadata[]; - queryStrategyVersion: MetadataQueryStrategyVersions; } export interface HostQueryResult { resultLength: number; result: HostMetadata | undefined; - queryStrategyVersion: MetadataQueryStrategyVersions; -} - -export interface MetadataQueryStrategy { - index: string; - extraBodyProperties?: JsonObject; - queryResponseToHostListResult: ( - searchResponse: SearchResponse - ) => HostListQueryResult; - queryResponseToHostResult: (searchResponse: SearchResponse) => HostQueryResult; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.test.ts index a871c7157d5e8..93fddc06b8068 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.test.ts @@ -33,8 +33,17 @@ describe('add_tags', () => { const tags2 = addTags(tags1, 'rule-1', false); expect(tags2).toEqual([ 'tag-1', + `${INTERNAL_RULE_ID_KEY}:rule-1`, `${INTERNAL_IMMUTABLE_KEY}:false`, + ]); + }); + + test('it should overwrite existing immutable tag if it exists', () => { + const tags1 = addTags(['tag-1', `${INTERNAL_IMMUTABLE_KEY}:true`], 'rule-1', false); + expect(tags1).toEqual([ + 'tag-1', `${INTERNAL_RULE_ID_KEY}:rule-1`, + `${INTERNAL_IMMUTABLE_KEY}:false`, ]); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.ts index 6ff4a54ad8e54..d66f961b38598 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.ts @@ -10,7 +10,9 @@ import { INTERNAL_RULE_ID_KEY, INTERNAL_IMMUTABLE_KEY } from '../../../../common export const addTags = (tags: string[], ruleId: string, immutable: boolean): string[] => { return Array.from( new Set([ - ...tags.filter((tag) => !tag.startsWith(INTERNAL_RULE_ID_KEY)), + ...tags.filter( + (tag) => !(tag.startsWith(INTERNAL_RULE_ID_KEY) || tag.startsWith(INTERNAL_IMMUTABLE_KEY)) + ), `${INTERNAL_RULE_ID_KEY}:${ruleId}`, `${INTERNAL_IMMUTABLE_KEY}:${immutable}`, ]) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.test.ts index 3046999a632c6..92b4dcff61b35 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.test.ts @@ -123,8 +123,8 @@ describe('duplicateRule', () => { }, "tags": Array [ "test", - "__internal_immutable:false", "__internal_rule_id:newId", + "__internal_immutable:false", ], "throttle": null, } diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 4389b22611748..a8ad6c919a04d 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -238,6 +238,7 @@ export class Plugin implements IPlugin initializeRuleDataTemplatesPromise ); @@ -338,7 +339,7 @@ export class Plugin implements IPlugin { } return this.getButton( - }> + }> , (activeSpace as Space).name diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts index 1e26ea09618d5..37e0a293b03a0 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts @@ -151,7 +151,14 @@ export async function executeEsQueryFactory( }, }, ], - docvalue_fields: [entity, dateField, geoField], + docvalue_fields: [ + entity, + { + field: dateField, + format: 'strict_date_optional_time', + }, + geoField, + ], _source: false, }, }, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts index 754af920b009e..21a536dd474ba 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts @@ -103,7 +103,7 @@ export function getActiveEntriesAndGenerateAlerts( locationsArr.forEach(({ location, shapeLocationId, dateInShape, docId }) => { const context = { entityId: entityName, - entityDateTime: dateInShape ? new Date(dateInShape).toISOString() : null, + entityDateTime: dateInShape || null, entityDocumentId: docId, detectionDateTime: new Date(currIntervalEndTime).toISOString(), entityLocation: `POINT (${location[0]} ${location[1]})`, diff --git a/x-pack/plugins/timelines/.i18nrc.json b/x-pack/plugins/timelines/.i18nrc.json deleted file mode 100644 index 4fe01ccc7bc69..0000000000000 --- a/x-pack/plugins/timelines/.i18nrc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "prefix": "timelines", - "paths": { - "timelines": "." - }, - "translations": ["translations/ja-JP.json"] -} diff --git a/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx b/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx index 8aecf403186c5..91df8d5eb4fbe 100644 --- a/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx +++ b/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx @@ -131,7 +131,10 @@ export const CloneTransformSection: FC = ({ match, location }) => { - + {typeof errorMessage !== 'undefined' && ( <> = React.memo( {created && ( - - + + } title={i18n.translate('xpack.transform.stepCreateForm.transformListCardTitle', { @@ -498,7 +497,7 @@ export const StepCreateForm: FC = React.memo( /> {started === true && createIndexPattern === true && indexPatternId === undefined && ( - + @@ -515,7 +514,7 @@ export const StepCreateForm: FC = React.memo( )} {isDiscoverAvailable && discoverLink !== undefined && ( - + } title={i18n.translate('xpack.transform.stepCreateForm.discoverCardTitle', { @@ -532,7 +531,7 @@ export const StepCreateForm: FC = React.memo( /> )} - +
)} diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/_wizard.scss b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/_wizard.scss index 1b493e9e74490..2fb415f8ab2d9 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/_wizard.scss +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/_wizard.scss @@ -8,22 +8,11 @@ } } -.transform__stepDefineForm { - align-items: flex-start; -} - -.transform__stepDefineFormLeftColumn { - min-width: 420px; - border-right: 1px solid $euiColorLightShade; -} - /* -This is an override to replicate the previous full-page-width of the transforms creation wizard -when it was in use within the ML plugin. The Kibana management section limits a max-width to 1200px -which is a bit narrow for the two column layout of the transform wizard. We might revisit this for -future versions to blend in more with the overall design of the Kibana management section. -The management section's navigation width is 192px + 24px right margin +This ensures the wizard goes full page width, and that the data grid in the page does not +cause the body of the wizard page to overflow into the side navigation of the Kibana +Stack Management page on resize. */ -.mgtPage__body--transformWizard { - max-width: calc(100% - 216px); +.transform__wizardBody { + max-width: calc(100% - 16px); } diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx index 5ae464affa016..63e21e5d8aa14 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, FC, useEffect, useRef, useState, createContext, useMemo } from 'react'; +import React, { Fragment, FC, useRef, useState, createContext, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; @@ -34,11 +34,6 @@ import { WizardNav } from '../wizard_nav'; import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; import type { RuntimeMappings } from '../step_define/common/types'; -enum KBN_MANAGEMENT_PAGE_CLASSNAME { - DEFAULT_BODY = 'mgtPage__body', - TRANSFORM_BODY_MODIFIER = 'mgtPage__body--transformWizard', -} - enum WIZARD_STEPS { DEFINE, DETAILS, @@ -121,34 +116,6 @@ export const Wizard: FC = React.memo(({ cloneConfig, searchItems }) // The CREATE state const [stepCreateState, setStepCreateState] = useState(getDefaultStepCreateState); - useEffect(() => { - // The transform plugin doesn't control the wrapping management page via React - // so we use plain JS to add and remove a custom CSS class to set the full - // page width to 100% for the transform wizard. It's done to replicate the layout - // as it was when transforms were part of the ML plugin. This will be revisited - // to come up with an approach that's more in line with the overall layout - // of the Kibana management section. - let managementBody = document.getElementsByClassName( - KBN_MANAGEMENT_PAGE_CLASSNAME.DEFAULT_BODY - ); - - if (managementBody.length > 0) { - managementBody[0].classList.replace( - KBN_MANAGEMENT_PAGE_CLASSNAME.DEFAULT_BODY, - KBN_MANAGEMENT_PAGE_CLASSNAME.TRANSFORM_BODY_MODIFIER - ); - return () => { - managementBody = document.getElementsByClassName( - KBN_MANAGEMENT_PAGE_CLASSNAME.TRANSFORM_BODY_MODIFIER - ); - managementBody[0].classList.replace( - KBN_MANAGEMENT_PAGE_CLASSNAME.TRANSFORM_BODY_MODIFIER, - KBN_MANAGEMENT_PAGE_CLASSNAME.DEFAULT_BODY - ); - }; - } - }, []); - const transformConfig = getCreateTransformRequestBody( indexPattern.title, stepDefineState, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx index d736bd60f2df6..4cb9ec926c049 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx @@ -68,7 +68,10 @@ export const CreateTransformSection: FC = ({ match }) => { - + {searchItemsError !== undefined && ( <> diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3c350c110be88..be3eff6fa9138 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5925,7 +5925,6 @@ "xpack.apm.transactionsTable.notFoundLabel": "トランザクションが見つかりませんでした。", "xpack.apm.transactionsTable.throughputColumnLabel": "スループット", "xpack.apm.tutorial.apmServer.title": "APM Server", - "xpack.apm.tutorial.elasticCloud.textPre": "APM Server を有効にするには、[the Elastic Cloud console] (https://cloud.elastic.co/deployments?q={cloudId}) に移動し、展開設定で APM を有効にします。有効になったら、このページを更新してください。", "xpack.apm.tutorial.elasticCloudInstructions.title": "APM エージェント", "xpack.apm.tutorial.specProvider.artifacts.application.label": "APM を起動", "xpack.apm.unitLabel": "単位を選択", @@ -8094,7 +8093,6 @@ "xpack.enterpriseSearch.workplaceSearch.groups.groupSourcesUpdated": "共有コンテンツソースが正常に更新されました。", "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.groupTableHeader": "グループ", "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.sourcesTableHeader": "コンテンツソース", - "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.usersTableHeader": "ユーザー", "xpack.enterpriseSearch.workplaceSearch.groups.groupUpdatedText": "前回更新日時{updatedAt}。", "xpack.enterpriseSearch.workplaceSearch.groups.heading": "グループを管理", "xpack.enterpriseSearch.workplaceSearch.groups.inviteUsers.action": "ユーザーを招待", @@ -8107,7 +8105,6 @@ "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveDescription": "グループはWorkplace Searchから削除されます。{name}を削除してよろしいですか?", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmTitleText": "確認", "xpack.enterpriseSearch.workplaceSearch.groups.overview.emptySourcesDescription": "コンテンツソースはこのグループと共有されていません。", - "xpack.enterpriseSearch.workplaceSearch.groups.overview.emptyUsersDescription": "このグループにはユーザーがありません。", "xpack.enterpriseSearch.workplaceSearch.groups.overview.groupSourcesDescription": "「{name}」グループのすべてのユーザーによって検索可能です。", "xpack.enterpriseSearch.workplaceSearch.groups.overview.groupSourcesTitle": "グループコンテンツソース", "xpack.enterpriseSearch.workplaceSearch.groups.overview.groupUsersDescription": "メンバーはグループのソースを検索できます。", @@ -13629,7 +13626,6 @@ "xpack.ml.annotationsTable.byColumnSMVName": "グループ基準", "xpack.ml.annotationsTable.detectorColumnName": "検知器", "xpack.ml.annotationsTable.editAnnotationsTooltip": "注釈を編集します", - "xpack.ml.annotationsTable.editAnnotationsTooltipAriaLabel": "注釈を編集します", "xpack.ml.annotationsTable.eventColumnName": "イベント", "xpack.ml.annotationsTable.fromColumnName": "開始:", "xpack.ml.annotationsTable.howToCreateAnnotationDescription": "注釈を作成するには、{linkToSingleMetricView} を開きます", @@ -17289,7 +17285,6 @@ "xpack.osquery.fleetIntegration.scheduleQueryGroupsButtonText": "クエリグループをスケジュール", "xpack.osquery.liveQueriesHistory.newLiveQueryButtonLabel": "新しいライブクエリ", "xpack.osquery.liveQueriesHistory.pageTitle": "ライブクエリ履歴", - "xpack.osquery.liveQueryActionResults.summary.agentsQueriedLabelText": "エージェントがクエリされました", "xpack.osquery.liveQueryActionResults.summary.failedLabelText": "失敗", "xpack.osquery.liveQueryActionResults.summary.pendingLabelText": "未応答", "xpack.osquery.liveQueryActionResults.summary.successfulLabelText": "成功", @@ -17304,8 +17299,6 @@ "xpack.osquery.liveQueryActions.table.createdAtColumnTitle": "作成日時:", "xpack.osquery.liveQueryActions.table.queryColumnTitle": "クエリ", "xpack.osquery.liveQueryActions.table.viewDetailsColumnTitle": "詳細を表示", - "xpack.osquery.liveQueryDetails.kpis.agentsFailedCountLabelText": "エージェントが失敗しました", - "xpack.osquery.liveQueryDetails.kpis.agentsQueriedLabelText": "エージェントがクエリされました", "xpack.osquery.liveQueryDetails.pageTitle": "ライブクエリ詳細", "xpack.osquery.liveQueryDetails.viewLiveQueriesHistoryTitle": "ライブクエリ履歴を表示", "xpack.osquery.liveQueryForm.form.submitButtonLabel": "送信", @@ -18556,16 +18549,13 @@ "xpack.securitySolution.administration.os.linux": "Linux", "xpack.securitySolution.administration.os.macos": "Mac", "xpack.securitySolution.administration.os.windows": "Windows", - "xpack.securitySolution.alertDetails.alertSummary": "アラート概要", "xpack.securitySolution.alertDetails.checkDocs": "マニュアルをご確認ください。", "xpack.securitySolution.alertDetails.ifCtiNotEnabled": "脅威インテリジェンスソースを有効にしていない場合で、この機能について関心がある場合は、", - "xpack.securitySolution.alertDetails.noEnrichmentFound": "Threat Intel Enrichmentが見つかりません", "xpack.securitySolution.alertDetails.summary": "まとめ", "xpack.securitySolution.alertDetails.summary.investigationGuide": "調査ガイド", "xpack.securitySolution.alertDetails.summary.readLess": "表示を減らす", "xpack.securitySolution.alertDetails.summary.readMore": "続きを読む", "xpack.securitySolution.alertDetails.threatIntel": "Threat Intel", - "xpack.securitySolution.alertDetails.threatSummary": "脅威概要", "xpack.securitySolution.alerts.riskScoreMapping.defaultDescriptionLabel": "このルールで生成されたすべてのアラートのリスクスコアを選択します。", "xpack.securitySolution.alerts.riskScoreMapping.defaultRiskScoreTitle": "デフォルトリスクスコア", "xpack.securitySolution.alerts.riskScoreMapping.mappingDescriptionLabel": "ソースイベント値を使用して、デフォルトリスクスコアを上書きします。", @@ -18990,7 +18980,6 @@ "xpack.securitySolution.detectionEngine.components.importRuleModal.selectRuleDescription": "インポートするセキュリティルール (検出エンジンビューからエクスポートしたもの) を選択します", "xpack.securitySolution.detectionEngine.createRule. stepScheduleRule.completeWithActivatingTitle": "ルールの作成と有効化", "xpack.securitySolution.detectionEngine.createRule. stepScheduleRule.completeWithoutActivatingTitle": "有効化せずにルールを作成", - "xpack.securitySolution.detectionEngine.createRule.backToRulesDescription": "検出ルールに戻る", "xpack.securitySolution.detectionEngine.createRule.editRuleButton": "編集", "xpack.securitySolution.detectionEngine.createRule.eqlRuleTypeDescription": "イベント相関関係", "xpack.securitySolution.detectionEngine.createRule.filtersLabel": "フィルター", @@ -19732,7 +19721,6 @@ "xpack.securitySolution.detectionEngine.ruleDescription.thresholdResultsAggregatedByDescription": "結果集約条件", "xpack.securitySolution.detectionEngine.ruleDescription.thresholdResultsAllDescription": "すべての結果", "xpack.securitySolution.detectionEngine.ruleDetails.activatedRuleLabel": "有効化", - "xpack.securitySolution.detectionEngine.ruleDetails.backToRulesDescription": "検出ルールに戻る", "xpack.securitySolution.detectionEngine.ruleDetails.errorCalloutTitle": "ルール失敗", "xpack.securitySolution.detectionEngine.ruleDetails.exceptionsTab": "例外", "xpack.securitySolution.detectionEngine.ruleDetails.experimentalDescription": "実験的", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4d9b34179e930..b58c62e8a67ef 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5963,7 +5963,6 @@ "xpack.apm.transactionsTable.notFoundLabel": "未找到任何事务。", "xpack.apm.transactionsTable.throughputColumnLabel": "吞吐量", "xpack.apm.tutorial.apmServer.title": "APM Server", - "xpack.apm.tutorial.elasticCloud.textPre": "要启用 APM Server,请前往 [Elastic Cloud 控制台](https://cloud.elastic.co/deployments?q={cloudId}) 并在部署设置中启用 APM。启用后,请刷新此页面。", "xpack.apm.tutorial.elasticCloudInstructions.title": "APM 代理", "xpack.apm.tutorial.specProvider.artifacts.application.label": "启动 APM", "xpack.apm.unitLabel": "选择单位", @@ -8162,7 +8161,6 @@ "xpack.enterpriseSearch.workplaceSearch.groups.groupSourcesUpdated": "已成功更新共享内容源。", "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.groupTableHeader": "组", "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.sourcesTableHeader": "内容源", - "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.usersTableHeader": "用户", "xpack.enterpriseSearch.workplaceSearch.groups.groupUpdatedText": "上次更新于 {updatedAt}。", "xpack.enterpriseSearch.workplaceSearch.groups.heading": "管理组", "xpack.enterpriseSearch.workplaceSearch.groups.inviteUsers.action": "邀请用户", @@ -8175,7 +8173,6 @@ "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveDescription": "您的组将从 Workplace Search 中删除。确定要移除 {name}?", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmTitleText": "确认", "xpack.enterpriseSearch.workplaceSearch.groups.overview.emptySourcesDescription": "未与此组共享任何内容源。", - "xpack.enterpriseSearch.workplaceSearch.groups.overview.emptyUsersDescription": "此组中没有用户。", "xpack.enterpriseSearch.workplaceSearch.groups.overview.groupSourcesDescription": "可按“{name}”组中的所有用户搜索。", "xpack.enterpriseSearch.workplaceSearch.groups.overview.groupSourcesTitle": "组内容源", "xpack.enterpriseSearch.workplaceSearch.groups.overview.groupUsersDescription": "成员将可以对该组的源进行搜索。", @@ -13808,7 +13805,6 @@ "xpack.ml.annotationsTable.byColumnSMVName": "依据", "xpack.ml.annotationsTable.detectorColumnName": "检测工具", "xpack.ml.annotationsTable.editAnnotationsTooltip": "编辑注释", - "xpack.ml.annotationsTable.editAnnotationsTooltipAriaLabel": "编辑注释", "xpack.ml.annotationsTable.eventColumnName": "事件", "xpack.ml.annotationsTable.fromColumnName": "自", "xpack.ml.annotationsTable.howToCreateAnnotationDescription": "要创建注释,请打开 {linkToSingleMetricView}", @@ -17527,7 +17523,6 @@ "xpack.osquery.fleetIntegration.scheduleQueryGroupsButtonText": "计划查询组", "xpack.osquery.liveQueriesHistory.newLiveQueryButtonLabel": "新建实时查询", "xpack.osquery.liveQueriesHistory.pageTitle": "实时查询历史记录", - "xpack.osquery.liveQueryActionResults.summary.agentsQueriedLabelText": "查询的代理", "xpack.osquery.liveQueryActionResults.summary.failedLabelText": "失败", "xpack.osquery.liveQueryActionResults.summary.pendingLabelText": "尚未响应", "xpack.osquery.liveQueryActionResults.summary.successfulLabelText": "成功", @@ -17542,8 +17537,6 @@ "xpack.osquery.liveQueryActions.table.createdAtColumnTitle": "创建于", "xpack.osquery.liveQueryActions.table.queryColumnTitle": "查询", "xpack.osquery.liveQueryActions.table.viewDetailsColumnTitle": "查看详情", - "xpack.osquery.liveQueryDetails.kpis.agentsFailedCountLabelText": "失败的代理", - "xpack.osquery.liveQueryDetails.kpis.agentsQueriedLabelText": "查询的代理", "xpack.osquery.liveQueryDetails.pageTitle": "实时查询详情", "xpack.osquery.liveQueryDetails.viewLiveQueriesHistoryTitle": "查看实时查询历史记录", "xpack.osquery.liveQueryForm.form.submitButtonLabel": "提交", @@ -18820,16 +18813,13 @@ "xpack.securitySolution.administration.os.linux": "Linux", "xpack.securitySolution.administration.os.macos": "Mac", "xpack.securitySolution.administration.os.windows": "Windows", - "xpack.securitySolution.alertDetails.alertSummary": "告警摘要", "xpack.securitySolution.alertDetails.checkDocs": "请查看我们的文档。", "xpack.securitySolution.alertDetails.ifCtiNotEnabled": "如果尚未启用任何威胁情报来源,并希望更多了解此功能,", - "xpack.securitySolution.alertDetails.noEnrichmentFound": "未找到威胁情报扩充", "xpack.securitySolution.alertDetails.summary": "摘要", "xpack.securitySolution.alertDetails.summary.investigationGuide": "调查指南", "xpack.securitySolution.alertDetails.summary.readLess": "阅读更少内容", "xpack.securitySolution.alertDetails.summary.readMore": "阅读更多内容", "xpack.securitySolution.alertDetails.threatIntel": "威胁情报", - "xpack.securitySolution.alertDetails.threatSummary": "威胁摘要", "xpack.securitySolution.alerts.riskScoreMapping.defaultDescriptionLabel": "选择此规则生成的所有告警的风险分数。", "xpack.securitySolution.alerts.riskScoreMapping.defaultRiskScoreTitle": "默认风险分数", "xpack.securitySolution.alerts.riskScoreMapping.mappingDescriptionLabel": "使用源事件值覆盖默认风险分数。", @@ -19269,7 +19259,6 @@ "xpack.securitySolution.detectionEngine.components.importRuleModal.successfullyImportedRulesTitle": "已成功导入 {totalRules} 个{totalRules, plural, other {规则}}", "xpack.securitySolution.detectionEngine.createRule. stepScheduleRule.completeWithActivatingTitle": "创建并激活规则", "xpack.securitySolution.detectionEngine.createRule. stepScheduleRule.completeWithoutActivatingTitle": "创建规则但不激活", - "xpack.securitySolution.detectionEngine.createRule.backToRulesDescription": "返回到检测规则", "xpack.securitySolution.detectionEngine.createRule.editRuleButton": "编辑", "xpack.securitySolution.detectionEngine.createRule.eqlRuleTypeDescription": "事件关联", "xpack.securitySolution.detectionEngine.createRule.filtersLabel": "筛选", @@ -20014,7 +20003,6 @@ "xpack.securitySolution.detectionEngine.ruleDescription.thresholdResultsAggregatedByDescription": "结果聚合依据", "xpack.securitySolution.detectionEngine.ruleDescription.thresholdResultsAllDescription": "所有结果", "xpack.securitySolution.detectionEngine.ruleDetails.activatedRuleLabel": "已激活", - "xpack.securitySolution.detectionEngine.ruleDetails.backToRulesDescription": "返回到检测规则", "xpack.securitySolution.detectionEngine.ruleDetails.errorCalloutTitle": "规则错误位置", "xpack.securitySolution.detectionEngine.ruleDetails.exceptionsTab": "例外", "xpack.securitySolution.detectionEngine.ruleDetails.experimentalDescription": "实验性", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx index 5d526e74564c5..56f333396908b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx @@ -43,6 +43,7 @@ export const IndexParamsFields = ({ ALERT_HISTORY_PREFIX, '' ); + const [isActionConnectorChanged, setIsActionConnectorChanged] = useState(false); const getDocumentToIndex = (doc: Array> | undefined) => doc && doc.length > 0 ? ((doc[0] as unknown) as string) : undefined; @@ -67,11 +68,12 @@ export const IndexParamsFields = ({ setUsePreconfiguredSchema(true); editAction('documents', [JSON.stringify(AlertHistoryDocumentTemplate)], index); setDocumentToIndex(JSON.stringify(AlertHistoryDocumentTemplate)); - } else { + } else if (isActionConnectorChanged) { setUsePreconfiguredSchema(false); editAction('documents', undefined, index); setDocumentToIndex(undefined); } + setIsActionConnectorChanged(true); // eslint-disable-next-line react-hooks/exhaustive-deps }, [actionConnector?.id]); diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx index 479a512b7238a..9f00dd2e8f061 100644 --- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiButtonEmpty, EuiHeaderLinks, EuiHeaderSectionItem, EuiToolTip } from '@elastic/eui'; +import { EuiHeaderLinks, EuiToolTip, EuiHeaderLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useHistory } from 'react-router-dom'; @@ -51,53 +51,45 @@ export function ActionMenuContent(): React.ReactElement { return ( - - + + + + + + {ANALYZE_MESSAGE}

}> + - -
-
- - - - - {ANALYZE_MESSAGE}

}> - - {ANALYZE_DATA} - -
-
- - - {ADD_DATA_LABEL} - - + {ANALYZE_DATA} + + + + + {ADD_DATA_LABEL} +
); } diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_title.test.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_title.test.tsx index 4fd6335c3d3ca..726ad235f7f49 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_title.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_title.test.tsx @@ -11,7 +11,7 @@ import { screen } from '@testing-library/react'; import { render } from '../../lib/helper/rtl_helpers'; import * as reactRouterDom from 'react-router-dom'; import { Ping } from '../../../common/runtime_types'; -import { MonitorPageTitle } from './monitor_title'; +import { MonitorPageTitle, MonitorPageTitleContent } from './monitor_title'; jest.mock('react-router-dom', () => { const originalModule = jest.requireActual('react-router-dom'); @@ -77,11 +77,17 @@ describe('MonitorTitle component', () => { }); it('renders the monitor heading and EnableMonitorAlert toggle', () => { - render(, { - state: { monitorStatus: { status: monitorStatusWithName, loading: false } }, - }); - expect(screen.getByRole('heading', { level: 1, name: monitorName })).toBeInTheDocument(); - expect(screen.getByTestId('uptimeDisplayDefineConnector')).toBeInTheDocument(); + render( + <> + + + , + { + state: { monitorStatus: { status: monitorStatusWithName, loading: false } }, + } + ); + expect(screen.getByText(monitorName)); + expect(screen.getByRole('switch')).toBeInTheDocument(); }); it('renders the user provided monitorId when the name is not present', () => { @@ -89,21 +95,24 @@ describe('MonitorTitle component', () => { render(, { state: { monitorStatus: { status: defaultMonitorStatus, loading: false } }, }); - expect(screen.getByRole('heading', { level: 1, name: defaultMonitorId })).toBeInTheDocument(); + expect(screen.getByText(defaultMonitorId)); }); it('renders the url when the monitorId is auto generated and the monitor name is not present', () => { mockReactRouterDomHooks({ useParamsResponse: { monitorId: autoGeneratedMonitorIdEncoded } }); - render(, { - state: { monitorStatus: { status: defaultMonitorStatus, loading: false } }, - }); - expect( - screen.getByRole('heading', { level: 1, name: defaultMonitorStatus.url?.full }) - ).toBeInTheDocument(); + render( +
+ +
, + { + state: { monitorStatus: { status: defaultMonitorStatus, loading: false } }, + } + ); + expect(screen.getByText(defaultMonitorStatus!.url!.full!)); }); it('renders beta disclaimer for synthetics monitors', () => { - render(, { + render(, { state: { monitorStatus: { status: defaultBrowserMonitorStatus, loading: false } }, }); const betaLink = screen.getByRole('link', { diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_title.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_title.tsx index 2112af0653669..aa68e2aa7fc4b 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_title.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_title.tsx @@ -5,15 +5,7 @@ * 2.0. */ -import { - EuiBadge, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiTitle, - EuiLink, - EuiText, -} from '@elastic/eui'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiLink, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { useSelector } from 'react-redux'; @@ -40,18 +32,11 @@ const getPageTitle = (monitorId: string, selectedMonitor: Ping | null) => { return monitorId; }; -export const MonitorPageTitle: React.FC = () => { +export const MonitorPageTitleContent: React.FC = () => { const monitorId = useMonitorId(); - const selectedMonitor = useSelector(monitorStatusSelector); - - const nameOrId = selectedMonitor?.monitor?.name || getPageTitle(monitorId, selectedMonitor); - const type = selectedMonitor?.monitor?.type; const isBrowser = type === 'browser'; - - useBreadcrumbs([{ text: nameOrId }]); - const renderMonitorType = (monitorType: string) => { switch (monitorType) { case 'http': @@ -86,12 +71,13 @@ export const MonitorPageTitle: React.FC = () => { return ''; } }; - return ( <> - -

{nameOrId}

-
+ + + + + @@ -118,7 +104,18 @@ export const MonitorPageTitle: React.FC = () => { )} - ); }; + +export const MonitorPageTitle: React.FC = () => { + const monitorId = useMonitorId(); + + const selectedMonitor = useSelector(monitorStatusSelector); + + const nameOrId = selectedMonitor?.monitor?.name || getPageTitle(monitorId, selectedMonitor); + + useBreadcrumbs([{ text: nameOrId }]); + + return {nameOrId}; +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx index 610107f406306..c24ecd9183865 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx @@ -16,7 +16,7 @@ import { useUiSetting$ } from '../../../../../../../../src/plugins/kibana_react/ import { useMonitorBreadcrumb } from './use_monitor_breadcrumb'; import { ClientPluginsStart } from '../../../../apps/plugin'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; -import { StepPageTitle } from './step_page_title'; +import { StepPageTitleContent } from './step_page_title'; import { StepPageNavigation } from './step_page_nav'; import { WaterfallChartContainer } from './waterfall/waterfall_chart_container'; @@ -78,10 +78,11 @@ export const StepDetailContainer: React.FC = ({ checkGroup, stepIndex }) return ( void; handleNextStep: () => void; } -export const StepPageTitle = ({ - stepName, + +export const StepPageTitleContent = ({ stepIndex, totalSteps, handleNextStep, @@ -29,11 +29,6 @@ export const StepPageTitle = ({ }: Props) => { return ( - - -

{stepName}

-
-
diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx index 278958bd1987b..22193fe4623d6 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx @@ -6,7 +6,7 @@ */ import { - EuiButtonEmpty, + EuiHeaderLink, EuiContextMenu, EuiContextMenuPanelDescriptor, EuiContextMenuPanelItemDescriptor, @@ -123,8 +123,7 @@ export const ToggleAlertFlyoutButtonComponent: React.FC = ({ return ( = ({ id="xpack.uptime.alerts.toggleAlertFlyoutButtonText" defaultMessage="Alerts and rules" /> - + } closePopover={() => setIsOpen(false)} isOpen={isOpen} diff --git a/x-pack/plugins/uptime/public/components/overview/synthetics_callout.test.tsx b/x-pack/plugins/uptime/public/components/overview/synthetics_callout.test.tsx index 5f6f9d7a7207e..ec9e5f958ec3a 100644 --- a/x-pack/plugins/uptime/public/components/overview/synthetics_callout.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/synthetics_callout.test.tsx @@ -67,9 +67,6 @@ describe('SyntheticsCallout', () => {
- `); }); @@ -128,9 +125,6 @@ describe('SyntheticsCallout', () => { - `); wrapper.find('EuiButton').simulate('click'); diff --git a/x-pack/plugins/uptime/public/components/overview/synthetics_callout.tsx b/x-pack/plugins/uptime/public/components/overview/synthetics_callout.tsx index fa28e42d7d0c1..4e9c3256f1578 100644 --- a/x-pack/plugins/uptime/public/components/overview/synthetics_callout.tsx +++ b/x-pack/plugins/uptime/public/components/overview/synthetics_callout.tsx @@ -5,14 +5,7 @@ * 2.0. */ -import { - EuiButton, - EuiButtonEmpty, - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, -} from '@elastic/eui'; +import { EuiButton, EuiButtonEmpty, EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -74,7 +67,6 @@ export const SyntheticsCallout = () => { - ); }; diff --git a/x-pack/plugins/uptime/public/lib/alert_types/translations.ts b/x-pack/plugins/uptime/public/lib/alert_types/translations.ts index bb4af761d240d..5122120479cf7 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/translations.ts +++ b/x-pack/plugins/uptime/public/lib/alert_types/translations.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; export const TlsTranslations = { - defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.legacy.defaultActionMessage', { + defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.defaultActionMessage', { defaultMessage: `Detected TLS certificate {commonName} from issuer {issuer} is {status}. Certificate {summary} `, values: { @@ -18,17 +18,16 @@ export const TlsTranslations = { status: '{{state.status}}', }, }), - name: i18n.translate('xpack.uptime.alerts.tls.legacy.clientName', { - defaultMessage: 'Uptime TLS (Legacy)', + name: i18n.translate('xpack.uptime.alerts.tls.clientName', { + defaultMessage: 'Uptime TLS', }), - description: i18n.translate('xpack.uptime.alerts.tls.legacy.description', { - defaultMessage: - 'Alert when the TLS certificate of an Uptime monitor is about to expire. This alert will be deprecated in a future version.', + description: i18n.translate('xpack.uptime.alerts.tls.description', { + defaultMessage: 'Alert when the TLS certificate of an Uptime monitor is about to expire.', }), }; export const TlsTranslationsLegacy = { - defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.defaultActionMessage', { + defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.legacy.defaultActionMessage', { defaultMessage: `Detected {count} TLS certificates expiring or becoming too old. {expiringConditionalOpen} Expiring cert count: {expiringCount} @@ -51,11 +50,12 @@ Aging Certificates: {agingCommonNameAndDate} agingConditionalClose: '{{/state.hasAging}}', }, }), - name: i18n.translate('xpack.uptime.alerts.tls.clientName', { + name: i18n.translate('xpack.uptime.alerts.tls.legacy.clientName', { defaultMessage: 'Uptime TLS', }), - description: i18n.translate('xpack.uptime.alerts.tls.description', { - defaultMessage: 'Alert when the TLS certificate of an Uptime monitor is about to expire.', + description: i18n.translate('xpack.uptime.alerts.tls.legacy.description', { + defaultMessage: + 'Alert when the TLS certificate of an Uptime monitor is about to expire. This rule type will be deprecated in a future version.', }), }; diff --git a/x-pack/plugins/uptime/public/routes.tsx b/x-pack/plugins/uptime/public/routes.tsx index e3c558cee2c32..2b0cc4dc5e5c2 100644 --- a/x-pack/plugins/uptime/public/routes.tsx +++ b/x-pack/plugins/uptime/public/routes.tsx @@ -23,7 +23,7 @@ import { UptimePage, useUptimeTelemetry } from './hooks'; import { OverviewPageComponent } from './pages/overview'; import { SyntheticsCheckSteps } from './pages/synthetics/synthetics_checks'; import { ClientPluginsStart } from './apps/plugin'; -import { MonitorPageTitle } from './components/monitor/monitor_title'; +import { MonitorPageTitle, MonitorPageTitleContent } from './components/monitor/monitor_title'; import { UptimeDatePicker } from './components/common/uptime_date_picker'; import { useKibana } from '../../../../src/plugins/kibana_react/public'; import { CertRefreshBtn } from './components/certificates/cert_refresh_btn'; @@ -36,10 +36,16 @@ interface RouteProps { dataTestSubj: string; title: string; telemetryId: UptimePage; - pageHeader?: { pageTitle: string | JSX.Element; rightSideItems?: JSX.Element[] }; + pageHeader?: { + children?: JSX.Element; + pageTitle: string | JSX.Element; + rightSideItems?: JSX.Element[]; + }; } -const baseTitle = 'Uptime - Kibana'; +const baseTitle = i18n.translate('xpack.uptime.routes.baseTitle', { + defaultMessage: 'Uptime - Kibana', +}); export const MONITORING_OVERVIEW_LABEL = i18n.translate('xpack.uptime.overview.heading', { defaultMessage: 'Monitors', @@ -47,18 +53,25 @@ export const MONITORING_OVERVIEW_LABEL = i18n.translate('xpack.uptime.overview.h const Routes: RouteProps[] = [ { - title: `Monitor | ${baseTitle}`, + title: i18n.translate('xpack.uptime.monitorRoute.title', { + defaultMessage: 'Monitor | {baseTitle}', + values: { baseTitle }, + }), path: MONITOR_ROUTE, component: MonitorPage, dataTestSubj: 'uptimeMonitorPage', telemetryId: UptimePage.Monitor, pageHeader: { + children: , pageTitle: , rightSideItems: [], }, }, { - title: `Settings | ${baseTitle}`, + title: i18n.translate('xpack.uptime.settingsRoute.title', { + defaultMessage: `Settings | {baseTitle}`, + values: { baseTitle }, + }), path: SETTINGS_ROUTE, component: SettingsPage, dataTestSubj: 'uptimeSettingsPage', @@ -70,7 +83,10 @@ const Routes: RouteProps[] = [ }, }, { - title: `Certificates | ${baseTitle}`, + title: i18n.translate('xpack.uptime.certificatesRoute.title', { + defaultMessage: `Certificates | {baseTitle}`, + values: { baseTitle }, + }), path: CERTIFICATES_ROUTE, component: CertificatesPage, dataTestSubj: 'uptimeCertificatesPage', @@ -81,7 +97,10 @@ const Routes: RouteProps[] = [ }, }, { - title: baseTitle, + title: i18n.translate('xpack.uptime.stepDetailRoute.title', { + defaultMessage: 'Synthetics detail | {baseTitle}', + values: { baseTitle }, + }), path: STEP_DETAIL_ROUTE, component: StepDetailPage, dataTestSubj: 'uptimeStepDetailPage', diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts index 29f2f0cca82bc..743e9f6bc75ac 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { generateFilterDSL, hasFilters, @@ -62,7 +61,12 @@ const mockOptions = ( shouldCheckStatus: true, }, services = alertsMock.createAlertServices(), - state = {} + state = {}, + rule = { + schedule: { + interval: '5m', + }, + } ): any => { services.scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); services.scopedClusterClient.asCurrentUser = (jest.fn() as unknown) as any; @@ -77,13 +81,16 @@ const mockOptions = ( params, services, state, + rule, }; }; describe('status check alert', () => { let toISOStringSpy: jest.SpyInstance; + const mockDate = new Date('2021-05-13T12:33:37.000Z'); beforeEach(() => { toISOStringSpy = jest.spyOn(Date.prototype, 'toISOString'); + Date.now = jest.fn().mockReturnValue(mockDate); }); afterEach(() => { @@ -108,10 +115,14 @@ describe('status check alert', () => { "filters": undefined, "locations": Array [], "numTimes": 5, - "timerange": Object { + "timespanRange": Object { "from": "now-15m", "to": "now", }, + "timestampRange": Object { + "from": 1620821917000, + "to": "now", + }, "uptimeEsClient": Object { "baseESClient": [MockFunction], "count": [Function], @@ -163,10 +174,14 @@ describe('status check alert', () => { "filters": undefined, "locations": Array [], "numTimes": 5, - "timerange": Object { + "timespanRange": Object { "from": "now-15m", "to": "now", }, + "timestampRange": Object { + "from": 1620821917000, + "to": "now", + }, "uptimeEsClient": Object { "baseESClient": [MockFunction], "count": [Function], @@ -476,10 +491,14 @@ describe('status check alert', () => { }, "locations": Array [], "numTimes": 3, - "timerange": Object { + "timespanRange": Object { "from": "now-15m", "to": "now", }, + "timestampRange": Object { + "from": 1620821917000, + "to": "now", + }, "uptimeEsClient": Object { "baseESClient": [MockFunction], "count": [Function], @@ -583,10 +602,14 @@ describe('status check alert', () => { }, "locations": Array [], "numTimes": 20, - "timerange": Object { + "timespanRange": Object { "from": "now-30h", "to": "now", }, + "timestampRange": Object { + "from": 1620714817000, + "to": "now", + }, "uptimeEsClient": Object { "baseESClient": [MockFunction], "count": [Function], @@ -900,6 +923,85 @@ describe('status check alert', () => { }); }); + it('generates timespan and @timestamp ranges appropriately', async () => { + const mockGetter = jest.fn(); + mockGetter.mockReturnValue([]); + const { server, libs, plugins } = bootstrapDependencies({ + getIndexPattern: jest.fn(), + getMonitorStatus: mockGetter, + }); + const alert = statusCheckAlertFactory(server, libs, plugins); + const options = mockOptions({ + numTimes: 20, + timerangeCount: 30, + timerangeUnit: 'h', + filters: { + 'monitor.type': ['http'], + 'observer.geo.name': [], + tags: [], + 'url.port': [], + }, + search: 'url.full: *', + }); + await alert.executor(options); + + expect(mockGetter).toHaveBeenCalledTimes(1); + expect(mockGetter.mock.calls[0][0]).toEqual( + expect.objectContaining({ + timespanRange: { + from: 'now-30h', + to: 'now', + }, + timestampRange: { + from: mockDate.setHours(mockDate.getHours() - 54).valueOf(), // now minus the timerange (30h), plus an additional 24 hour buffer + to: 'now', + }, + }) + ); + }); + + it('uses the larger of alert interval and timerange when defining timestampRange', async () => { + const mockGetter = jest.fn(); + mockGetter.mockReturnValue([]); + const { server, libs, plugins } = bootstrapDependencies({ + getIndexPattern: jest.fn(), + getMonitorStatus: mockGetter, + }); + const alert = statusCheckAlertFactory(server, libs, plugins); + const options = mockOptions( + { + numTimes: 20, + timerangeCount: 30, + timerangeUnit: 'h', + filters: { + 'monitor.type': ['http'], + 'observer.geo.name': [], + tags: [], + 'url.port': [], + }, + search: 'url.full: *', + }, + undefined, + undefined, + { schedule: { interval: '60h' } } + ); + await alert.executor(options); + + expect(mockGetter).toHaveBeenCalledTimes(1); + expect(mockGetter.mock.calls[0][0]).toEqual( + expect.objectContaining({ + timespanRange: { + from: 'now-30h', + to: 'now', + }, + timestampRange: { + from: mockDate.setHours(mockDate.getHours() - 60).valueOf(), // 60h rule schedule interval is larger than 30h timerange, so use now - 60h to define timestamp range + to: 'now', + }, + }) + ); + }); + describe('hasFilters', () => { it('returns false for undefined filters', () => { expect(hasFilters()).toBe(false); diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index 6f3e3303f6bdc..364518bba720a 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -4,7 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import datemath from '@elastic/datemath'; +import { min } from 'lodash'; import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import Mustache from 'mustache'; @@ -31,6 +32,34 @@ import { UMServerLibs, UptimeESClient } from '../lib'; export type ActionGroupIds = ActionGroupIdsOf; +/** + * Returns the appropriate range for filtering the documents by `@timestamp`. + * + * We check monitor status by `monitor.timespan`, but need to first cut down on the number of documents + * searched by filtering by `@timestamp`. To ensure that we catch as many documents as possible which could + * likely contain a down monitor with a `monitor.timespan` in the given timerange, we create a filter + * range for `@timestamp` that is the greater of either: from now to now - timerange interval - 24 hours + * OR from now to now - rule interval + * @param ruleScheduleLookback - string representing now minus the interval at which the rule is ran + * @param timerangeLookback - string representing now minus the timerange configured by the user for checking down monitors + */ +export function getTimestampRange({ + ruleScheduleLookback, + timerangeLookback, +}: Record<'ruleScheduleLookback' | 'timerangeLookback', string>) { + const scheduleIntervalAbsoluteTime = datemath.parse(ruleScheduleLookback)?.valueOf(); + const defaultIntervalAbsoluteTime = datemath + .parse(timerangeLookback) + ?.subtract('24', 'hours') + .valueOf(); + const from = min([scheduleIntervalAbsoluteTime, defaultIntervalAbsoluteTime]) ?? 'now-24h'; + + return { + to: 'now', + from, + }; +} + const getMonIdByLoc = (monitorId: string, location: string) => { return monitorId + '-' + location; }; @@ -264,6 +293,9 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( params: rawParams, state, services: { alertInstanceFactory }, + rule: { + schedule: { interval }, + }, }, uptimeEsClient, }) { @@ -279,14 +311,22 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( isAutoGenerated, timerange: oldVersionTimeRange, } = rawParams; - const filterString = await formatFilterString(uptimeEsClient, filters, search, libs); - const timerange = oldVersionTimeRange || { - from: `now-${String(timerangeCount) + timerangeUnit}`, + const timespanInterval = `${String(timerangeCount)}${timerangeUnit}`; + + // Range filter for `monitor.timespan`, the range of time the ping is valid + const timespanRange = oldVersionTimeRange || { + from: `now-${timespanInterval}`, to: 'now', }; + // Range filter for `@timestamp`, the time the document was indexed + const timestampRange = getTimestampRange({ + ruleScheduleLookback: `now-${interval}`, + timerangeLookback: timespanRange.from, + }); + let downMonitorsByLocation: GetMonitorStatusResult[] = []; // if oldVersionTimeRange present means it's 7.7 format and @@ -294,7 +334,8 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( if (!(!oldVersionTimeRange && shouldCheckStatus === false)) { downMonitorsByLocation = await libs.requests.getMonitorStatus({ uptimeEsClient, - timerange, + timespanRange, + timestampRange, numTimes, locations: [], filters: filterString, diff --git a/x-pack/plugins/uptime/server/lib/requests/get_certs.ts b/x-pack/plugins/uptime/server/lib/requests/get_certs.ts index 1b20ed9085fef..7639484f51737 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_certs.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_certs.ts @@ -138,7 +138,6 @@ export const getCerts: UMElasticsearchQueryFn = asyn searchBody.query.bool.filter.push(validityFilters); } - // console.log(JSON.stringify(params, null, 2)); const { body: result } = await uptimeEsClient.search({ body: searchBody, }); diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.test.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.test.ts index 6d88ccb9a9eff..08b675576a5d2 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.test.ts @@ -85,10 +85,14 @@ describe('getMonitorStatus', () => { filters: exampleFilter, locations: [], numTimes: 5, - timerange: { + timespanRange: { from: 'now-10m', to: 'now-1m', }, + timestampRange: { + from: 'now-24h', + to: 'now', + }, }); expect(esMock.search).toHaveBeenCalledTimes(1); const [params] = esMock.search.mock.calls[0]; @@ -144,6 +148,14 @@ describe('getMonitorStatus', () => { Object { "range": Object { "@timestamp": Object { + "gte": "now-24h", + "lte": "now", + }, + }, + }, + Object { + "range": Object { + "monitor.timespan": Object { "gte": "now-10m", "lte": "now-1m", }, @@ -202,10 +214,14 @@ describe('getMonitorStatus', () => { uptimeEsClient, locations: ['fairbanks', 'harrisburg'], numTimes: 1, - timerange: { + timespanRange: { from: 'now-2m', to: 'now', }, + timestampRange: { + from: 'now-24h', + to: 'now', + }, }); expect(esMock.search).toHaveBeenCalledTimes(1); const [params] = esMock.search.mock.calls[0]; @@ -261,6 +277,14 @@ describe('getMonitorStatus', () => { Object { "range": Object { "@timestamp": Object { + "gte": "now-24h", + "lte": "now", + }, + }, + }, + Object { + "range": Object { + "monitor.timespan": Object { "gte": "now-2m", "lte": "now", }, @@ -298,10 +322,14 @@ describe('getMonitorStatus', () => { genBucketItem ); const clientParameters = { - timerange: { + timespanRange: { from: 'now-15m', to: 'now', }, + timestampRange: { + from: 'now-24h', + to: 'now', + }, numTimes: 5, locations: [], filters: { @@ -415,6 +443,14 @@ describe('getMonitorStatus', () => { Object { "range": Object { "@timestamp": Object { + "gte": "now-24h", + "lte": "now", + }, + }, + }, + Object { + "range": Object { + "monitor.timespan": Object { "gte": "now-15m", "lte": "now", }, @@ -485,10 +521,14 @@ describe('getMonitorStatus', () => { genBucketItem ); const clientParameters = { - timerange: { + timespanRange: { from: 'now-15m', to: 'now', }, + timestampRange: { + from: 'now-24h', + to: 'now', + }, numTimes: 5, locations: [], filters: { @@ -562,6 +602,14 @@ describe('getMonitorStatus', () => { Object { "range": Object { "@timestamp": Object { + "gte": "now-24h", + "lte": "now", + }, + }, + }, + Object { + "range": Object { + "monitor.timespan": Object { "gte": "now-15m", "lte": "now", }, @@ -618,10 +666,14 @@ describe('getMonitorStatus', () => { filters: undefined, locations: [], numTimes: 5, - timerange: { + timespanRange: { from: 'now-12m', to: 'now-2m', }, + timestampRange: { + from: 'now-24h', + to: 'now', + }, }; const { uptimeEsClient } = getUptimeESMockClient(esMock); @@ -684,6 +736,14 @@ describe('getMonitorStatus', () => { Object { "range": Object { "@timestamp": Object { + "gte": "now-24h", + "lte": "now", + }, + }, + }, + Object { + "range": Object { + "monitor.timespan": Object { "gte": "now-12m", "lte": "now-2m", }, @@ -810,10 +870,14 @@ describe('getMonitorStatus', () => { uptimeEsClient, locations: [], numTimes: 5, - timerange: { + timespanRange: { from: 'now-10m', to: 'now-1m', }, + timestampRange: { + from: 'now-24h', + to: 'now', + }, }); expect(result.length).toBe(8); expect(result).toMatchInlineSnapshot(` diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts index 07047bd0be7bc..15e6fe30db186 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts @@ -15,7 +15,8 @@ export interface GetMonitorStatusParams { filters?: JsonObject; locations: string[]; numTimes: number; - timerange: { from: string; to: string }; + timespanRange: { from: string; to: string }; + timestampRange: { from: string | number; to: string }; } export interface GetMonitorStatusResult { @@ -43,7 +44,7 @@ export type AfterKey = Record | undefined; export const getMonitorStatus: UMElasticsearchQueryFn< GetMonitorStatusParams, GetMonitorStatusResult[] -> = async ({ uptimeEsClient, filters, locations, numTimes, timerange: { from, to } }) => { +> = async ({ uptimeEsClient, filters, locations, numTimes, timespanRange, timestampRange }) => { let afterKey: AfterKey; const STATUS = 'down'; @@ -63,8 +64,16 @@ export const getMonitorStatus: UMElasticsearchQueryFn< { range: { '@timestamp': { - gte: from, - lte: to, + gte: timestampRange.from, + lte: timestampRange.to, + }, + }, + }, + { + range: { + 'monitor.timespan': { + gte: timespanRange.from, + lte: timespanRange.to, }, }, }, diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 1a7f9acc9f1a3..9e527835231b4 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -45,6 +45,9 @@ const onlyNotInCoverageTests = [ require.resolve('../test/detection_engine_api_integration/basic/config.ts'), require.resolve('../test/lists_api_integration/security_and_spaces/config.ts'), require.resolve('../test/plugin_api_integration/config.ts'), + require.resolve('../test/rule_registry/security_and_spaces/config_basic.ts'), + require.resolve('../test/rule_registry/security_and_spaces/config_trial.ts'), + require.resolve('../test/rule_registry/spaces_only/config_trial.ts'), require.resolve('../test/security_api_integration/saml.config.ts'), require.resolve('../test/security_api_integration/session_idle.config.ts'), require.resolve('../test/security_api_integration/session_invalidate.config.ts'), diff --git a/x-pack/test/accessibility/apps/reporting.ts b/x-pack/test/accessibility/apps/reporting.ts new file mode 100644 index 0000000000000..bccb650fa08ca --- /dev/null +++ b/x-pack/test/accessibility/apps/reporting.ts @@ -0,0 +1,76 @@ +/* + * 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 { FtrProviderContext } from '../ftr_provider_context'; + +import { JOB_PARAMS_RISON_CSV_DEPRECATED } from '../../reporting_api_integration/services/fixtures'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const { common } = getPageObjects(['common']); + const retry = getService('retry'); + const a11y = getService('a11y'); + const testSubjects = getService('testSubjects'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const reporting = getService('reporting'); + const esArchiver = getService('esArchiver'); + const security = getService('security'); + + describe('Reporting', () => { + const createReportingUser = async () => { + await security.user.create(reporting.REPORTING_USER_USERNAME, { + password: reporting.REPORTING_USER_PASSWORD, + roles: ['reporting_user', 'data_analyst', 'kibana_user'], // Deprecated: using built-in `reporting_user` role grants all Reporting privileges + full_name: 'a reporting user', + }); + }; + + const deleteReportingUser = async () => { + await security.user.delete(reporting.REPORTING_USER_USERNAME); + }; + + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/reporting/logs'); + await esArchiver.load('x-pack/test/functional/es_archives/logstash_functional'); + + await createReportingUser(); + await reporting.loginReportingUser(); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/reporting/logs'); + await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); + + await deleteReportingUser(); + }); + + beforeEach(async () => { + // Add one report + await supertestWithoutAuth + .post(`/api/reporting/generate/csv`) + .auth(reporting.REPORTING_USER_USERNAME, reporting.REPORTING_USER_PASSWORD) + .set('kbn-xsrf', 'xxx') + .send({ jobParams: JOB_PARAMS_RISON_CSV_DEPRECATED }) + .expect(200); + + await retry.waitFor('Reporting app', async () => { + await common.navigateToApp('reporting'); + return testSubjects.exists('reportingPageHeader'); + }); + }); + + afterEach(async () => { + await reporting.deleteAllReports(); + }); + + it('List reports view', async () => { + await retry.waitForWithTimeout('A reporting list item', 5000, () => { + return testSubjects.exists('reportingListItemObjectTitle'); + }); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index 81cfd70a23956..e79bbdb86a88a 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -37,6 +37,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./apps/security_solution'), require.resolve('./apps/ml_embeddables_in_dashboard'), require.resolve('./apps/remote_clusters'), + require.resolve('./apps/reporting'), ], pageObjects, diff --git a/x-pack/test/api_integration/apis/lens/field_stats.ts b/x-pack/test/api_integration/apis/lens/field_stats.ts index 5dcb749f54b31..5090fe14576d5 100644 --- a/x-pack/test/api_integration/apis/lens/field_stats.ts +++ b/x-pack/test/api_integration/apis/lens/field_stats.ts @@ -427,6 +427,38 @@ export default ({ getService }: FtrProviderContext) => { expect(body.totalDocuments).to.eql(425); }); + + it('should allow filtering on a runtime field other than the field in use', async () => { + const { body } = await supertest + .post('/api/lens/index_stats/logstash-2015.09.22/field') + .set(COMMON_HEADERS) + .send({ + dslQuery: { + bool: { + filter: [{ exists: { field: 'runtime_string_field' } }], + }, + }, + fromDate: TEST_START_TIME, + toDate: TEST_END_TIME, + fieldName: 'runtime_number_field', + }) + .expect(200); + + expect(body).to.eql({ + totalDocuments: 4634, + sampledDocuments: 4634, + sampledValues: 4634, + topValues: { + buckets: [ + { + count: 4634, + key: 5, + }, + ], + }, + histogram: { buckets: [] }, + }); + }); }); describe('histogram', () => { diff --git a/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts b/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts index 4ce9d4871246c..7987875a75519 100644 --- a/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts +++ b/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts @@ -52,6 +52,7 @@ export default ({ getService }: FtrProviderContext) => { aggregatableNotExistsFields: [{ fieldName: 'sourcetype', existsInDocs: false }], nonAggregatableExistsFields: [{ fieldName: 'type', existsInDocs: true, stats: {} }], nonAggregatableNotExistsFields: [], + errors: [], }, }, }, @@ -98,6 +99,7 @@ export default ({ getService }: FtrProviderContext) => { aggregatableNotExistsFields: [{ fieldName: 'sourcetype', existsInDocs: false }], nonAggregatableExistsFields: [{ fieldName: 'type', existsInDocs: true, stats: {} }], nonAggregatableNotExistsFields: [], + errors: [], }, }, }, diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index 7154debc3e195..394672ac07fc5 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -82,5 +82,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./results')); loadTestFile(require.resolve('./saved_objects')); loadTestFile(require.resolve('./system')); + loadTestFile(require.resolve('./trained_models')); }); } diff --git a/x-pack/test/api_integration/apis/ml/trained_models/delete_model.ts b/x-pack/test/api_integration/apis/ml/trained_models/delete_model.ts new file mode 100644 index 0000000000000..3848330a95fb9 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/trained_models/delete_model.ts @@ -0,0 +1,67 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; + +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('DELETE trained_models', () => { + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.api.createdTestTrainedModels('regression', 2); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('deletes trained model by id', async () => { + const { body: deleteResponseBody } = await supertest + .delete(`/api/ml/trained_models/dfa_regression_model_n_0`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(deleteResponseBody).to.eql({ acknowledged: true }); + + // verify that model is actually deleted + await supertest + .get(`/api/ml/trained_models/dfa_regression_model_n_0`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + }); + + it('returns 404 if requested trained model does not exist', async () => { + await supertest + .delete(`/api/ml/trained_models/not_existing_model`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + }); + + it('does not allow to delete trained model if the user does not have required permissions', async () => { + await supertest + .delete(`/api/ml/trained_models/dfa_regression_model_n_1`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(403); + + // verify that model has not been deleted + await supertest + .get(`/api/ml/trained_models/dfa_regression_model_n_1`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/trained_models/get_model_pipelines.ts b/x-pack/test/api_integration/apis/ml/trained_models/get_model_pipelines.ts new file mode 100644 index 0000000000000..cc347056f02a3 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/trained_models/get_model_pipelines.ts @@ -0,0 +1,51 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; + +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('GET trained_models/pipelines', () => { + let testModelIds: string[] = []; + + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + testModelIds = await ml.api.createdTestTrainedModels('regression', 2, true); + }); + + after(async () => { + // delete all created ingest pipelines + await Promise.all(testModelIds.map((modelId) => ml.api.deleteIngestPipeline(modelId))); + await ml.api.cleanMlIndices(); + }); + + it('returns trained model pipelines by id', async () => { + const { body } = await supertest + .get(`/api/ml/trained_models/dfa_regression_model_n_0/pipelines`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.length).to.eql(1); + expect(body[0].model_id).to.eql('dfa_regression_model_n_0'); + expect(Object.keys(body[0].pipelines).length).to.eql(1); + }); + + it('returns an error in case user does not have required permission', async () => { + await supertest + .get(`/api/ml/trained_models/dfa_regression_model_n_0/pipelines`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(403); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/trained_models/get_model_stats.ts b/x-pack/test/api_integration/apis/ml/trained_models/get_model_stats.ts new file mode 100644 index 0000000000000..76f108836996f --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/trained_models/get_model_stats.ts @@ -0,0 +1,54 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; + +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('GET trained_models/_stats', () => { + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.api.createdTestTrainedModels('regression', 2); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('returns trained model stats by id', async () => { + const { body } = await supertest + .get(`/api/ml/trained_models/dfa_regression_model_n_0/_stats`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.count).to.eql(1); + expect(body.trained_model_stats[0].model_id).to.eql('dfa_regression_model_n_0'); + }); + + it('returns 404 if requested trained model does not exist', async () => { + await supertest + .get(`/api/ml/trained_models/not_existing_model/_stats`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + }); + + it('returns an error for unauthorized user', async () => { + await supertest + .get(`/api/ml/trained_models/dfa_regression_model_n_0/_stats`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .expect(403); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/trained_models/get_models.ts b/x-pack/test/api_integration/apis/ml/trained_models/get_models.ts new file mode 100644 index 0000000000000..604dff6a98a9a --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/trained_models/get_models.ts @@ -0,0 +1,88 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; + +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('GET trained_models', () => { + let testModelIds: string[] = []; + + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + testModelIds = await ml.api.createdTestTrainedModels('regression', 5, true); + await ml.api.createModelAlias('dfa_regression_model_n_0', 'dfa_regression_model_alias'); + await ml.api.createIngestPipeline('dfa_regression_model_alias'); + }); + + after(async () => { + // delete created ingest pipelines + await Promise.all( + ['dfa_regression_model_alias', ...testModelIds].map((modelId) => + ml.api.deleteIngestPipeline(modelId) + ) + ); + await ml.api.cleanMlIndices(); + }); + + it('returns all trained models with associated pipelines including aliases', async () => { + const { body } = await supertest + .get(`/api/ml/trained_models?with_pipelines=true`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + // Created models + system model + expect(body.length).to.eql(6); + + const sampleModel = body.find((v: any) => v.model_id === 'dfa_regression_model_n_0'); + expect(Object.keys(sampleModel.pipelines).length).to.eql(2); + }); + + it('returns models without pipeline in case user does not have required permission', async () => { + const { body } = await supertest + .get(`/api/ml/trained_models?with_pipelines=true`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + // Created models + system model + expect(body.length).to.eql(6); + const sampleModel = body.find((v: any) => v.model_id === 'dfa_regression_model_n_0'); + expect(sampleModel.pipelines).to.eql(undefined); + }); + + it('returns trained model by id', async () => { + const { body } = await supertest + .get(`/api/ml/trained_models/dfa_regression_model_n_1`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + expect(body.length).to.eql(1); + expect(body[0].model_id).to.eql('dfa_regression_model_n_1'); + }); + + it('returns 404 if requested trained model does not exist', async () => { + await supertest + .get(`/api/ml/trained_models/not_existing_model`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + }); + + it('returns an error for unauthorized user', async () => { + await supertest + .get(`/api/ml/trained_models/dfa_regression_model_n_1`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .expect(403); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/trained_models/index.ts b/x-pack/test/api_integration/apis/ml/trained_models/index.ts new file mode 100644 index 0000000000000..d1812dc188b00 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/trained_models/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('trained models', function () { + loadTestFile(require.resolve('./get_models')); + loadTestFile(require.resolve('./get_model_stats')); + loadTestFile(require.resolve('./get_model_pipelines')); + loadTestFile(require.resolve('./delete_model')); + }); +} diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index 2468bfec63321..2576a5eaf9bc9 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -21,7 +21,23 @@ export default function ({ getService }: FtrProviderContext) { // If you're removing a privilege, this breaks backwards compatibility // Roles are associated with these privileges, and we shouldn't be removing them in a minor version. const expected = { + global: ['all', 'read'], + space: ['all', 'read'], features: { + graph: ['all', 'read'], + savedObjectsTagging: ['all', 'read'], + canvas: ['all', 'read', 'minimal_all', 'minimal_read', 'generate_report'], + maps: ['all', 'read'], + fleet: ['all', 'read'], + actions: ['all', 'read'], + stackAlerts: ['all', 'read'], + ml: ['all', 'read'], + siem: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_all', 'cases_read'], + observabilityCases: ['all', 'read'], + uptime: ['all', 'read'], + infrastructure: ['all', 'read'], + logs: ['all', 'read'], + apm: ['all', 'read', 'minimal_all', 'minimal_read', 'alerts_all', 'alerts_read'], discover: [ 'all', 'read', @@ -53,24 +69,8 @@ export default function ({ getService }: FtrProviderContext) { advancedSettings: ['all', 'read'], indexPatterns: ['all', 'read'], savedObjectsManagement: ['all', 'read'], - savedObjectsTagging: ['all', 'read'], timelion: ['all', 'read'], - graph: ['all', 'read'], - maps: ['all', 'read'], - canvas: ['all', 'read', 'minimal_all', 'minimal_read', 'generate_report'], - infrastructure: ['all', 'read'], - logs: ['all', 'read'], - observabilityCases: ['all', 'read'], - uptime: ['all', 'read'], - apm: ['all', 'read'], - ml: ['all', 'read'], - siem: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_all', 'cases_read'], - fleet: ['all', 'read'], - stackAlerts: ['all', 'read'], - actions: ['all', 'read'], }, - global: ['all', 'read'], - space: ['all', 'read'], reserved: ['ml_user', 'ml_admin', 'ml_apm_user', 'monitoring'], }; diff --git a/x-pack/test/api_integration/apis/security_solution/events.ts b/x-pack/test/api_integration/apis/security_solution/events.ts index ff4256f1a1adf..2150b022ac425 100644 --- a/x-pack/test/api_integration/apis/security_solution/events.ts +++ b/x-pack/test/api_integration/apis/security_solution/events.ts @@ -7,6 +7,11 @@ import expect from '@kbn/expect'; +import { secOnly } from '../../../rule_registry/common/lib/authentication/users'; +import { + createSpacesAndUsers, + deleteSpacesAndUsers, +} from '../../../rule_registry/common/lib/authentication/'; import { Direction, TimelineEventsQueries, @@ -407,10 +412,19 @@ export default function ({ getService }: FtrProviderContext) { const retry = getService('retry'); const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); describe('Timeline', () => { - before(() => esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts')); - after(() => esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts')); + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); + await createSpacesAndUsers(getService); + }); + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); + await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); + await deleteSpacesAndUsers(getService); + }); it('Make sure that we get Timeline data', async () => { await retry.try(async () => { @@ -454,6 +468,60 @@ export default function ({ getService }: FtrProviderContext) { }); }); + // TODO: unskip this test once authz is added to search strategy + it.skip('Make sure that we get Timeline data using the hunter role and do not receive observability alerts', async () => { + await retry.try(async () => { + const requestBody = { + defaultIndex: ['.alerts*'], // query both .alerts-observability-apm and .alerts-security-solution + docValueFields: [], + factoryQueryType: TimelineEventsQueries.all, + fieldRequested: FIELD_REQUESTED, + // fields: [], + filterQuery: { + bool: { + filter: [ + { + match_all: {}, + }, + ], + }, + }, + pagination: { + activePage: 0, + querySize: 25, + }, + language: 'kuery', + sort: [ + { + field: '@timestamp', + direction: Direction.desc, + type: 'number', + }, + ], + timerange: { + from: FROM, + to: TO, + interval: '12h', + }, + }; + const resp = await supertestWithoutAuth + .post('/internal/search/securitySolutionTimelineSearchStrategy/') + .auth(secOnly.username, secOnly.password) // using security 'hunter' role + .set('kbn-xsrf', 'true') + .set('Content-Type', 'application/json') + .send(requestBody) + .expect(200); + + const timeline = resp.body; + + // we inject one alert into the security solutions alerts index and another alert into the observability alerts index + // therefore when accessing the .alerts* index with the security solution user, + // only security solution alerts should be returned since the security solution user + // is not authorized to view observability alerts. + expect(timeline.totalCount).to.be(1); + }); + }); + it('Make sure that pagination is working in Timeline query', async () => { await retry.try(async () => { const resp = await supertest diff --git a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts b/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts index e07681afe2203..4e3740a1ccb1c 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts @@ -367,6 +367,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { "kibana.rac.alert.id": Array [ "apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED", ], + "kibana.rac.alert.owner": Array [ + "apm", + ], "kibana.rac.alert.producer": Array [ "apm", ], @@ -437,6 +440,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { "kibana.rac.alert.id": Array [ "apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED", ], + "kibana.rac.alert.owner": Array [ + "apm", + ], "kibana.rac.alert.producer": Array [ "apm", ], @@ -541,6 +547,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { "kibana.rac.alert.id": Array [ "apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED", ], + "kibana.rac.alert.owner": Array [ + "apm", + ], "kibana.rac.alert.producer": Array [ "apm", ], diff --git a/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts b/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts index 36eef019f7bf7..ed79d7200c4ed 100644 --- a/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts +++ b/x-pack/test/fleet_api_integration/apis/fleet_telemetry.ts @@ -30,6 +30,13 @@ export default function (providerContext: FtrProviderContext) { case 'offline': data = { last_checkin: '2017-06-07T18:59:04.498Z' }; break; + // Agent with last checkin status as error and currently unenrolling => should displayd updating status + case 'error-unenrolling': + data = { + last_checkin_status: 'error', + unenrollment_started_at: '2017-06-07T18:59:04.498Z', + }; + break; default: data = { last_checkin: new Date().toISOString() }; } @@ -95,6 +102,7 @@ export default function (providerContext: FtrProviderContext) { await generateAgent('offline', defaultServerPolicy.id); await generateAgent('error', defaultServerPolicy.id); await generateAgent('degraded', defaultServerPolicy.id); + await generateAgent('error-unenrolling', defaultServerPolicy.id); }); it('should return the correct telemetry values for fleet', async () => { @@ -109,12 +117,12 @@ export default function (providerContext: FtrProviderContext) { .expect(200); expect(apiResponse.stack_stats.kibana.plugins.fleet.agents).eql({ - total_enrolled: 7, + total_enrolled: 8, healthy: 3, unhealthy: 3, offline: 1, - updating: 0, - total_all_statuses: 7, + updating: 1, + total_all_statuses: 8, }); expect(apiResponse.stack_stats.kibana.plugins.fleet.fleet_server).eql({ diff --git a/x-pack/test/functional/apps/canvas/smoke_test.js b/x-pack/test/functional/apps/canvas/smoke_test.js index fcc04aafdbcd8..cb29840c4b2aa 100644 --- a/x-pack/test/functional/apps/canvas/smoke_test.js +++ b/x-pack/test/functional/apps/canvas/smoke_test.js @@ -6,7 +6,6 @@ */ import expect from '@kbn/expect'; -import { parse } from 'url'; export default function canvasSmokeTest({ getService, getPageObjects }) { const testSubjects = getService('testSubjects'); @@ -45,7 +44,7 @@ export default function canvasSmokeTest({ getService, getPageObjects }) { const url = await browser.getCurrentUrl(); // remove all the search params, just compare the route - const hashRoute = parse(url).hash.split('?')[0]; + const hashRoute = new URL(url).hash.split('?')[0]; expect(hashRoute).to.equal(`#/workpad/${testWorkpadId}/page/1`); }); }); diff --git a/x-pack/test/functional/apps/lens/formula.ts b/x-pack/test/functional/apps/lens/formula.ts index 8b87db21a1ffe..6148215d8b6d2 100644 --- a/x-pack/test/functional/apps/lens/formula.ts +++ b/x-pack/test/functional/apps/lens/formula.ts @@ -16,7 +16,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const fieldEditor = getService('fieldEditor'); - describe('lens formula', () => { + // FLAKY: https://github.com/elastic/kibana/issues/105016 + describe.skip('lens formula', () => { it('should transition from count to formula', async () => { await PageObjects.visualize.gotoVisualizationLandingPage(); await listingTable.searchForItemWithName('lnsXYvis'); diff --git a/x-pack/test/functional/apps/monitoring/cluster/list.js b/x-pack/test/functional/apps/monitoring/cluster/list.js index f88e30f717141..09361f88f5652 100644 --- a/x-pack/test/functional/apps/monitoring/cluster/list.js +++ b/x-pack/test/functional/apps/monitoring/cluster/list.js @@ -26,6 +26,8 @@ export default function ({ getService, getPageObjects }) { to: 'Aug 16, 2017 @ 00:00:00.000', }); + await clusterList.closeAlertsModal(); + await clusterList.assertDefaults(); }); @@ -83,6 +85,8 @@ export default function ({ getService, getPageObjects }) { to: 'Sep 7, 2017 @ 20:18:55.733', }); + await clusterList.closeAlertsModal(); + await clusterList.assertDefaults(); }); diff --git a/x-pack/test/functional/apps/monitoring/elasticsearch/nodes_mb.js b/x-pack/test/functional/apps/monitoring/elasticsearch/nodes_mb.js index 7e9a0fec70824..a031b828e2632 100644 --- a/x-pack/test/functional/apps/monitoring/elasticsearch/nodes_mb.js +++ b/x-pack/test/functional/apps/monitoring/elasticsearch/nodes_mb.js @@ -263,7 +263,7 @@ export default function ({ getService, getPageObjects }) { } ); - overview.closeAlertsModal(); + await overview.closeAlertsModal(); // go to nodes listing await overview.clickEsNodes(); diff --git a/x-pack/test/functional/apps/monitoring/index.js b/x-pack/test/functional/apps/monitoring/index.js index 24ace88f334f0..213007c7b71df 100644 --- a/x-pack/test/functional/apps/monitoring/index.js +++ b/x-pack/test/functional/apps/monitoring/index.js @@ -46,7 +46,7 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./beats/listing')); loadTestFile(require.resolve('./beats/beat_detail')); - loadTestFile(require.resolve('./time_filter')); + // loadTestFile(require.resolve('./time_filter')); loadTestFile(require.resolve('./enable_monitoring')); loadTestFile(require.resolve('./setup/metricbeat_migration')); diff --git a/x-pack/test/functional/es_archives/rule_registry/alerts/data.json b/x-pack/test/functional/es_archives/rule_registry/alerts/data.json new file mode 100644 index 0000000000000..a9837210c2e5a --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_registry/alerts/data.json @@ -0,0 +1,29 @@ +{ + "type": "doc", + "value": { + "index": ".alerts-observability-apm", + "id": "NoxgpHkBqbdrfX07MqXV", + "source": { + "@timestamp": "2020-12-16T15:16:18.570Z", + "rule.id": "apm.error_rate", + "message": "hello world 1", + "kibana.rac.alert.owner": "apm", + "kibana.rac.alert.status": "open" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".alerts-security.alerts", + "id": "020202", + "source": { + "@timestamp": "2020-12-16T15:16:18.570Z", + "rule.id": "siem.signals", + "message": "hello world security", + "kibana.rac.alert.owner": "siem", + "kibana.rac.alert.status": "open" + } + } +} diff --git a/x-pack/test/functional/es_archives/rule_registry/alerts/mappings.json b/x-pack/test/functional/es_archives/rule_registry/alerts/mappings.json new file mode 100644 index 0000000000000..4cb178d979982 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_registry/alerts/mappings.json @@ -0,0 +1,47 @@ +{ + "type": "index", + "value": { + "index": ".alerts-observability-apm", + "mappings": { + "properties": { + "message": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "kibana.rac.alert.owner": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } +} + +{ + "type": "index", + "value": { + "index": ".alerts-security.alerts", + "mappings": { + "properties": { + "message": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "kibana.rac.alert.owner": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } +} diff --git a/x-pack/test/functional/es_archives/visualize/default/data.json b/x-pack/test/functional/es_archives/visualize/default/data.json index 7d0ad0c25f96d..a16e1676611ce 100644 --- a/x-pack/test/functional/es_archives/visualize/default/data.json +++ b/x-pack/test/functional/es_archives/visualize/default/data.json @@ -157,7 +157,7 @@ "timeFieldName": "@timestamp", "title": "logstash-2015.09.22", "fields":"[{\"name\":\"scripted_date\",\"type\":\"date\",\"count\":0,\"scripted\":true,\"script\":\"1234\",\"lang\":\"painless\",\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"scripted_string\",\"type\":\"string\",\"count\":0,\"scripted\":true,\"script\":\"return 'hello'\",\"lang\":\"painless\",\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false}]", - "runtimeFieldMap":"{\"runtime_string_field\":{\"type\":\"keyword\",\"script\":{\"source\":\"emit('hello world!')\"}}}" + "runtimeFieldMap":"{\"runtime_string_field\":{\"type\":\"keyword\",\"script\":{\"source\":\"emit('hello world!')\"}},\"runtime_number_field\":{\"type\":\"double\",\"script\":{\"source\":\"emit(5)\"}}}" }, "migrationVersion": { "index-pattern": "7.11.0" diff --git a/x-pack/test/functional/services/index.ts b/x-pack/test/functional/services/index.ts index 99293c71676b4..273db212400ab 100644 --- a/x-pack/test/functional/services/index.ts +++ b/x-pack/test/functional/services/index.ts @@ -9,6 +9,7 @@ import { services as kibanaFunctionalServices } from '../../../../test/functiona import { services as kibanaApiIntegrationServices } from '../../../../test/api_integration/services'; import { services as kibanaXPackApiIntegrationServices } from '../../api_integration/services'; import { services as commonServices } from '../../common/services'; +import { ReportingFunctionalProvider } from '../../reporting_functional/services'; import { MonitoringNoDataProvider, @@ -107,5 +108,6 @@ export const services = { dashboardDrilldownPanelActions: DashboardDrilldownPanelActionsProvider, dashboardDrilldownsManage: DashboardDrilldownsManageProvider, dashboardPanelTimeRange: DashboardPanelTimeRangeProvider, + reporting: ReportingFunctionalProvider, searchSessions: SearchSessionsService, }; diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index 728e3ff8fc8e6..ec5ca4c661157 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -8,6 +8,8 @@ import { estypes } from '@elastic/elasticsearch'; import expect from '@kbn/expect'; import { ProvidedType } from '@kbn/test'; +import fs from 'fs'; +import path from 'path'; import { Calendar } from '../../../../plugins/ml/server/models/calendar/index'; import { Annotation } from '../../../../plugins/ml/common/types/annotations'; import { DataFrameAnalyticsConfig } from '../../../../plugins/ml/public/application/data_frame_analytics/common'; @@ -25,6 +27,8 @@ import { import { COMMON_REQUEST_HEADERS } from '../../../functional/services/ml/common_api'; import { PutTrainedModelConfig } from '../../../../plugins/ml/common/types/trained_models'; +type ModelType = 'regression' | 'classification'; + export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { const es = getService('es'); const log = getService('log'); @@ -943,5 +947,93 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { log.debug('> Trained model crated'); return model; }, + + async createdTestTrainedModels( + modelType: ModelType, + count: number = 10, + withIngestPipelines = false + ) { + const compressedDefinition = this.getCompressedModelDefinition(modelType); + + const modelIds = new Array(count).fill(null).map((v, i) => `dfa_${modelType}_model_n_${i}`); + + const models = modelIds.map((id) => { + return { + model_id: id, + body: { + compressed_definition: compressedDefinition, + inference_config: { + [modelType]: {}, + }, + input: { + field_names: ['common_field'], + }, + } as PutTrainedModelConfig, + }; + }); + + for (const model of models) { + await this.createTrainedModel(model.model_id, model.body); + if (withIngestPipelines) { + await this.createIngestPipeline(model.model_id); + } + } + + return modelIds; + }, + + /** + * Retrieves compressed model definition from the test resources. + * @param modelType + */ + getCompressedModelDefinition(modelType: ModelType) { + return fs.readFileSync( + path.resolve( + __dirname, + 'resources', + 'trained_model_definitions', + `minimum_valid_config_${modelType}.json.gz.b64` + ), + 'utf-8' + ); + }, + + async createModelAlias(modelId: string, modelAlias: string) { + log.debug(`Creating alias for model "${modelId}"`); + await esSupertest + .put(`/_ml/trained_models/${modelId}/model_aliases/${modelAlias}`) + .expect(200); + log.debug('> Model alias created'); + }, + + /** + * Creates ingest pipelines for trained model + * @param modelId + */ + async createIngestPipeline(modelId: string) { + log.debug(`Creating ingest pipeline for trained model with id "${modelId}"`); + const ingestPipeline = await esSupertest + .put(`/_ingest/pipeline/pipeline_${modelId}`) + .send({ + processors: [ + { + inference: { + model_id: modelId, + }, + }, + ], + }) + .expect(200) + .then((res) => res.body); + + log.debug('> Ingest pipeline crated'); + return ingestPipeline; + }, + + async deleteIngestPipeline(modelId: string) { + log.debug(`Deleting ingest pipeline for trained model with id "${modelId}"`); + await esSupertest.delete(`/_ingest/pipeline/pipeline_${modelId}`).expect(200); + log.debug('> Ingest pipeline deleted'); + }, }; } diff --git a/x-pack/test/functional/services/ml/trained_models.ts b/x-pack/test/functional/services/ml/trained_models.ts index ae799efbbd30c..7a1fa1714ca14 100644 --- a/x-pack/test/functional/services/ml/trained_models.ts +++ b/x-pack/test/functional/services/ml/trained_models.ts @@ -5,12 +5,9 @@ * 2.0. */ -import fs from 'fs'; -import path from 'path'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; import { MlApi } from './api'; -import { PutTrainedModelConfig } from '../../../../plugins/ml/common/types/trained_models'; import { MlCommonUI } from './common_ui'; type ModelType = 'regression' | 'classification'; @@ -24,38 +21,7 @@ export function TrainedModelsProvider( return { async createdTestTrainedModels(modelType: ModelType, count: number = 10) { - const compressedDefinition = this.getCompressedModelDefinition(modelType); - - const models = new Array(count).fill(null).map((v, i) => { - return { - model_id: `dfa_${modelType}_model_n_${i}`, - body: { - compressed_definition: compressedDefinition, - inference_config: { - [modelType]: {}, - }, - input: { - field_names: ['common_field'], - }, - } as PutTrainedModelConfig, - }; - }); - - for (const model of models) { - await mlApi.createTrainedModel(model.model_id, model.body); - } - }, - - getCompressedModelDefinition(modelType: ModelType) { - return fs.readFileSync( - path.resolve( - __dirname, - 'resources', - 'trained_model_definitions', - `minimum_valid_config_${modelType}.json.gz.b64` - ), - 'utf-8' - ); + await mlApi.createdTestTrainedModels(modelType, count); }, async assertStats(expectedTotalCount: number) { diff --git a/x-pack/test/functional/services/monitoring/cluster_list.js b/x-pack/test/functional/services/monitoring/cluster_list.js index aea82fbb6b793..f63e7b6cd125e 100644 --- a/x-pack/test/functional/services/monitoring/cluster_list.js +++ b/x-pack/test/functional/services/monitoring/cluster_list.js @@ -15,6 +15,7 @@ export function MonitoringClusterListProvider({ getService, getPageObjects }) { const SUBJ_SEARCH_BAR = `${SUBJ_TABLE_CONTAINER} > monitoringTableToolBar`; const SUBJ_CLUSTER_ROW_PREFIX = `${SUBJ_TABLE_CONTAINER} > clusterRow_`; + const ALERTS_MODAL_BUTTON = 'alerts-modal-button'; return new (class ClusterList { async assertDefaults() { @@ -41,6 +42,10 @@ export function MonitoringClusterListProvider({ getService, getPageObjects }) { return PageObjects.monitoring.tableClearFilter(SUBJ_SEARCH_BAR); } + closeAlertsModal() { + return testSubjects.click(ALERTS_MODAL_BUTTON); + } + getClusterLink(clusterUuid) { return testSubjects.find(`${SUBJ_CLUSTER_ROW_PREFIX}${clusterUuid} > clusterLink`); } diff --git a/x-pack/test/load/runner.ts b/x-pack/test/load/runner.ts index 2d379391b2089..0bea5992f5539 100644 --- a/x-pack/test/load/runner.ts +++ b/x-pack/test/load/runner.ts @@ -18,6 +18,7 @@ const simulationPackage = 'org.kibanaLoadTest.simulation'; const simulationFIleExtension = '.scala'; const gatlingProjectRootPath: string = process.env.GATLING_PROJECT_PATH || resolve(REPO_ROOT, '../kibana-load-testing'); +const puppeteerProjectRootPath: string = resolve(gatlingProjectRootPath, 'puppeteer'); const simulationEntry: string = process.env.GATLING_SIMULATIONS || 'branch.DemoJourney'; if (!Fs.existsSync(gatlingProjectRootPath)) { @@ -52,6 +53,15 @@ export async function GatlingTestRunner({ getService }: FtrProviderContext) { const log = getService('log'); await withProcRunner(log, async (procs) => { + await procs.run('node build/index.js', { + cmd: 'node', + args: ['build/index.js'], + cwd: puppeteerProjectRootPath, + env: { + ...process.env, + }, + wait: true, + }); for (let i = 0; i < simulationClasses.length; i++) { await procs.run('gatling: test', { cmd: 'mvn', diff --git a/x-pack/test/reporting_api_integration/reporting_without_security/ilm_migration_apis.ts b/x-pack/test/reporting_api_integration/reporting_without_security/ilm_migration_apis.ts index a0f4a3f91fe32..a9b6798a0224f 100644 --- a/x-pack/test/reporting_api_integration/reporting_without_security/ilm_migration_apis.ts +++ b/x-pack/test/reporting_api_integration/reporting_without_security/ilm_migration_apis.ts @@ -47,7 +47,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('detects when reporting indices should be migrated due to missing ILM policy', async () => { - await reportingAPI.makeAllReportingPoliciesUnmanaged(); + await reportingAPI.makeAllReportingIndicesUnmanaged(); // TODO: Remove "any" when no longer through type issue "policy_id" missing await es.ilm.deleteLifecycle({ policy: ILM_POLICY_NAME } as any); @@ -63,7 +63,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('detects when reporting indices should be migrated due to unmanaged indices', async () => { - await reportingAPI.makeAllReportingPoliciesUnmanaged(); + await reportingAPI.makeAllReportingIndicesUnmanaged(); await supertestNoAuth .post(`/api/reporting/generate/csv`) .set('kbn-xsrf', 'xxx') diff --git a/x-pack/test/reporting_api_integration/services/scenarios.ts b/x-pack/test/reporting_api_integration/services/scenarios.ts index eb32de9d0dc9c..08c07e0e257ed 100644 --- a/x-pack/test/reporting_api_integration/services/scenarios.ts +++ b/x-pack/test/reporting_api_integration/services/scenarios.ts @@ -181,8 +181,8 @@ export function createScenarios({ getService }: Pick { - log.debug('ReportingAPI.makeAllReportingPoliciesUnmanaged'); + const makeAllReportingIndicesUnmanaged = async () => { + log.debug('ReportingAPI.makeAllReportingIndicesUnmanaged'); const settings: any = { 'index.lifecycle.name': null, }; @@ -214,6 +214,6 @@ export function createScenarios({ getService }: Pick { + const xPackApiIntegrationTestsConfig = await readConfigFile( + require.resolve('../../api_integration/config.ts') + ); + + const servers = { + ...xPackApiIntegrationTestsConfig.get('servers'), + elasticsearch: { + ...xPackApiIntegrationTestsConfig.get('servers.elasticsearch'), + protocol: ssl ? 'https' : 'http', + }, + }; + + return { + testFiles: testFiles ? testFiles : [require.resolve('../tests/common')], + servers, + services, + junit: { + reportName: 'X-Pack Rule Registry Alerts Client API Integration Tests', + }, + esTestCluster: { + ...xPackApiIntegrationTestsConfig.get('esTestCluster'), + license, + ssl, + serverArgs: [ + `xpack.license.self_generated.type=${license}`, + `xpack.security.enabled=${ + !disabledPlugins.includes('security') && ['trial', 'basic'].includes(license) + }`, + ], + }, + kbnTestServer: { + ...xPackApiIntegrationTestsConfig.get('kbnTestServer'), + serverArgs: [ + ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'), + `--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`, + `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, + '--xpack.eventLog.logEntries=true', + ...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`), + `--server.xsrf.whitelist=${JSON.stringify(getAllExternalServiceSimulatorPaths())}`, + ...(ssl + ? [ + `--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, + `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, + ] + : []), + ], + }, + }; + }; +} diff --git a/x-pack/plugins/console_extensions/server/config.ts b/x-pack/test/rule_registry/common/ftr_provider_context.d.ts similarity index 56% rename from x-pack/plugins/console_extensions/server/config.ts rename to x-pack/test/rule_registry/common/ftr_provider_context.d.ts index 15b06bf93ffbe..aa56557c09df8 100644 --- a/x-pack/plugins/console_extensions/server/config.ts +++ b/x-pack/test/rule_registry/common/ftr_provider_context.d.ts @@ -5,10 +5,8 @@ * 2.0. */ -import { schema, TypeOf } from '@kbn/config-schema'; +import { GenericFtrProviderContext } from '@kbn/test'; -export type ConfigType = TypeOf; +import { services } from './services'; -export const config = schema.object({ - enabled: schema.boolean({ defaultValue: true }), -}); +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/rule_registry/common/lib/authentication/index.ts b/x-pack/test/rule_registry/common/lib/authentication/index.ts new file mode 100644 index 0000000000000..f76159976a902 --- /dev/null +++ b/x-pack/test/rule_registry/common/lib/authentication/index.ts @@ -0,0 +1,105 @@ +/* + * 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 { FtrProviderContext as CommonFtrProviderContext } from '../../../common/ftr_provider_context'; +import { Role, User, UserInfo } from './types'; +import { allUsers } from './users'; +import { allRoles } from './roles'; +import { spaces } from './spaces'; + +export const getUserInfo = (user: User): UserInfo => ({ + username: user.username, + full_name: user.username.replace('_', ' '), + email: `${user.username}@elastic.co`, +}); + +export const createSpaces = async (getService: CommonFtrProviderContext['getService']) => { + const spacesService = getService('spaces'); + for (const space of spaces) { + await spacesService.create(space); + } +}; + +/** + * Creates the users and roles for use in the tests. Defaults to specific users and roles used by the security_and_spaces + * scenarios but can be passed specific ones as well. + */ +export const createUsersAndRoles = async ( + getService: CommonFtrProviderContext['getService'], + usersToCreate: User[] = allUsers, + rolesToCreate: Role[] = allRoles +) => { + const security = getService('security'); + + const createRole = async ({ name, privileges }: Role) => { + return security.role.create(name, privileges); + }; + + const createUser = async (user: User) => { + const userInfo = getUserInfo(user); + + return security.user.create(user.username, { + password: user.password, + roles: user.roles, + full_name: userInfo.full_name, + email: userInfo.email, + }); + }; + + for (const role of rolesToCreate) { + await createRole(role); + } + + for (const user of usersToCreate) { + await createUser(user); + } +}; + +export const deleteSpaces = async (getService: CommonFtrProviderContext['getService']) => { + const spacesService = getService('spaces'); + for (const space of spaces) { + try { + await spacesService.delete(space.id); + } catch (error) { + // ignore errors because if a migration is run it will delete the .kibana index which remove the spaces and users + } + } +}; + +export const deleteUsersAndRoles = async ( + getService: CommonFtrProviderContext['getService'], + usersToDelete: User[] = allUsers, + rolesToDelete: Role[] = allRoles +) => { + const security = getService('security'); + + for (const user of usersToDelete) { + try { + await security.user.delete(user.username); + } catch (error) { + // ignore errors because if a migration is run it will delete the .kibana index which remove the spaces and users + } + } + + for (const role of rolesToDelete) { + try { + await security.role.delete(role.name); + } catch (error) { + // ignore errors because if a migration is run it will delete the .kibana index which remove the spaces and users + } + } +}; + +export const createSpacesAndUsers = async (getService: CommonFtrProviderContext['getService']) => { + await createSpaces(getService); + await createUsersAndRoles(getService); +}; + +export const deleteSpacesAndUsers = async (getService: CommonFtrProviderContext['getService']) => { + await deleteSpaces(getService); + await deleteUsersAndRoles(getService); +}; diff --git a/x-pack/test/rule_registry/common/lib/authentication/roles.ts b/x-pack/test/rule_registry/common/lib/authentication/roles.ts new file mode 100644 index 0000000000000..e38378dcfc8f2 --- /dev/null +++ b/x-pack/test/rule_registry/common/lib/authentication/roles.ts @@ -0,0 +1,435 @@ +/* + * 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 { Role } from './types'; + +export const noKibanaPrivileges: Role = { + name: 'no_kibana_privileges', + privileges: { + elasticsearch: { + indices: [], + }, + }, +}; + +export const globalRead: Role = { + name: 'global_read', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + siem: ['read'], + apm: ['read'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const securitySolutionOnlyAll: Role = { + name: 'sec_only_all_spaces_space1', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + siem: ['all'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const securitySolutionOnlyAllSpace2: Role = { + name: 'sec_only_all_spaces_space2', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + siem: ['all'], + }, + spaces: ['space2'], + }, + ], + }, +}; + +export const securitySolutionOnlyRead: Role = { + name: 'sec_only_read_spaces_space1', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + siem: ['read'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const securitySolutionOnlyReadSpace2: Role = { + name: 'sec_only_read_spaces_space2', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + siem: ['read'], + }, + spaces: ['space2'], + }, + ], + }, +}; + +export const observabilityOnlyAll: Role = { + name: 'obs_only_all_spaces_space1', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['all'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const observabilityOnlyAllSpace2: Role = { + name: 'obs_only_all_spaces_space2', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['all'], + }, + spaces: ['space2'], + }, + ], + }, +}; + +export const observabilityOnlyRead: Role = { + name: 'obs_only_read_spaces_space1', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['read'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const observabilityOnlyReadSpace2: Role = { + name: 'obs_only_read_spaces_space2', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['read'], + }, + spaces: ['space2'], + }, + ], + }, +}; + +/** + * These roles have access to all spaces. + */ +export const securitySolutionOnlyAllSpacesAll: Role = { + name: 'sec_only_all_spaces_all', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + siem: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const securitySolutionOnlyReadSpacesAll: Role = { + name: 'sec_only_read_spaces_all', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + siem: ['read'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const observabilityOnlyAllSpacesAll: Role = { + name: 'obs_only_all_spaces_all', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const observabilityOnlyReadSpacesAll: Role = { + name: 'obs_only_read_all_spaces_all', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['read'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const roles = [ + noKibanaPrivileges, + globalRead, + securitySolutionOnlyAll, + securitySolutionOnlyRead, + observabilityOnlyAll, + observabilityOnlyRead, + observabilityOnlyReadSpacesAll, +]; + +/** + * These roles are only to be used in the 'trial' tests + * since they rely on subfeature privileges which are a gold licencse feature + * maybe put these roles into a separate roles file like "trial_roles"? + */ +export const observabilityMinReadAlertsRead: Role = { + name: 'obs_only_alerts_read', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['minimal_read', 'alerts_read'], + ruleRegistry: ['all'], + actions: ['read'], + builtInAlerts: ['all'], + alerting: ['all'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const observabilityMinReadAlertsReadSpacesAll: Role = { + name: 'obs_minimal_read_alerts_read_spaces_all', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['minimal_read', 'alerts_read'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const observabilityMinimalRead: Role = { + name: 'obs_minimal_read', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['minimal_read'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const observabilityMinimalReadSpacesAll: Role = { + name: 'obs_minimal_read_spaces_all', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['minimal_read'], + }, + spaces: ['*'], + }, + ], + }, +}; + +/** + * **************************************** + * These are used for testing update alerts privileges + * **************************************** + * **************************************** + * **************************************** + * **************************************** + * **************************************** + * **************************************** + * **************************************** + * **************************************** + */ + +export const observabilityMinReadAlertsAll: Role = { + name: 'obs_only_alerts_all', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['minimal_read', 'alerts_all'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const observabilityMinReadAlertsAllSpacesAll: Role = { + name: 'obs_minimal_read_alerts_all_spaces_all', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['minimal_read', 'alerts_all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const observabilityMinimalAll: Role = { + name: 'obs_minimal_all', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['minimal_all'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const observabilityMinimalAllSpacesAll: Role = { + name: 'obs_minimal_all_spaces_all', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + apm: ['minimal_all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const allRoles = [ + noKibanaPrivileges, + globalRead, + securitySolutionOnlyAll, + securitySolutionOnlyRead, + observabilityOnlyAll, + observabilityOnlyRead, + securitySolutionOnlyAllSpacesAll, + securitySolutionOnlyReadSpacesAll, + observabilityOnlyAllSpacesAll, + observabilityOnlyReadSpacesAll, + observabilityMinReadAlertsRead, + observabilityMinReadAlertsReadSpacesAll, + observabilityMinimalRead, + observabilityMinimalReadSpacesAll, + observabilityMinReadAlertsAll, + observabilityMinReadAlertsAllSpacesAll, + observabilityMinimalAll, + observabilityMinimalAllSpacesAll, + securitySolutionOnlyAllSpace2, + securitySolutionOnlyReadSpace2, + observabilityOnlyAllSpace2, + observabilityOnlyReadSpace2, +]; diff --git a/x-pack/test/rule_registry/common/lib/authentication/spaces.ts b/x-pack/test/rule_registry/common/lib/authentication/spaces.ts new file mode 100644 index 0000000000000..556b1686601ff --- /dev/null +++ b/x-pack/test/rule_registry/common/lib/authentication/spaces.ts @@ -0,0 +1,26 @@ +/* + * 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 { Space } from './types'; + +const space1: Space = { + id: 'space1', + name: 'Space 1', + disabledFeatures: [], +}; + +const space2: Space = { + id: 'space2', + name: 'Space 2', + disabledFeatures: [], +}; + +export const spaces: Space[] = [space1, space2]; + +export const getSpaceUrlPrefix = (spaceId?: string) => { + return spaceId && spaceId !== 'default' ? `/s/${spaceId}` : ``; +}; diff --git a/x-pack/test/rule_registry/common/lib/authentication/types.ts b/x-pack/test/rule_registry/common/lib/authentication/types.ts new file mode 100644 index 0000000000000..3bf3629441f93 --- /dev/null +++ b/x-pack/test/rule_registry/common/lib/authentication/types.ts @@ -0,0 +1,54 @@ +/* + * 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 Space { + id: string; + namespace?: string; + name: string; + disabledFeatures: string[]; +} + +export interface User { + username: string; + password: string; + description?: string; + roles: string[]; +} + +export interface UserInfo { + username: string; + full_name: string; + email: string; +} + +interface FeaturesPrivileges { + [featureId: string]: string[]; +} + +interface ElasticsearchIndices { + names: string[]; + privileges: string[]; +} + +export interface ElasticSearchPrivilege { + cluster?: string[]; + indices?: ElasticsearchIndices[]; +} + +export interface KibanaPrivilege { + spaces: string[]; + base?: string[]; + feature?: FeaturesPrivileges; +} + +export interface Role { + name: string; + privileges: { + elasticsearch?: ElasticSearchPrivilege; + kibana?: KibanaPrivilege[]; + }; +} diff --git a/x-pack/test/rule_registry/common/lib/authentication/users.ts b/x-pack/test/rule_registry/common/lib/authentication/users.ts new file mode 100644 index 0000000000000..e142b3d1f56a3 --- /dev/null +++ b/x-pack/test/rule_registry/common/lib/authentication/users.ts @@ -0,0 +1,301 @@ +/* + * 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 { + securitySolutionOnlyAll, + observabilityOnlyAll, + securitySolutionOnlyRead, + observabilityOnlyRead, + globalRead as globalReadRole, + noKibanaPrivileges as noKibanaPrivilegesRole, + securitySolutionOnlyAllSpacesAll, + securitySolutionOnlyReadSpacesAll, + observabilityOnlyAllSpacesAll, + observabilityOnlyReadSpacesAll, + // trial license roles + observabilityMinReadAlertsAll, + observabilityMinReadAlertsRead, + observabilityMinReadAlertsReadSpacesAll, + observabilityMinimalRead, + observabilityMinimalReadSpacesAll, + securitySolutionOnlyAllSpace2, + securitySolutionOnlyReadSpace2, + observabilityOnlyAllSpace2, + observabilityOnlyReadSpace2, + observabilityMinReadAlertsAllSpacesAll, +} from './roles'; +import { User } from './types'; + +export const superUser: User = { + username: 'superuser', + password: 'superuser', + roles: ['superuser'], +}; + +export const secOnly: User = { + username: 'sec_only_all_spaces_space1', + password: 'sec_only_all_spaces_space1', + roles: [securitySolutionOnlyAll.name], +}; + +export const secOnlySpace2: User = { + username: 'sec_only_all_spaces_space2', + password: 'sec_only_all_spaces_space2', + roles: [securitySolutionOnlyAllSpace2.name], +}; + +export const secOnlyRead: User = { + username: 'sec_only_read_spaces_space1', + password: 'sec_only_read_spaces_space1', + roles: [securitySolutionOnlyRead.name], +}; + +export const secOnlyReadSpace2: User = { + username: 'sec_only_read_spaces_space2', + password: 'sec_only_read_spaces_space2', + roles: [securitySolutionOnlyReadSpace2.name], +}; + +export const obsOnly: User = { + username: 'obs_only_all_spaces_space1', + password: 'obs_only_all_spaces_space1', + roles: [observabilityOnlyAll.name], +}; + +export const obsOnlySpace2: User = { + username: 'obs_only_all_spaces_space2', + password: 'obs_only_all_spaces_space2', + roles: [observabilityOnlyAllSpace2.name], +}; + +export const obsOnlyRead: User = { + username: 'obs_only_read_spaces_space1', + password: 'obs_only_read_spaces_space1', + roles: [observabilityOnlyRead.name], +}; + +export const obsOnlyReadSpace2: User = { + username: 'obs_only_read_spaces_space2', + password: 'obs_only_read_spaces_space2', + roles: [observabilityOnlyReadSpace2.name], +}; + +export const obsSec: User = { + username: 'sec_only_all_spaces_space1_and_obs_only_all_spaces_space1', + password: 'sec_only_all_spaces_space1_and_obs_only_all_spaces_space1', + roles: [securitySolutionOnlyAll.name, observabilityOnlyAll.name], +}; + +export const obsSecAllSpace2: User = { + username: 'sec_only_all_spaces_space2_and_obs_only_all_spaces_space2', + password: 'sec_only_all_spaces_space2_and_obs_only_all_spaces_space2', + roles: [securitySolutionOnlyAllSpace2.name, observabilityOnlyAllSpace2.name], +}; + +export const obsSecRead: User = { + username: 'sec_only_read_spaces_space1_and_obs_only_read_spaces_space1', + password: 'sec_only_read_spaces_space1_and_obs_only_read_spaces_space1', + roles: [securitySolutionOnlyRead.name, observabilityOnlyRead.name], +}; + +export const obsSecReadSpace2: User = { + username: 'sec_only_read_spaces_space2_and_obs_only_read_spaces_space2', + password: 'sec_only_read_spaces_space2_and_obs_only_read_spaces_space2', + roles: [securitySolutionOnlyReadSpace2.name, observabilityOnlyReadSpace2.name], +}; + +export const globalRead: User = { + username: 'global_read', + password: 'global_read', + roles: [globalReadRole.name], +}; + +export const noKibanaPrivileges: User = { + username: 'no_kibana_privileges', + password: 'no_kibana_privileges', + roles: [noKibanaPrivilegesRole.name], +}; + +export const obsOnlyReadSpacesAll: User = { + username: 'obs_only_read_all_spaces_all', + password: 'obs_only_read_all_spaces_all', + roles: [observabilityOnlyReadSpacesAll.name], +}; + +export const users = [ + superUser, + secOnly, + secOnlyRead, + obsOnly, + obsOnlyRead, + obsSec, + obsSecRead, + globalRead, + noKibanaPrivileges, + obsOnlyReadSpacesAll, +]; + +/** + * These users will have access to all spaces. + */ + +export const secOnlySpacesAll: User = { + username: 'sec_only_all_spaces_all', + password: 'sec_only_all_spaces_all', + roles: [securitySolutionOnlyAllSpacesAll.name], +}; + +export const secOnlyReadSpacesAll: User = { + username: 'sec_only_read_spaces_all', + password: 'sec_only_read_spaces_all', + roles: [securitySolutionOnlyReadSpacesAll.name], +}; + +export const obsOnlySpacesAll: User = { + username: 'obs_only_all_spaces_all', + password: 'obs_only_all_spaces_all', + roles: [observabilityOnlyAllSpacesAll.name], +}; + +export const obsSecSpacesAll: User = { + username: 'sec_only_all_spaces_all_and_obs_only_all_spaces_all', + password: 'sec_only_all_spaces_all_and_obs_only_all_spaces_all', + roles: [securitySolutionOnlyAllSpacesAll.name, observabilityOnlyAllSpacesAll.name], +}; + +export const obsSecReadSpacesAll: User = { + username: 'sec_only_read_all_spaces_all_and_obs_only_read_all_spaces_all', + password: 'sec_only_read_all_spaces_all_and_obs_only_read_all_spaces_all', + roles: [securitySolutionOnlyReadSpacesAll.name, observabilityOnlyReadSpacesAll.name], +}; + +/** + * These users are for the security_only tests because most of them have access to the default space instead of 'space1' + */ +export const usersDefaultSpace = [ + superUser, + secOnlySpacesAll, + secOnlyReadSpacesAll, + obsOnlySpacesAll, + obsOnlyReadSpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + globalRead, + noKibanaPrivileges, +]; + +/** + * Trial users with trial roles + */ + +// apm: ['minimal_read', 'alerts_read'] +// spaces: ['space1'] +export const obsMinReadAlertsRead: User = { + username: 'obs_minimal_read_alerts_read_single_space', + password: 'obs_minimal_read_alerts_read_single_space', + roles: [observabilityMinReadAlertsRead.name], +}; + +// apm: ['minimal_read', 'alerts_read'] +// spaces: ['*'] +export const obsMinReadAlertsReadSpacesAll: User = { + username: 'obs_minimal_read_alerts_read_all_spaces', + password: 'obs_minimal_read_alerts_read_all_spaces', + roles: [observabilityMinReadAlertsReadSpacesAll.name], +}; + +// apm: ['minimal_read'] +// spaces: ['space1'] +export const obsMinRead: User = { + username: 'obs_minimal_read_single_space', + password: 'obs_minimal_read_single_space', + roles: [observabilityMinimalRead.name], +}; + +// apm: ['minimal_read'] +// spaces: ['*'] +export const obsMinReadSpacesAll: User = { + username: 'obs_minimal_read_all_space', + password: 'obs_minimal_read_all_space', + roles: [observabilityMinimalReadSpacesAll.name], +}; + +// FOR UPDATES +// apm: ['minimal_read', 'alerts_all'] +// spaces: ['space1'] +export const obsMinReadAlertsAll: User = { + username: 'obs_minimal_read_alerts_all_single_space', + password: 'obs_minimal_read_alerts_all_single_space', + roles: [observabilityMinReadAlertsAll.name], +}; + +// apm: ['minimal_read', 'alerts_all'] +// spaces: ['*'] +export const obsMinReadAlertsAllSpacesAll: User = { + username: 'obs_minimal_read_alerts_all_all_spaces', + password: 'obs_minimal_read_alerts_all_all_spaces', + roles: [observabilityMinReadAlertsAllSpacesAll.name], +}; + +// apm: ['minimal_all'] +// spaces: ['space1'] +export const obsMinAll: User = { + username: 'obs_minimal_all_single_space', + password: 'obs_minimal_all_single_space', + roles: [observabilityMinimalRead.name], +}; + +// apm: ['minimal_all'] +// spaces: ['*'] +export const obsMinAllSpacesAll: User = { + username: 'obs_minimal_all_all_space', + password: 'obs_minimal_read_all_space', + roles: [observabilityMinimalReadSpacesAll.name], +}; + +export const trialUsers = [ + obsMinReadAlertsRead, + obsMinReadAlertsReadSpacesAll, + obsMinRead, + obsMinReadSpacesAll, + obsMinReadAlertsAll, + obsMinReadAlertsAllSpacesAll, + obsMinAll, + obsMinAllSpacesAll, +]; + +export const allUsers = [ + superUser, + secOnly, + secOnlyRead, + obsOnly, + obsOnlyRead, + obsSec, + obsSecRead, + globalRead, + noKibanaPrivileges, + obsOnlyReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, + obsOnlySpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + obsMinReadAlertsRead, + obsMinReadAlertsReadSpacesAll, + obsMinRead, + obsMinReadSpacesAll, + obsMinReadAlertsAll, + obsMinReadAlertsAllSpacesAll, + obsMinAll, + obsMinAllSpacesAll, + secOnlySpace2, + secOnlyReadSpace2, + obsOnlySpace2, + obsOnlyReadSpace2, + obsSecAllSpace2, + obsSecReadSpace2, +]; diff --git a/x-pack/test/rule_registry/common/services.ts b/x-pack/test/rule_registry/common/services.ts new file mode 100644 index 0000000000000..7e415338c405f --- /dev/null +++ b/x-pack/test/rule_registry/common/services.ts @@ -0,0 +1,8 @@ +/* + * 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 { services } from '../../api_integration/services'; diff --git a/x-pack/test/rule_registry/security_and_spaces/config_basic.ts b/x-pack/test/rule_registry/security_and_spaces/config_basic.ts new file mode 100644 index 0000000000000..98b7b1abe98e7 --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/config_basic.ts @@ -0,0 +1,15 @@ +/* + * 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 { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('security_and_spaces', { + license: 'basic', + ssl: true, + testFiles: [require.resolve('./tests/basic')], +}); diff --git a/x-pack/test/rule_registry/security_and_spaces/config_trial.ts b/x-pack/test/rule_registry/security_and_spaces/config_trial.ts new file mode 100644 index 0000000000000..b5328fd83c2cb --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/config_trial.ts @@ -0,0 +1,15 @@ +/* + * 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 { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('security_and_spaces', { + license: 'trial', + ssl: true, + testFiles: [require.resolve('./tests/trial')], +}); diff --git a/x-pack/test/rule_registry/security_and_spaces/roles_users_utils/index.ts b/x-pack/test/rule_registry/security_and_spaces/roles_users_utils/index.ts new file mode 100644 index 0000000000000..b320446cbe05f --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/roles_users_utils/index.ts @@ -0,0 +1,124 @@ +/* + * 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 { assertUnreachable } from '../../../../plugins/security_solution/common/utility_types'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + t1AnalystUser, + t2AnalystUser, + hunterUser, + ruleAuthorUser, + socManagerUser, + platformEngineerUser, + detectionsAdminUser, + readerUser, + t1AnalystRole, + t2AnalystRole, + hunterRole, + ruleAuthorRole, + socManagerRole, + platformEngineerRole, + detectionsAdminRole, + readerRole, +} from '../../../../plugins/security_solution/server/lib/detection_engine/scripts/roles_users'; + +import { ROLES } from '../../../../plugins/security_solution/common/test'; + +export const createUserAndRole = async ( + getService: FtrProviderContext['getService'], + role: ROLES +): Promise => { + switch (role) { + case ROLES.detections_admin: + return postRoleAndUser( + ROLES.detections_admin, + detectionsAdminRole, + detectionsAdminUser, + getService + ); + case ROLES.t1_analyst: + return postRoleAndUser(ROLES.t1_analyst, t1AnalystRole, t1AnalystUser, getService); + case ROLES.t2_analyst: + return postRoleAndUser(ROLES.t2_analyst, t2AnalystRole, t2AnalystUser, getService); + case ROLES.hunter: + return postRoleAndUser(ROLES.hunter, hunterRole, hunterUser, getService); + case ROLES.rule_author: + return postRoleAndUser(ROLES.rule_author, ruleAuthorRole, ruleAuthorUser, getService); + case ROLES.soc_manager: + return postRoleAndUser(ROLES.soc_manager, socManagerRole, socManagerUser, getService); + case ROLES.platform_engineer: + return postRoleAndUser( + ROLES.platform_engineer, + platformEngineerRole, + platformEngineerUser, + getService + ); + case ROLES.reader: + return postRoleAndUser(ROLES.reader, readerRole, readerUser, getService); + default: + return assertUnreachable(role); + } +}; + +/** + * Given a roleName and security service this will delete the roleName + * and user + * @param roleName The user and role to delete with the same name + * @param securityService The security service + */ +export const deleteUserAndRole = async ( + getService: FtrProviderContext['getService'], + roleName: ROLES +): Promise => { + const securityService = getService('security'); + await securityService.user.delete(roleName); + await securityService.role.delete(roleName); +}; + +interface UserInterface { + password: string; + roles: string[]; + full_name: string; + email: string; +} + +interface RoleInterface { + elasticsearch: { + cluster: string[]; + indices: Array<{ + names: string[]; + privileges: string[]; + }>; + }; + kibana: Array<{ + feature: { + ml: string[]; + siem: string[]; + actions: string[]; + builtInAlerts: string[]; + }; + spaces: string[]; + }>; +} + +export const postRoleAndUser = async ( + roleName: string, + role: RoleInterface, + user: UserInterface, + getService: FtrProviderContext['getService'] +): Promise => { + const securityService = getService('security'); + await securityService.role.create(roleName, { + kibana: role.kibana, + elasticsearch: role.elasticsearch, + }); + await securityService.user.create(roleName, { + password: 'changeme', + full_name: user.full_name, + roles: user.roles, + }); +}; diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alert_by_id.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alert_by_id.ts new file mode 100644 index 0000000000000..cf3cc88f2cfc0 --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alert_by_id.ts @@ -0,0 +1,210 @@ +/* + * 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 expect from '@kbn/expect'; + +import { + superUser, + globalRead, + obsOnly, + obsOnlyRead, + obsSec, + obsSecRead, + secOnly, + secOnlyRead, + secOnlySpace2, + secOnlyReadSpace2, + obsSecAllSpace2, + obsSecReadSpace2, + obsOnlySpace2, + obsOnlyReadSpace2, + obsOnlySpacesAll, + obsSecSpacesAll, + secOnlySpacesAll, + noKibanaPrivileges, +} from '../../../common/lib/authentication/users'; +import type { User } from '../../../common/lib/authentication/types'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { getSpaceUrlPrefix } from '../../../common/lib/authentication/spaces'; + +interface TestCase { + /** The space where the alert exists */ + space: string; + /** The ID of the alert */ + alertId: string; + /** The index of the alert */ + index: string; + /** Authorized users */ + authorizedUsers: User[]; + /** Unauthorized users */ + unauthorizedUsers: User[]; +} + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const TEST_URL = '/internal/rac/alerts'; + const ALERTS_INDEX_URL = `${TEST_URL}/index`; + const SPACE1 = 'space1'; + const SPACE2 = 'space2'; + const APM_ALERT_ID = 'NoxgpHkBqbdrfX07MqXV'; + const APM_ALERT_INDEX = '.alerts-observability-apm'; + const SECURITY_SOLUTION_ALERT_ID = '020202'; + const SECURITY_SOLUTION_ALERT_INDEX = '.alerts-security.alerts'; + + const getAPMIndexName = async (user: User) => { + const { + body: indexNames, + }: { body: { index_name: string[] | undefined } } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'true') + .expect(200); + const observabilityIndex = indexNames?.index_name?.find( + (indexName) => indexName === APM_ALERT_INDEX + ); + expect(observabilityIndex).to.eql(APM_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below + }; + + const getSecuritySolutionIndexName = async (user: User) => { + const { + body: indexNames, + }: { body: { index_name: string[] | undefined } } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'true') + .expect(200); + const securitySolution = indexNames?.index_name?.find( + (indexName) => indexName === SECURITY_SOLUTION_ALERT_INDEX + ); + expect(securitySolution).to.eql(SECURITY_SOLUTION_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below + }; + + describe('Alerts - GET - RBAC - spaces', () => { + before(async () => { + await getSecuritySolutionIndexName(superUser); + await getAPMIndexName(superUser); + + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + function addTests({ space, authorizedUsers, unauthorizedUsers, alertId, index }: TestCase) { + authorizedUsers.forEach(({ username, password }) => { + it(`${username} should be able to access alert ${alertId} in ${space}/${index}`, async () => { + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(space)}${TEST_URL}?id=${alertId}&index=${index}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(200); + }); + + it(`${username} should fail to access a non-existent alert in ${space}/${index}`, async () => { + const fakeAlertId = 'some-alert-id-that-doesnt-exist'; + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(space)}${TEST_URL}?id=${fakeAlertId}&index=${index}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(404); + }); + + it(`${username} should return a 404 when trying to accesses not-existent alerts as data index`, async () => { + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(space)}${TEST_URL}?id=${APM_ALERT_ID}&index=myfakeindex`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(404); + }); + }); + + unauthorizedUsers.forEach(({ username, password }) => { + it(`${username} should NOT be able to access alert ${alertId} in ${space}/${index}`, async () => { + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(space)}${TEST_URL}?id=${alertId}&index=${index}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(403); + }); + }); + } + + describe('Security Solution', () => { + const authorizedInAllSpaces = [superUser, globalRead, secOnlySpacesAll, obsSecSpacesAll]; + const authorizedOnlyInSpace1 = [secOnly, secOnlyRead, obsSec, obsSecRead]; + const authorizedOnlyInSpace2 = [ + secOnlySpace2, + secOnlyReadSpace2, + obsSecAllSpace2, + obsSecReadSpace2, + ]; + const unauthorized = [ + // these users are not authorized to access alerts for the Security Solution in any space + obsOnly, + obsOnlyRead, + obsOnlySpace2, + obsOnlyReadSpace2, + obsOnlySpacesAll, + noKibanaPrivileges, + ]; + + addTests({ + space: SPACE1, + alertId: SECURITY_SOLUTION_ALERT_ID, + index: SECURITY_SOLUTION_ALERT_INDEX, + authorizedUsers: [...authorizedInAllSpaces, ...authorizedOnlyInSpace1], + unauthorizedUsers: [...authorizedOnlyInSpace2, ...unauthorized], + }); + addTests({ + space: SPACE2, + alertId: SECURITY_SOLUTION_ALERT_ID, + index: SECURITY_SOLUTION_ALERT_INDEX, + authorizedUsers: [...authorizedInAllSpaces, ...authorizedOnlyInSpace2], + unauthorizedUsers: [...authorizedOnlyInSpace1, ...unauthorized], + }); + }); + + describe('APM', () => { + const authorizedInAllSpaces = [superUser, globalRead, obsOnlySpacesAll, obsSecSpacesAll]; + const authorizedOnlyInSpace1 = [obsOnly, obsOnlyRead, obsSec, obsSecRead]; + const authorizedOnlyInSpace2 = [ + obsOnlySpace2, + obsOnlyReadSpace2, + obsSecAllSpace2, + obsSecReadSpace2, + ]; + const unauthorized = [ + // these users are not authorized to access alerts for APM in any space + secOnly, + secOnlyRead, + secOnlySpace2, + secOnlyReadSpace2, + secOnlySpacesAll, + noKibanaPrivileges, + ]; + + addTests({ + space: SPACE1, + alertId: APM_ALERT_ID, + index: APM_ALERT_INDEX, + authorizedUsers: [...authorizedInAllSpaces, ...authorizedOnlyInSpace1], + unauthorizedUsers: [...authorizedOnlyInSpace2, ...unauthorized], + }); + addTests({ + space: SPACE2, + alertId: APM_ALERT_ID, + index: APM_ALERT_INDEX, + authorizedUsers: [...authorizedInAllSpaces, ...authorizedOnlyInSpace2], + unauthorizedUsers: [...authorizedOnlyInSpace1, ...unauthorized], + }); + }); + }); +}; diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts new file mode 100644 index 0000000000000..baea62c157218 --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts @@ -0,0 +1,29 @@ +/* + * 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 { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { createSpacesAndUsers, deleteSpacesAndUsers } from '../../../common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile, getService }: FtrProviderContext): void => { + describe('rules security and spaces enabled: basic', function () { + // Fastest ciGroup for the moment. + this.tags('ciGroup5'); + + before(async () => { + await createSpacesAndUsers(getService); + }); + + after(async () => { + await deleteSpacesAndUsers(getService); + }); + + // Basic + loadTestFile(require.resolve('./get_alert_by_id')); + loadTestFile(require.resolve('./update_alert')); + }); +}; diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/update_alert.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/update_alert.ts new file mode 100644 index 0000000000000..4fb087e813768 --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/update_alert.ts @@ -0,0 +1,251 @@ +/* + * 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 expect from '@kbn/expect'; + +import { + superUser, + globalRead, + obsOnly, + obsOnlyRead, + obsSec, + obsSecRead, + secOnly, + secOnlyRead, + secOnlySpace2, + secOnlyReadSpace2, + obsSecAllSpace2, + obsSecReadSpace2, + obsOnlySpace2, + obsOnlyReadSpace2, + obsOnlySpacesAll, + obsSecSpacesAll, + secOnlySpacesAll, + noKibanaPrivileges, +} from '../../../common/lib/authentication/users'; +import type { User } from '../../../common/lib/authentication/types'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { getSpaceUrlPrefix } from '../../../common/lib/authentication/spaces'; + +interface TestCase { + /** The space where the alert exists */ + space: string; + /** The ID of the alert */ + alertId: string; + /** The index of the alert */ + index: string; + /** Authorized users */ + authorizedUsers: User[]; + /** Unauthorized users */ + unauthorizedUsers: User[]; +} + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const TEST_URL = '/internal/rac/alerts'; + const ALERTS_INDEX_URL = `${TEST_URL}/index`; + const SPACE1 = 'space1'; + const SPACE2 = 'space2'; + const APM_ALERT_ID = 'NoxgpHkBqbdrfX07MqXV'; + const APM_ALERT_INDEX = '.alerts-observability-apm'; + const SECURITY_SOLUTION_ALERT_ID = '020202'; + const SECURITY_SOLUTION_ALERT_INDEX = '.alerts-security.alerts'; + const ALERT_VERSION = Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'); // required for optimistic concurrency control + + const getAPMIndexName = async (user: User) => { + const { + body: indexNames, + }: { body: { index_name: string[] | undefined } } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'true') + .expect(200); + const observabilityIndex = indexNames?.index_name?.find( + (indexName) => indexName === APM_ALERT_INDEX + ); + expect(observabilityIndex).to.eql(APM_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below + }; + + const getSecuritySolutionIndexName = async (user: User) => { + const { + body: indexNames, + }: { body: { index_name: string[] | undefined } } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'true') + .expect(200); + const securitySolution = indexNames?.index_name?.find( + (indexName) => indexName === SECURITY_SOLUTION_ALERT_INDEX + ); + expect(securitySolution).to.eql(SECURITY_SOLUTION_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below + }; + + describe('Alert - Update - RBAC - spaces', () => { + before(async () => { + await getSecuritySolutionIndexName(superUser); + await getAPMIndexName(superUser); + }); + + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + function addTests({ space, authorizedUsers, unauthorizedUsers, alertId, index }: TestCase) { + authorizedUsers.forEach(({ username, password }) => { + it(`${username} should be able to update alert ${alertId} in ${space}/${index}`, async () => { + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); // since this is a success case, reload the test data immediately beforehand + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(space)}${TEST_URL}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .send({ + ids: [alertId], + status: 'closed', + index, + _version: ALERT_VERSION, + }) + .expect(200); + }); + + it(`${username} should fail to update alert ${alertId} in ${space}/${index} with an incorrect version`, async () => { + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(space)}${TEST_URL}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .send({ + ids: [alertId], + status: 'closed', + index, + _version: Buffer.from(JSON.stringify([999, 999]), 'utf8').toString('base64'), + }) + .expect(409); + }); + + it(`${username} should fail to update a non-existent alert in ${space}/${index}`, async () => { + const fakeAlertId = 'some-alert-id-that-doesnt-exist'; + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(space)}${TEST_URL}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .send({ + ids: [fakeAlertId], + status: 'closed', + index, + _version: ALERT_VERSION, + }) + .expect(404); + }); + + it(`${username} should return a 404 when superuser accesses not-existent alerts as data index`, async () => { + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(space)}${TEST_URL}?id=${APM_ALERT_ID}&index=myfakeindex`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .send({ + ids: [APM_ALERT_ID], + status: 'closed', + index: 'this index does not exist', + _version: ALERT_VERSION, + }) + .expect(404); + }); + }); + + unauthorizedUsers.forEach(({ username, password }) => { + it(`${username} should NOT be able to update alert ${alertId} in ${space}/${index}`, async () => { + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(space)}${TEST_URL}`) + .auth(username, password) + .set('kbn-xsrf', 'true') + .send({ + ids: [alertId], + status: 'closed', + index, + _version: ALERT_VERSION, + }) + .expect(403); + }); + }); + } + + describe('Security Solution', () => { + const authorizedInAllSpaces = [superUser, secOnlySpacesAll, obsSecSpacesAll]; + const authorizedOnlyInSpace1 = [secOnly, obsSec]; + const authorizedOnlyInSpace2 = [secOnlySpace2, obsSecAllSpace2]; + const unauthorized = [ + // these users are not authorized to update alerts for the Security Solution in any space + globalRead, + secOnlyRead, + obsSecRead, + secOnlyReadSpace2, + obsSecReadSpace2, + obsOnly, + obsOnlyRead, + obsOnlySpace2, + obsOnlyReadSpace2, + obsOnlySpacesAll, + noKibanaPrivileges, + ]; + + addTests({ + space: SPACE1, + alertId: SECURITY_SOLUTION_ALERT_ID, + index: SECURITY_SOLUTION_ALERT_INDEX, + authorizedUsers: [...authorizedInAllSpaces, ...authorizedOnlyInSpace1], + unauthorizedUsers: [...authorizedOnlyInSpace2, ...unauthorized], + }); + addTests({ + space: SPACE2, + alertId: SECURITY_SOLUTION_ALERT_ID, + index: SECURITY_SOLUTION_ALERT_INDEX, + authorizedUsers: [...authorizedInAllSpaces, ...authorizedOnlyInSpace2], + unauthorizedUsers: [...authorizedOnlyInSpace1, ...unauthorized], + }); + }); + + describe('APM', () => { + const authorizedInAllSpaces = [superUser, obsOnlySpacesAll, obsSecSpacesAll]; + const authorizedOnlyInSpace1 = [obsOnly, obsSec]; + const authorizedOnlyInSpace2 = [obsOnlySpace2, obsSecAllSpace2]; + const unauthorized = [ + // these users are not authorized to update alerts for APM in any space + globalRead, + obsOnlyRead, + obsSecRead, + obsOnlyReadSpace2, + obsSecReadSpace2, + secOnly, + secOnlyRead, + secOnlySpace2, + secOnlyReadSpace2, + secOnlySpacesAll, + noKibanaPrivileges, + ]; + + addTests({ + space: SPACE1, + alertId: APM_ALERT_ID, + index: APM_ALERT_INDEX, + authorizedUsers: [...authorizedInAllSpaces, ...authorizedOnlyInSpace1], + unauthorizedUsers: [...authorizedOnlyInSpace2, ...unauthorized], + }); + addTests({ + space: SPACE2, + alertId: APM_ALERT_ID, + index: APM_ALERT_INDEX, + authorizedUsers: [...authorizedInAllSpaces, ...authorizedOnlyInSpace2], + unauthorizedUsers: [...authorizedOnlyInSpace1, ...unauthorized], + }); + }); + }); +}; diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/trial/get_alerts.ts b/x-pack/test/rule_registry/security_and_spaces/tests/trial/get_alerts.ts new file mode 100644 index 0000000000000..a38f6cf3263b1 --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/tests/trial/get_alerts.ts @@ -0,0 +1,127 @@ +/* + * 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 expect from '@kbn/expect'; + +import { + superUser, + obsMinReadSpacesAll, + obsMinRead, + obsMinReadAlertsRead, + obsMinReadAlertsReadSpacesAll, +} from '../../../common/lib/authentication/users'; +import type { User } from '../../../common/lib/authentication/types'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { getSpaceUrlPrefix } from '../../../common/lib/authentication/spaces'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const TEST_URL = '/internal/rac/alerts'; + const ALERTS_INDEX_URL = `${TEST_URL}/index`; + const SPACE1 = 'space1'; + const SPACE2 = 'space2'; + + const getAPMIndexName = async (user: User) => { + const { + body: indexNames, + }: { body: { index_name: string[] | undefined } } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'true') + .expect(200); + const observabilityIndex = indexNames?.index_name?.find( + (indexName) => indexName === '.alerts-observability-apm' + ); + expect(observabilityIndex).to.eql('.alerts-observability-apm'); + return observabilityIndex; + }; + + describe('rbac with subfeatures', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + describe('Users:', () => { + // user with minimal_read and alerts_read privileges should be able to access apm alert + it(`${obsMinReadAlertsRead.username} should be able to access the APM alert in ${SPACE1}`, async () => { + const apmIndex = await getAPMIndexName(superUser); + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&index=${apmIndex}`) + .auth(obsMinReadAlertsRead.username, obsMinReadAlertsRead.password) + .set('kbn-xsrf', 'true') + .expect(200); + }); + it(`${obsMinReadAlertsReadSpacesAll.username} should be able to access the APM alert in ${SPACE1}`, async () => { + const apmIndex = await getAPMIndexName(superUser); + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&index=${apmIndex}`) + .auth(obsMinReadAlertsReadSpacesAll.username, obsMinReadAlertsReadSpacesAll.password) + .set('kbn-xsrf', 'true') + .expect(200); + }); + + it(`${obsMinRead.username} should NOT be able to access the APM alert in ${SPACE1}`, async () => { + const apmIndex = await getAPMIndexName(superUser); + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&index=${apmIndex}`) + .auth(obsMinRead.username, obsMinRead.password) + .set('kbn-xsrf', 'true') + .expect(403); + }); + + it(`${obsMinReadSpacesAll.username} should NOT be able to access the APM alert in ${SPACE1}`, async () => { + const apmIndex = await getAPMIndexName(superUser); + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&index=${apmIndex}`) + .auth(obsMinReadSpacesAll.username, obsMinReadSpacesAll.password) + .set('kbn-xsrf', 'true') + .expect(403); + }); + }); + + describe('Space:', () => { + it(`${obsMinReadAlertsRead.username} should NOT be able to access the APM alert in ${SPACE2}`, async () => { + const apmIndex = await getAPMIndexName(superUser); + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE2)}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&index=${apmIndex}`) + .auth(obsMinReadAlertsRead.username, obsMinReadAlertsRead.password) + .set('kbn-xsrf', 'true') + .expect(403); + }); + + it(`${obsMinReadAlertsReadSpacesAll.username} should be able to access the APM alert in ${SPACE2}`, async () => { + const apmIndex = await getAPMIndexName(superUser); + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE2)}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&index=${apmIndex}`) + .auth(obsMinReadAlertsReadSpacesAll.username, obsMinReadAlertsReadSpacesAll.password) + .set('kbn-xsrf', 'true') + .expect(200); + }); + + describe('extra params', () => { + it('should NOT allow to pass a filter query parameter', async () => { + await supertest + .get(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}?sortOrder=asc&namespaces[0]=*`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + }); + + it('should NOT allow to pass a non supported query parameter', async () => { + await supertest + .get(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}?notExists=something`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + }); + }); + }); + }); +}; diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/trial/index.ts b/x-pack/test/rule_registry/security_and_spaces/tests/trial/index.ts new file mode 100644 index 0000000000000..5e89f99200f2d --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/tests/trial/index.ts @@ -0,0 +1,104 @@ +/* + * 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 { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { + createSpaces, + createUsersAndRoles, + deleteSpaces, + deleteUsersAndRoles, +} from '../../../common/lib/authentication'; + +import { + observabilityMinReadAlertsRead, + observabilityMinReadAlertsReadSpacesAll, + observabilityMinimalRead, + observabilityMinimalReadSpacesAll, + observabilityMinReadAlertsAll, + observabilityMinReadAlertsAllSpacesAll, + observabilityMinimalAll, + observabilityMinimalAllSpacesAll, +} from '../../../common/lib/authentication/roles'; +import { + obsMinReadAlertsRead, + obsMinReadAlertsReadSpacesAll, + obsMinRead, + obsMinReadSpacesAll, + superUser, + obsMinReadAlertsAll, + obsMinReadAlertsAllSpacesAll, + obsMinAll, + obsMinAllSpacesAll, +} from '../../../common/lib/authentication/users'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile, getService }: FtrProviderContext): void => { + describe('rules security and spaces enabled: trial', function () { + // Fastest ciGroup for the moment. + this.tags('ciGroup5'); + + before(async () => { + await createSpaces(getService); + await createUsersAndRoles( + getService, + [ + obsMinReadAlertsRead, + obsMinReadAlertsReadSpacesAll, + obsMinRead, + obsMinReadSpacesAll, + superUser, + obsMinReadAlertsAll, + obsMinReadAlertsAllSpacesAll, + obsMinAll, + obsMinAllSpacesAll, + ], + [ + observabilityMinReadAlertsRead, + observabilityMinReadAlertsReadSpacesAll, + observabilityMinimalRead, + observabilityMinimalReadSpacesAll, + observabilityMinReadAlertsAll, + observabilityMinReadAlertsAllSpacesAll, + observabilityMinimalAll, + observabilityMinimalAllSpacesAll, + ] + ); + }); + + after(async () => { + await deleteSpaces(getService); + await deleteUsersAndRoles( + getService, + [ + obsMinReadAlertsRead, + obsMinReadAlertsReadSpacesAll, + obsMinRead, + obsMinReadSpacesAll, + superUser, + obsMinReadAlertsAll, + obsMinReadAlertsAllSpacesAll, + obsMinAll, + obsMinAllSpacesAll, + ], + [ + observabilityMinReadAlertsRead, + observabilityMinReadAlertsReadSpacesAll, + observabilityMinimalRead, + observabilityMinimalReadSpacesAll, + observabilityMinReadAlertsAll, + observabilityMinReadAlertsAllSpacesAll, + observabilityMinimalAll, + observabilityMinimalAllSpacesAll, + ] + ); + }); + + // Trial + loadTestFile(require.resolve('./get_alerts')); + loadTestFile(require.resolve('./update_alert')); + }); +}; diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/trial/update_alert.ts b/x-pack/test/rule_registry/security_and_spaces/tests/trial/update_alert.ts new file mode 100644 index 0000000000000..c126c434bd4cf --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/tests/trial/update_alert.ts @@ -0,0 +1,189 @@ +/* + * 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 expect from '@kbn/expect'; + +import { + superUser, + obsMinReadAlertsAll, + obsMinReadAlertsAllSpacesAll, + obsMinAll, + obsMinAllSpacesAll, +} from '../../../common/lib/authentication/users'; +import type { User } from '../../../common/lib/authentication/types'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { getSpaceUrlPrefix } from '../../../common/lib/authentication/spaces'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const TEST_URL = '/internal/rac/alerts'; + const ALERTS_INDEX_URL = `${TEST_URL}/index`; + const SPACE1 = 'space1'; + + const getAPMIndexName = async (user: User) => { + const { + body: indexNames, + }: { body: { index_name: string[] | undefined } } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'true') + .expect(200); + const observabilityIndex = indexNames?.index_name?.find( + (indexName) => indexName === '.alerts-observability-apm' + ); + expect(observabilityIndex).to.eql('.alerts-observability-apm'); + return observabilityIndex; + }; + + describe('rbac', () => { + describe('Users update:', () => { + beforeEach(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + afterEach(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + it(`${superUser.username} should be able to update the APM alert in ${SPACE1}`, async () => { + const apmIndex = await getAPMIndexName(superUser); + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) + .auth(superUser.username, superUser.password) + .set('kbn-xsrf', 'true') + .send({ + ids: ['NoxgpHkBqbdrfX07MqXV'], + status: 'closed', + index: apmIndex, + _version: Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'), + }) + .expect(200); + }); + + it(`${superUser.username} should receive a 409 if trying to update an old alert document version`, async () => { + const apmIndex = await getAPMIndexName(superUser); + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) + .auth(superUser.username, superUser.password) + .set('kbn-xsrf', 'true') + .send({ + ids: ['NoxgpHkBqbdrfX07MqXV'], + status: 'closed', + index: apmIndex, + _version: Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'), + }) + .expect(200); + + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) + .auth(superUser.username, superUser.password) + .set('kbn-xsrf', 'true') + .send({ + ids: ['NoxgpHkBqbdrfX07MqXV'], + status: 'closed', + index: apmIndex, + _version: Buffer.from(JSON.stringify([999, 999]), 'utf8').toString('base64'), + }) + .expect(409); + }); + + it(`${obsMinReadAlertsAllSpacesAll.username} should be able to update the APM alert in ${SPACE1}`, async () => { + const apmIndex = await getAPMIndexName(superUser); + const res = await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) + .auth(obsMinReadAlertsAllSpacesAll.username, obsMinReadAlertsAllSpacesAll.password) + .set('kbn-xsrf', 'true') + .send({ + ids: ['NoxgpHkBqbdrfX07MqXV'], + status: 'closed', + index: apmIndex, + _version: Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'), + }) + .expect(200); + expect(res.body).to.eql({ + success: true, + _index: '.alerts-observability-apm', + _id: 'NoxgpHkBqbdrfX07MqXV', + result: 'updated', + _shards: { total: 2, successful: 1, failed: 0 }, + _version: 'WzEsMV0=', + _seq_no: 1, + _primary_term: 1, + }); + }); + it(`${obsMinReadAlertsAllSpacesAll.username} should receive a 409 if trying to update an old alert document version`, async () => { + const apmIndex = await getAPMIndexName(superUser); + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) + .auth(obsMinReadAlertsAllSpacesAll.username, obsMinReadAlertsAllSpacesAll.password) + .set('kbn-xsrf', 'true') + .send({ + ids: ['NoxgpHkBqbdrfX07MqXV'], + status: 'closed', + index: apmIndex, + _version: Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'), + }) + .expect(200); + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) + .auth(obsMinReadAlertsAllSpacesAll.username, obsMinReadAlertsAllSpacesAll.password) + .set('kbn-xsrf', 'true') + .send({ + ids: ['NoxgpHkBqbdrfX07MqXV'], + status: 'closed', + index: apmIndex, + _version: Buffer.from(JSON.stringify([999, 999]), 'utf8').toString('base64'), + }) + .expect(409); + }); + + it(`${obsMinReadAlertsAll.username} should be able to update the APM alert in ${SPACE1}`, async () => { + const apmIndex = await getAPMIndexName(superUser); + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) + .auth(obsMinReadAlertsAll.username, obsMinReadAlertsAll.password) + .set('kbn-xsrf', 'true') + .send({ + ids: ['NoxgpHkBqbdrfX07MqXV'], + status: 'closed', + index: apmIndex, + _version: Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'), + }) + .expect(200); + }); + it(`${obsMinAll.username} should NOT be able to update the APM alert in ${SPACE1}`, async () => { + const apmIndex = await getAPMIndexName(superUser); + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) + .auth(obsMinAll.username, obsMinAll.password) + .set('kbn-xsrf', 'true') + .send({ + ids: ['NoxgpHkBqbdrfX07MqXV'], + status: 'closed', + index: apmIndex, + _version: Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'), + }) + .expect(403); + }); + + it(`${obsMinAllSpacesAll.username} should NOT be able to update the APM alert in ${SPACE1}`, async () => { + const apmIndex = await getAPMIndexName(superUser); + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`) + .auth(obsMinAllSpacesAll.username, obsMinAllSpacesAll.password) + .set('kbn-xsrf', 'true') + .send({ + ids: ['NoxgpHkBqbdrfX07MqXV'], + status: 'closed', + index: apmIndex, + _version: Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'), + }) + .expect(403); + }); + }); + }); +}; diff --git a/x-pack/test/rule_registry/spaces_only/config_trial.ts b/x-pack/test/rule_registry/spaces_only/config_trial.ts new file mode 100644 index 0000000000000..e788a16d0272f --- /dev/null +++ b/x-pack/test/rule_registry/spaces_only/config_trial.ts @@ -0,0 +1,16 @@ +/* + * 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 { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('spaces_only', { + license: 'trial', + disabledPlugins: ['security'], + ssl: false, + testFiles: [require.resolve('./tests/trial')], +}); diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/get_alert_by_id.ts b/x-pack/test/rule_registry/spaces_only/tests/trial/get_alert_by_id.ts new file mode 100644 index 0000000000000..df188718bff19 --- /dev/null +++ b/x-pack/test/rule_registry/spaces_only/tests/trial/get_alert_by_id.ts @@ -0,0 +1,87 @@ +/* + * 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 expect from '@kbn/expect'; + +import { superUser } from '../../../common/lib/authentication/users'; +import type { User } from '../../../common/lib/authentication/types'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { getSpaceUrlPrefix } from '../../../common/lib/authentication/spaces'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const TEST_URL = '/internal/rac/alerts'; + const ALERTS_INDEX_URL = `${TEST_URL}/index`; + const SPACE1 = 'space1'; + const SPACE2 = 'space2'; + const APM_ALERT_ID = 'NoxgpHkBqbdrfX07MqXV'; + const APM_ALERT_INDEX = '.alerts-observability-apm'; + const SECURITY_SOLUTION_ALERT_INDEX = '.alerts-security.alerts'; + + const getAPMIndexName = async (user: User) => { + const { + body: indexNames, + }: { body: { index_name: string[] | undefined } } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) + .set('kbn-xsrf', 'true') + .expect(200); + const observabilityIndex = indexNames?.index_name?.find( + (indexName) => indexName === APM_ALERT_INDEX + ); + expect(observabilityIndex).to.eql(APM_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below + }; + + const getSecuritySolutionIndexName = async (user: User) => { + const { + body: indexNames, + }: { body: { index_name: string[] | undefined } } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) + .set('kbn-xsrf', 'true') + .expect(200); + const securitySolution = indexNames?.index_name?.find( + (indexName) => indexName === SECURITY_SOLUTION_ALERT_INDEX + ); + expect(securitySolution).to.eql(SECURITY_SOLUTION_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below + }; + + describe('Alerts - GET - RBAC - spaces', () => { + before(async () => { + await getSecuritySolutionIndexName(superUser); + await getAPMIndexName(superUser); + + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + it('should return a 404 when superuser accesses not-existent alert', async () => { + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix()}${TEST_URL}?id=myfakeid&index=${APM_ALERT_INDEX}`) + .set('kbn-xsrf', 'true') + .expect(404); + }); + + it('should return a 404 when superuser accesses not-existent alerts as data index', async () => { + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix()}${TEST_URL}?id=${APM_ALERT_ID}&index=myfakeindex`) + .set('kbn-xsrf', 'true') + .expect(404); + }); + + it(`${superUser.username} should be able to access alert ${APM_ALERT_ID} in ${SPACE2}/${APM_ALERT_INDEX}`, async () => { + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE2)}${TEST_URL}?id=${APM_ALERT_ID}&index=${APM_ALERT_INDEX}`) + .set('kbn-xsrf', 'true') + .expect(200); + }); + }); +}; diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/index.ts b/x-pack/test/rule_registry/spaces_only/tests/trial/index.ts new file mode 100644 index 0000000000000..6deba4c68d0e2 --- /dev/null +++ b/x-pack/test/rule_registry/spaces_only/tests/trial/index.ts @@ -0,0 +1,29 @@ +/* + * 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 { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { createSpaces, deleteSpaces } from '../../../common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile, getService }: FtrProviderContext): void => { + describe('rule registry spaces only: trial', function () { + // Fastest ciGroup for the moment. + this.tags('ciGroup5'); + + before(async () => { + await createSpaces(getService); + }); + + after(async () => { + await deleteSpaces(getService); + }); + + // Basic + loadTestFile(require.resolve('./get_alert_by_id')); + loadTestFile(require.resolve('./update_alert')); + }); +}; diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/update_alert.ts b/x-pack/test/rule_registry/spaces_only/tests/trial/update_alert.ts new file mode 100644 index 0000000000000..f5179b253b701 --- /dev/null +++ b/x-pack/test/rule_registry/spaces_only/tests/trial/update_alert.ts @@ -0,0 +1,108 @@ +/* + * 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 expect from '@kbn/expect'; + +import { superUser } from '../../../common/lib/authentication/users'; +import type { User } from '../../../common/lib/authentication/types'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { getSpaceUrlPrefix } from '../../../common/lib/authentication/spaces'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const TEST_URL = '/internal/rac/alerts'; + const ALERTS_INDEX_URL = `${TEST_URL}/index`; + const SPACE1 = 'space1'; + const SPACE2 = 'space2'; + const APM_ALERT_ID = 'NoxgpHkBqbdrfX07MqXV'; + const APM_ALERT_INDEX = '.alerts-observability-apm'; + const SECURITY_SOLUTION_ALERT_INDEX = '.alerts-security.alerts'; + const ALERT_VERSION = Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'); // required for optimistic concurrency control + + const getAPMIndexName = async (user: User) => { + const { + body: indexNames, + }: { body: { index_name: string[] | undefined } } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) + .set('kbn-xsrf', 'true') + .expect(200); + const observabilityIndex = indexNames?.index_name?.find( + (indexName) => indexName === APM_ALERT_INDEX + ); + expect(observabilityIndex).to.eql(APM_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below + }; + + const getSecuritySolutionIndexName = async (user: User) => { + const { + body: indexNames, + }: { body: { index_name: string[] | undefined } } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) + .set('kbn-xsrf', 'true') + .expect(200); + const securitySolution = indexNames?.index_name?.find( + (indexName) => indexName === SECURITY_SOLUTION_ALERT_INDEX + ); + expect(securitySolution).to.eql(SECURITY_SOLUTION_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below + }; + + describe('Alert - Update - RBAC - spaces', () => { + before(async () => { + await getSecuritySolutionIndexName(superUser); + await getAPMIndexName(superUser); + }); + + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + it('should return a 404 when superuser accesses not-existent alert', async () => { + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix()}${TEST_URL}`) + .set('kbn-xsrf', 'true') + .send({ + ids: ['this id does not exist'], + status: 'closed', + index: APM_ALERT_INDEX, + _version: Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'), + }) + .expect(404); + }); + + it('should return a 404 when superuser accesses not-existent alerts as data index', async () => { + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix()}${TEST_URL}`) + .set('kbn-xsrf', 'true') + .send({ + ids: [APM_ALERT_ID], + status: 'closed', + index: 'this index does not exist', + _version: Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'), + }) + .expect(404); + }); + + it(`${superUser.username} should be able to update alert ${APM_ALERT_ID} in ${SPACE2}/${APM_ALERT_INDEX}`, async () => { + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); // since this is a success case, reload the test data immediately beforehand + await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE2)}${TEST_URL}`) + .set('kbn-xsrf', 'true') + .send({ + ids: [APM_ALERT_ID], + status: 'closed', + index: APM_ALERT_INDEX, + _version: ALERT_VERSION, + }) + .expect(200); + }); + }); +}; diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts index 1a52bd18f80af..e1763b6ad4404 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts @@ -29,7 +29,6 @@ export default function endpointAPIIntegrationTests(providerContext: FtrProvider }); loadTestFile(require.resolve('./resolver/index')); loadTestFile(require.resolve('./metadata')); - loadTestFile(require.resolve('./metadata_v1')); loadTestFile(require.resolve('./policy')); loadTestFile(require.resolve('./package')); }); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts index b5d98c115d194..1f57cd1b6db34 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts @@ -12,7 +12,6 @@ import { deleteAllDocsFromMetadataIndex, deleteMetadataStream, } from './data_stream_helper'; -import { MetadataQueryStrategyVersions } from '../../../plugins/security_solution/common/endpoint/types'; import { HOST_METADATA_LIST_ROUTE } from '../../../plugins/security_solution/common/endpoint/constants'; /** @@ -88,7 +87,6 @@ export default function ({ getService }: FtrProviderContext) { expect(body.hosts.length).to.eql(1); expect(body.request_page_size).to.eql(1); expect(body.request_page_index).to.eql(1); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_2); }); /* test that when paging properties produces no result, the total should reflect the actual number of metadata @@ -113,7 +111,6 @@ export default function ({ getService }: FtrProviderContext) { expect(body.hosts.length).to.eql(0); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(30); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_2); }); it('metadata api should return 400 when pagingProperties is below boundaries.', async () => { @@ -148,7 +145,6 @@ export default function ({ getService }: FtrProviderContext) { expect(body.hosts.length).to.eql(2); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_2); }); it('metadata api should return page based on filters and paging passed.', async () => { @@ -186,7 +182,6 @@ export default function ({ getService }: FtrProviderContext) { expect(body.hosts.length).to.eql(2); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_2); }); it('metadata api should return page based on host.os.Ext.variant filter.', async () => { @@ -208,7 +203,6 @@ export default function ({ getService }: FtrProviderContext) { expect(body.hosts.length).to.eql(2); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_2); }); it('metadata api should return the latest event for all the events for an endpoint', async () => { @@ -231,7 +225,6 @@ export default function ({ getService }: FtrProviderContext) { expect(body.hosts.length).to.eql(1); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_2); }); it('metadata api should return the latest event for all the events where policy status is not success', async () => { @@ -275,7 +268,6 @@ export default function ({ getService }: FtrProviderContext) { expect(body.hosts.length).to.eql(1); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_2); }); it('metadata api should return all hosts when filter is empty string', async () => { @@ -292,7 +284,6 @@ export default function ({ getService }: FtrProviderContext) { expect(body.hosts.length).to.eql(numberOfHostsInFixture); expect(body.request_page_size).to.eql(10); expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_2); }); }); }); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/metadata_v1.ts b/x-pack/test/security_solution_endpoint_api_int/apis/metadata_v1.ts deleted file mode 100644 index d8cf1a11fac0a..0000000000000 --- a/x-pack/test/security_solution_endpoint_api_int/apis/metadata_v1.ts +++ /dev/null @@ -1,290 +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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../ftr_provider_context'; -import { deleteMetadataStream } from './data_stream_helper'; -import { METADATA_REQUEST_V1_ROUTE } from '../../../plugins/security_solution/server/endpoint/routes/metadata'; -import { MetadataQueryStrategyVersions } from '../../../plugins/security_solution/common/endpoint/types'; - -/** - * The number of host documents in the es archive. - */ -const numberOfHostsInFixture = 3; - -export default function ({ getService }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); - const supertest = getService('supertest'); - describe('test metadata api v1', () => { - describe(`POST ${METADATA_REQUEST_V1_ROUTE} when index is empty`, () => { - it('metadata api should return empty result when index is empty', async () => { - // the endpoint uses data streams and es archiver does not support deleting them at the moment so we need - // to do it manually - await deleteMetadataStream(getService); - const { body } = await supertest - .post(`${METADATA_REQUEST_V1_ROUTE}`) - .set('kbn-xsrf', 'xxx') - .send() - .expect(200); - expect(body.total).to.eql(0); - expect(body.hosts.length).to.eql(0); - expect(body.request_page_size).to.eql(10); - expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1); - }); - }); - - describe(`POST ${METADATA_REQUEST_V1_ROUTE} when index is not empty`, () => { - before( - async () => - await esArchiver.load( - 'x-pack/test/functional/es_archives/endpoint/metadata/api_feature', - { useCreate: true } - ) - ); - // the endpoint uses data streams and es archiver does not support deleting them at the moment so we need - // to do it manually - after(async () => await deleteMetadataStream(getService)); - it('metadata api should return one entry for each host with default paging', async () => { - const { body } = await supertest - .post(`${METADATA_REQUEST_V1_ROUTE}`) - .set('kbn-xsrf', 'xxx') - .send() - .expect(200); - expect(body.total).to.eql(numberOfHostsInFixture); - expect(body.hosts.length).to.eql(numberOfHostsInFixture); - expect(body.request_page_size).to.eql(10); - expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1); - }); - - it('metadata api should return page based on paging properties passed.', async () => { - const { body } = await supertest - .post(`${METADATA_REQUEST_V1_ROUTE}`) - .set('kbn-xsrf', 'xxx') - .send({ - paging_properties: [ - { - page_size: 1, - }, - { - page_index: 1, - }, - ], - }) - .expect(200); - expect(body.total).to.eql(numberOfHostsInFixture); - expect(body.hosts.length).to.eql(1); - expect(body.request_page_size).to.eql(1); - expect(body.request_page_index).to.eql(1); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1); - }); - - /* test that when paging properties produces no result, the total should reflect the actual number of metadata - in the index. - */ - it('metadata api should return accurate total metadata if page index produces no result', async () => { - const { body } = await supertest - .post(`${METADATA_REQUEST_V1_ROUTE}`) - .set('kbn-xsrf', 'xxx') - .send({ - paging_properties: [ - { - page_size: 10, - }, - { - page_index: 3, - }, - ], - }) - .expect(200); - expect(body.total).to.eql(numberOfHostsInFixture); - expect(body.hosts.length).to.eql(0); - expect(body.request_page_size).to.eql(10); - expect(body.request_page_index).to.eql(30); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1); - }); - - it('metadata api should return 400 when pagingProperties is below boundaries.', async () => { - const { body } = await supertest - .post(`${METADATA_REQUEST_V1_ROUTE}`) - .set('kbn-xsrf', 'xxx') - .send({ - paging_properties: [ - { - page_size: 0, - }, - { - page_index: 1, - }, - ], - }) - .expect(400); - expect(body.message).to.contain('Value must be equal to or greater than [1]'); - }); - - it('metadata api should return page based on filters passed.', async () => { - const { body } = await supertest - .post(`${METADATA_REQUEST_V1_ROUTE}`) - .set('kbn-xsrf', 'xxx') - .send({ - filters: { - kql: 'not host.ip:10.46.229.234', - }, - }) - .expect(200); - expect(body.total).to.eql(2); - expect(body.hosts.length).to.eql(2); - expect(body.request_page_size).to.eql(10); - expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1); - }); - - it('metadata api should return page based on filters and paging passed.', async () => { - const notIncludedIp = '10.46.229.234'; - const { body } = await supertest - .post(`${METADATA_REQUEST_V1_ROUTE}`) - .set('kbn-xsrf', 'xxx') - .send({ - paging_properties: [ - { - page_size: 10, - }, - { - page_index: 0, - }, - ], - filters: { - kql: `not host.ip:${notIncludedIp}`, - }, - }) - .expect(200); - expect(body.total).to.eql(2); - const resultIps: string[] = [].concat( - ...body.hosts.map((hostInfo: Record) => hostInfo.metadata.host.ip) - ); - expect(resultIps).to.eql([ - '10.192.213.130', - '10.70.28.129', - '10.101.149.26', - '2606:a000:ffc0:39:11ef:37b9:3371:578c', - ]); - expect(resultIps).not.include.eql(notIncludedIp); - expect(body.hosts.length).to.eql(2); - expect(body.request_page_size).to.eql(10); - expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1); - }); - - it('metadata api should return page based on host.os.Ext.variant filter.', async () => { - const variantValue = 'Windows Pro'; - const { body } = await supertest - .post(`${METADATA_REQUEST_V1_ROUTE}`) - .set('kbn-xsrf', 'xxx') - .send({ - filters: { - kql: `host.os.Ext.variant:${variantValue}`, - }, - }) - .expect(200); - expect(body.total).to.eql(2); - const resultOsVariantValue: Set = new Set( - body.hosts.map((hostInfo: Record) => hostInfo.metadata.host.os.Ext.variant) - ); - expect(Array.from(resultOsVariantValue)).to.eql([variantValue]); - expect(body.hosts.length).to.eql(2); - expect(body.request_page_size).to.eql(10); - expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1); - }); - - it('metadata api should return the latest event for all the events for an endpoint', async () => { - const targetEndpointIp = '10.46.229.234'; - const { body } = await supertest - .post(`${METADATA_REQUEST_V1_ROUTE}`) - .set('kbn-xsrf', 'xxx') - .send({ - filters: { - kql: `host.ip:${targetEndpointIp}`, - }, - }) - .expect(200); - expect(body.total).to.eql(1); - const resultIp: string = body.hosts[0].metadata.host.ip.filter( - (ip: string) => ip === targetEndpointIp - ); - expect(resultIp).to.eql([targetEndpointIp]); - expect(body.hosts[0].metadata.event.created).to.eql(1618841405309); - expect(body.hosts.length).to.eql(1); - expect(body.request_page_size).to.eql(10); - expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1); - }); - - it('metadata api should return the latest event for all the events where policy status is not success', async () => { - const { body } = await supertest - .post(`${METADATA_REQUEST_V1_ROUTE}`) - .set('kbn-xsrf', 'xxx') - .send({ - filters: { - kql: `not Endpoint.policy.applied.status:success`, - }, - }) - .expect(200); - const statuses: Set = new Set( - body.hosts.map( - (hostInfo: Record) => hostInfo.metadata.Endpoint.policy.applied.status - ) - ); - expect(statuses.size).to.eql(1); - expect(Array.from(statuses)).to.eql(['failure']); - }); - - it('metadata api should return the endpoint based on the elastic agent id, and status should be unhealthy', async () => { - const targetEndpointId = 'fc0ff548-feba-41b6-8367-65e8790d0eaf'; - const targetElasticAgentId = '023fa40c-411d-4188-a941-4147bfadd095'; - const { body } = await supertest - .post(`${METADATA_REQUEST_V1_ROUTE}`) - .set('kbn-xsrf', 'xxx') - .send({ - filters: { - kql: `elastic.agent.id:${targetElasticAgentId}`, - }, - }) - .expect(200); - expect(body.total).to.eql(1); - const resultHostId: string = body.hosts[0].metadata.host.id; - const resultElasticAgentId: string = body.hosts[0].metadata.elastic.agent.id; - expect(resultHostId).to.eql(targetEndpointId); - expect(resultElasticAgentId).to.eql(targetElasticAgentId); - expect(body.hosts[0].metadata.event.created).to.eql(1618841405309); - expect(body.hosts[0].host_status).to.eql('unhealthy'); - expect(body.hosts.length).to.eql(1); - expect(body.request_page_size).to.eql(10); - expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1); - }); - - it('metadata api should return all hosts when filter is empty string', async () => { - const { body } = await supertest - .post(`${METADATA_REQUEST_V1_ROUTE}`) - .set('kbn-xsrf', 'xxx') - .send({ - filters: { - kql: '', - }, - }) - .expect(200); - expect(body.total).to.eql(numberOfHostsInFixture); - expect(body.hosts.length).to.eql(numberOfHostsInFixture); - expect(body.request_page_size).to.eql(10); - expect(body.request_page_index).to.eql(0); - expect(body.query_strategy_version).to.eql(MetadataQueryStrategyVersions.VERSION_1); - }); - }); - }); -} diff --git a/x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js b/x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js index a22e4438c7dbd..588ff9a6e9f92 100644 --- a/x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js +++ b/x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js @@ -5,16 +5,7 @@ * 2.0. */ -import fs from 'fs'; -import { resolve } from 'path'; import expect from '@kbn/expect'; -import { Client as EsClient } from '@elastic/elasticsearch'; -import { KbnClient } from '@kbn/test'; -import { EsArchiver } from '@kbn/es-archiver'; -import { CA_CERT_PATH, REPO_ROOT } from '@kbn/dev-utils'; - -const INTEGRATION_TEST_ROOT = process.env.WORKSPACE || resolve(REPO_ROOT, '../integration-test'); -const ARCHIVE = resolve(INTEGRATION_TEST_ROOT, 'test/es_archives/metricbeat'); export default ({ getService, getPageObjects }) => { describe('Cross cluster search test in discover', async () => { @@ -212,151 +203,5 @@ export default ({ getService, getPageObjects }) => { expect(hitCount).to.be.lessThan(originalHitCount); }); }); - - describe('Detection engine', async function () { - const supertest = getService('supertest'); - const esSupertest = getService('esSupertest'); - const config = getService('config'); - - const esClient = new EsClient({ - ssl: { - ca: fs.readFileSync(CA_CERT_PATH, 'utf-8'), - }, - nodes: [process.env.TEST_ES_URLDATA], - requestTimeout: config.get('timeouts.esRequestTimeout'), - }); - - const kbnClient = new KbnClient({ - log, - url: process.env.TEST_KIBANA_URLDATA, - certificateAuthorities: config.get('servers.kibana.certificateAuthorities'), - uiSettingDefaults: kibanaServer.uiSettings, - }); - - const esArchiver = new EsArchiver({ - log, - client: esClient, - kbnClient, - }); - - let signalsId; - let dataId; - let ruleId; - - before('Prepare .siem-signal-*', async function () { - log.info('Create index'); - // visit app/security so to create .siem-signals-* as side effect - await PageObjects.common.navigateToApp('security', { insertTimestamp: false }); - - log.info('Create index pattern'); - signalsId = await supertest - .post('/api/index_patterns/index_pattern') - .set('kbn-xsrf', 'true') - .send({ - index_pattern: { - title: '.siem-signals-*', - }, - override: true, - }) - .expect(200) - .then((res) => JSON.parse(res.text).index_pattern.id); - log.debug('id: ' + signalsId); - }); - - before('Prepare data:metricbeat-*', async function () { - log.info('Create index'); - await esArchiver.load(ARCHIVE); - - log.info('Create index pattern'); - dataId = await supertest - .post('/api/index_patterns/index_pattern') - .set('kbn-xsrf', 'true') - .send({ - index_pattern: { - title: 'data:metricbeat-*', - }, - override: true, - }) - .expect(200) - .then((res) => JSON.parse(res.text).index_pattern.id); - log.debug('id: ' + dataId); - }); - - before('Add detection rule', async function () { - ruleId = await supertest - .post('/api/detection_engine/rules') - .set('kbn-xsrf', 'true') - .send({ - description: 'This is the description of the rule', - risk_score: 17, - severity: 'low', - interval: '10s', - name: 'CCS_Detection_test', - type: 'query', - from: 'now-1y', - index: ['data:metricbeat-*'], - query: '*:*', - language: 'kuery', - enabled: true, - }) - .expect(200) - .then((res) => JSON.parse(res.text).id); - log.debug('id: ' + ruleId); - }); - - after('Clean up detection rule', async function () { - if (ruleId !== undefined) { - log.debug('id: ' + ruleId); - await supertest - .delete('/api/detection_engine/rules?id=' + ruleId) - .set('kbn-xsrf', 'true') - .expect(200); - } - }); - - after('Clean up data:metricbeat-*', async function () { - if (dataId !== undefined) { - log.info('Delete index pattern'); - log.debug('id: ' + dataId); - await supertest - .delete('/api/index_patterns/index_pattern/' + dataId) - .set('kbn-xsrf', 'true') - .expect(200); - } - - log.info('Delete index'); - await esArchiver.unload(ARCHIVE); - }); - - after('Clean up .siem-signal-*', async function () { - if (signalsId !== undefined) { - log.info('Delete index pattern: .siem-signals-*'); - log.debug('id: ' + signalsId); - await supertest - .delete('/api/index_patterns/index_pattern/' + signalsId) - .set('kbn-xsrf', 'true') - .expect(200); - } - - log.info('Delete index alias: .siem-signals-default'); - await esSupertest - .delete('/.siem-signals-default-000001/_alias/.siem-signals-default') - .expect(200); - - log.info('Delete index: .siem-signals-default-000001'); - await esSupertest.delete('/.siem-signals-default-000001').expect(200); - }); - - it('Should generate alerts based on remote events', async function () { - log.info('Check if any alert got to .siem-signals-*'); - await PageObjects.common.navigateToApp('discover', { insertTimestamp: false }); - await PageObjects.discover.selectIndexPattern('.siem-signals-*'); - await retry.tryForTime(30000, async () => { - const hitCount = await PageObjects.discover.getHitCount(); - log.debug('### hit count = ' + hitCount); - expect(hitCount).to.be('100'); - }); - }); - }); }); }; diff --git a/x-pack/test/stack_functional_integration/apps/monitoring/_monitoring_metricbeat.js b/x-pack/test/stack_functional_integration/apps/monitoring/_monitoring_metricbeat.js index d8341c56aa25c..9dcc18b3c3f20 100644 --- a/x-pack/test/stack_functional_integration/apps/monitoring/_monitoring_metricbeat.js +++ b/x-pack/test/stack_functional_integration/apps/monitoring/_monitoring_metricbeat.js @@ -12,6 +12,7 @@ export default ({ getService, getPageObjects }) => { const log = getService('log'); const testSubjects = getService('testSubjects'); const isSaml = !!process.env.VM.includes('saml') || !!process.env.VM.includes('oidc'); + const clusterOverview = getService('monitoringClusterOverview'); before(async () => { await browser.setWindowSize(1200, 800); @@ -25,6 +26,7 @@ export default ({ getService, getPageObjects }) => { } // navigateToApp without a username and password will default to the superuser await PageObjects.common.navigateToApp('monitoring', { insertTimestamp: false }); + await clusterOverview.closeAlertsModal(); }); it('should have Monitoring already enabled', async () => { diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 0424891064cd3..cd43e7108b06d 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -55,7 +55,6 @@ { "path": "../plugins/banners/tsconfig.json" }, { "path": "../plugins/cases/tsconfig.json" }, { "path": "../plugins/cloud/tsconfig.json" }, - { "path": "../plugins/console_extensions/tsconfig.json" }, { "path": "../plugins/dashboard_mode/tsconfig.json" }, { "path": "../plugins/enterprise_search/tsconfig.json" }, { "path": "../plugins/fleet/tsconfig.json" }, diff --git a/x-pack/test/upgrade/apps/dashboard/dashboard_smoke_tests.ts b/x-pack/test/upgrade/apps/dashboard/dashboard_smoke_tests.ts index 73819b5bac695..0bc3cd7c2610e 100644 --- a/x-pack/test/upgrade/apps/dashboard/dashboard_smoke_tests.ts +++ b/x-pack/test/upgrade/apps/dashboard/dashboard_smoke_tests.ts @@ -16,6 +16,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const renderable = getService('renderable'); const dashboardExpect = getService('dashboardExpect'); const PageObjects = getPageObjects(['common', 'header', 'home', 'dashboard', 'timePicker']); + const kibanaServer = getService('kibanaServer'); + const browser = getService('browser'); describe('dashboard smoke tests', function describeIndexTests() { const spaces = [ @@ -36,6 +38,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { basePath, }); await PageObjects.header.waitUntilLoadingHasFinished(); + await kibanaServer.uiSettings.update( + { + 'visualization:visualize:legacyChartsLibrary': true, + 'visualization:visualize:legacyPieChartsLibrary': true, + }, + { space } + ); + await browser.refresh(); }); dashboardTests.forEach(({ name, numPanels }) => { it('should launch sample ' + name + ' data set dashboard', async () => { @@ -56,9 +66,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await renderable.waitForRender(); log.debug('Checking pie charts rendered'); await pieChart.expectPieSliceCount(4); - // https://github.com/elastic/kibana/issues/92887 - // log.debug('Checking area, bar and heatmap charts rendered'); - // await dashboardExpect.seriesElementCount(15); + log.debug('Checking area, bar and heatmap charts rendered'); + await dashboardExpect.seriesElementCount(15); log.debug('Checking saved searches rendered'); await dashboardExpect.savedSearchRowCount(49); log.debug('Checking input controls rendered'); diff --git a/x-pack/test/upgrade/apps/reporting/reporting_smoke_tests.ts b/x-pack/test/upgrade/apps/reporting/reporting_smoke_tests.ts index c00e761d54226..20fc34f77dbf8 100644 --- a/x-pack/test/upgrade/apps/reporting/reporting_smoke_tests.ts +++ b/x-pack/test/upgrade/apps/reporting/reporting_smoke_tests.ts @@ -69,6 +69,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { if (type === 'pdf_optimize') { await testSubjects.click('usePrintLayout'); } + const advOpt = await find.byXPath(`//button[descendant::*[text()='Advanced options']]`); + await advOpt.click(); const postUrl = await find.byXPath(`//button[descendant::*[text()='Copy POST URL']]`); await postUrl.click(); const url = await browser.getClipboardValue(); diff --git a/yarn.lock b/yarn.lock index 51df4a1e72b90..7a70953379f54 100644 --- a/yarn.lock +++ b/yarn.lock @@ -209,6 +209,13 @@ dependencies: "@babel/types" "^7.12.5" +"@babel/helper-module-imports@^7.7.0": + version "7.13.12" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz#c6a369a6f3621cb25da014078684da9196b61977" + integrity sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA== + dependencies: + "@babel/types" "^7.13.12" + "@babel/helper-module-transforms@^7.12.1": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz#7954fec71f5b32c48e4b303b437c34453fd7247c" @@ -548,6 +555,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-syntax-jsx@^7.2.0": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.13.tgz#044fb81ebad6698fe62c478875575bcbb9b70f15" + integrity sha512-d4HM23Q1K7oq/SLNmG6mRt85l2csmQ0cHRaxRXjKW0YFdEXqlZ5kzFQKH5Uc3rDJECgu+yCRgPkG04Mm98R/1g== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + "@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" @@ -1154,6 +1168,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.13.10": + version "7.13.17" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.17.tgz#8966d1fc9593bf848602f0662d6b4d0069e3a7ec" + integrity sha512-NCdgJEelPTSh+FEFylhnP1ylq848l1z9t9N0j1Lfbcw0+KXGjsTvUmkxy+voLLXB5SOKMbLLx4jxYliGrYQseA== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.10.4", "@babel/template@^7.12.13", "@babel/template@^7.12.7", "@babel/template@^7.3.3", "@babel/template@^7.4.4": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327" @@ -1187,6 +1208,15 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" +"@babel/types@^7.13.12": + version "7.13.14" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.14.tgz#c35a4abb15c7cd45a2746d78ab328e362cbace0d" + integrity sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ== + dependencies: + "@babel/helper-validator-identifier" "^7.12.11" + lodash "^4.17.19" + to-fast-properties "^2.0.0" + "@base2/pretty-print-object@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@base2/pretty-print-object/-/pretty-print-object-1.0.0.tgz#860ce718b0b73f4009e153541faff2cb6b85d047" @@ -1359,10 +1389,10 @@ dependencies: object-hash "^1.3.0" -"@elastic/charts@31.1.0": - version "31.1.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-31.1.0.tgz#cebfd45e672ab19d7d6c5a7f7e3115a6eaa41e8f" - integrity sha512-D2zPT7CRweRdbfhO9gd1+YBm0ETdJsEkh+Su0I6tleINqKKuSB+kPOG6t+fm0+HsR72pX4dKvT60ikZJZ3fRhg== +"@elastic/charts@32.0.0": + version "32.0.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-32.0.0.tgz#f747b8fa931027ba7476a6284be03383cd6a6dab" + integrity sha512-3cvX0Clezocd6/T2R5h3+nilPdIgWrO+it043giyW5U0pAtFC5P+5VyNEjn22LlD3zzbndxAbXHSj0QDvHXOBw== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0" @@ -1592,6 +1622,41 @@ resolved "https://registry.yarnpkg.com/@elastic/ui-ace/-/ui-ace-0.2.3.tgz#5281aed47a79b7216c55542b0675e435692f20cd" integrity sha512-Nti5s2dplBPhSKRwJxG9JXTMOev4jVOWcnTJD1TOkJr1MUBYKVZcNcJtIVMSvahWGmP0B/UfO9q9lyRqdivkvQ== +"@emotion/babel-plugin-jsx-pragmatic@^0.1.5": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@emotion/babel-plugin-jsx-pragmatic/-/babel-plugin-jsx-pragmatic-0.1.5.tgz#27debfe9c27c4d83574d509787ae553bf8a34d7e" + integrity sha512-y+3AJ0SItMDaAgGPVkQBC/S/BaqaPACkQ6MyCI2CUlrjTxKttTVfD3TMtcs7vLEcLxqzZ1xiG0vzwCXjhopawQ== + dependencies: + "@babel/plugin-syntax-jsx" "^7.2.0" + +"@emotion/babel-plugin@^11.2.0": + version "11.2.0" + resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.2.0.tgz#f25c6df8ec045dad5ae6ca63df0791673b98c920" + integrity sha512-lsnQBnl3l4wu/FJoyHnYRpHJeIPNkOBMbtDUIXcO8luulwRKZXPvA10zd2eXVN6dABIWNX4E34en/jkejIg/yA== + dependencies: + "@babel/helper-module-imports" "^7.7.0" + "@babel/plugin-syntax-jsx" "^7.12.1" + "@babel/runtime" "^7.7.2" + "@emotion/hash" "^0.8.0" + "@emotion/memoize" "^0.7.5" + "@emotion/serialize" "^1.0.0" + babel-plugin-macros "^2.6.1" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "^4.0.3" + +"@emotion/babel-preset-css-prop@^11.2.0": + version "11.2.0" + resolved "https://registry.yarnpkg.com/@emotion/babel-preset-css-prop/-/babel-preset-css-prop-11.2.0.tgz#c7e945f56b2610b438f0dc8ae5253fc55488de0e" + integrity sha512-9XLQm2eLPYTho+Cx1LQTDA1rATjoAaB4O+ds55XDvoAa+Z16Hhg8y5Vihj3C8E6+ilDM8SV5A9Z6z+yj0YIRBg== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.12.1" + "@babel/runtime" "^7.7.2" + "@emotion/babel-plugin" "^11.2.0" + "@emotion/babel-plugin-jsx-pragmatic" "^0.1.5" + "@emotion/babel-utils@^0.6.4": version "0.6.10" resolved "https://registry.yarnpkg.com/@emotion/babel-utils/-/babel-utils-0.6.10.tgz#83dbf3dfa933fae9fc566e54fbb45f14674c6ccc" @@ -1614,6 +1679,17 @@ "@emotion/utils" "0.11.3" "@emotion/weak-memoize" "0.2.5" +"@emotion/cache@^11.4.0": + version "11.4.0" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.4.0.tgz#293fc9d9a7a38b9aad8e9337e5014366c3b09ac0" + integrity sha512-Zx70bjE7LErRO9OaZrhf22Qye1y4F7iDl+ITjet0J+i+B88PrAOBkKvaAWhxsZf72tDLajwCgfCjJ2dvH77C3g== + dependencies: + "@emotion/memoize" "^0.7.4" + "@emotion/sheet" "^1.0.0" + "@emotion/utils" "^1.0.0" + "@emotion/weak-memoize" "^0.2.5" + stylis "^4.0.3" + "@emotion/core@^10.0.9", "@emotion/core@^10.1.1": version "10.1.1" resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.1.1.tgz#c956c1365f2f2481960064bcb8c4732e5fb612c3" @@ -1626,6 +1702,14 @@ "@emotion/sheet" "0.9.4" "@emotion/utils" "0.11.3" +"@emotion/css-prettifier@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@emotion/css-prettifier/-/css-prettifier-1.0.0.tgz#3ed4240d93c9798c001cedf27dd0aa960bdddd1a" + integrity sha512-efxSrRTiTqHTQVKW15Gz5H4pNAw8OqcG8NaiwkJIkqIdNXTD4Qr1zC1Ou6r2acd1oJJ2s56nb1ClnXMiWoj6gQ== + dependencies: + "@emotion/memoize" "^0.7.4" + stylis "^4.0.3" + "@emotion/css@^10.0.27", "@emotion/css@^10.0.9": version "10.0.27" resolved "https://registry.yarnpkg.com/@emotion/css/-/css-10.0.27.tgz#3a7458198fbbebb53b01b2b87f64e5e21241e14c" @@ -1635,7 +1719,7 @@ "@emotion/utils" "0.11.3" babel-plugin-emotion "^10.0.27" -"@emotion/hash@0.8.0": +"@emotion/hash@0.8.0", "@emotion/hash@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== @@ -1652,6 +1736,17 @@ dependencies: "@emotion/memoize" "0.7.4" +"@emotion/jest@^11.3.0": + version "11.3.0" + resolved "https://registry.yarnpkg.com/@emotion/jest/-/jest-11.3.0.tgz#43bed6dcb47c8691b346cee231861ebc8f9b0016" + integrity sha512-LZqYc3yerhic1IvAcEwBLRs1DsUt3oY7Oz6n+e+HU32iYOK/vpfzlhgmQURE94BHfv6eCOj6DV38f3jSnIkBkQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@emotion/css-prettifier" "^1.0.0" + chalk "^4.1.0" + specificity "^0.4.1" + stylis "^4.0.3" + "@emotion/memoize@0.7.4": version "0.7.4" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" @@ -1662,6 +1757,24 @@ resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.6.6.tgz#004b98298d04c7ca3b4f50ca2035d4f60d2eed1b" integrity sha512-h4t4jFjtm1YV7UirAFuSuFGyLa+NNxjdkq6DpFLANNQY5rHueFZHVY+8Cu1HYVP6DrheB0kv4m5xPjo7eKT7yQ== +"@emotion/memoize@^0.7.4", "@emotion/memoize@^0.7.5": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.5.tgz#2c40f81449a4e554e9fc6396910ed4843ec2be50" + integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ== + +"@emotion/react@^11.4.0": + version "11.4.0" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.4.0.tgz#2465ad7b073a691409b88dfd96dc17097ddad9b7" + integrity sha512-4XklWsl9BdtatLoJpSjusXhpKv9YVteYKh9hPKP1Sxl+mswEFoUe0WtmtWjxEjkA51DQ2QRMCNOvKcSlCQ7ivg== + dependencies: + "@babel/runtime" "^7.13.10" + "@emotion/cache" "^11.4.0" + "@emotion/serialize" "^1.0.2" + "@emotion/sheet" "^1.0.1" + "@emotion/utils" "^1.0.0" + "@emotion/weak-memoize" "^0.2.5" + hoist-non-react-statics "^3.3.1" + "@emotion/serialize@^0.11.15", "@emotion/serialize@^0.11.16": version "0.11.16" resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.16.tgz#dee05f9e96ad2fb25a5206b6d759b2d1ed3379ad" @@ -1683,11 +1796,27 @@ "@emotion/unitless" "^0.6.7" "@emotion/utils" "^0.8.2" +"@emotion/serialize@^1.0.0", "@emotion/serialize@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.0.2.tgz#77cb21a0571c9f68eb66087754a65fa97bfcd965" + integrity sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A== + dependencies: + "@emotion/hash" "^0.8.0" + "@emotion/memoize" "^0.7.4" + "@emotion/unitless" "^0.7.5" + "@emotion/utils" "^1.0.0" + csstype "^3.0.2" + "@emotion/sheet@0.9.4": version "0.9.4" resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.4.tgz#894374bea39ec30f489bbfc3438192b9774d32e5" integrity sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA== +"@emotion/sheet@^1.0.0", "@emotion/sheet@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.0.1.tgz#245f54abb02dfd82326e28689f34c27aa9b2a698" + integrity sha512-GbIvVMe4U+Zc+929N1V7nW6YYJtidj31lidSmdYcWozwoBIObXBnaJkKNDjZrLm9Nc0BR+ZyHNaRZxqNZbof5g== + "@emotion/styled-base@^10.0.27": version "10.0.31" resolved "https://registry.yarnpkg.com/@emotion/styled-base/-/styled-base-10.0.31.tgz#940957ee0aa15c6974adc7d494ff19765a2f742a" @@ -1716,7 +1845,7 @@ resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.7.1.tgz#50f63225e712d99e2b2b39c19c70fff023793ca5" integrity sha512-/SLmSIkN13M//53TtNxgxo57mcJk/UJIDFRKwOiLIBEyBHEcipgR6hNMQ/59Sl4VjCJ0Z/3zeAZyvnSLPG/1HQ== -"@emotion/unitless@0.7.5", "@emotion/unitless@^0.7.4": +"@emotion/unitless@0.7.5", "@emotion/unitless@^0.7.4", "@emotion/unitless@^0.7.5": version "0.7.5" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== @@ -1736,7 +1865,12 @@ resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.8.2.tgz#576ff7fb1230185b619a75d258cbc98f0867a8dc" integrity sha512-rLu3wcBWH4P5q1CGoSSH/i9hrXs7SlbRLkoq9IGuoPYNGQvDJ3pt/wmOM+XgYjIDRMVIdkUWt0RsfzF50JfnCw== -"@emotion/weak-memoize@0.2.5": +"@emotion/utils@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.0.0.tgz#abe06a83160b10570816c913990245813a2fd6af" + integrity sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA== + +"@emotion/weak-memoize@0.2.5", "@emotion/weak-memoize@^0.2.5": version "0.2.5" resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== @@ -7790,7 +7924,7 @@ babel-plugin-jest-hoist@^26.6.2: "@types/babel__core" "^7.0.0" "@types/babel__traverse" "^7.0.6" -babel-plugin-macros@^2.0.0, babel-plugin-macros@^2.8.0: +babel-plugin-macros@^2.0.0, babel-plugin-macros@^2.6.1, babel-plugin-macros@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138" integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg== @@ -9487,15 +9621,6 @@ cli-width@^3.0.0: resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== -clipboard@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.4.tgz#836dafd66cf0fea5d71ce5d5b0bf6e958009112d" - integrity sha512-Vw26VSLRpJfBofiVaFb/I8PVfdI1OxKcYShe6fm0sP/DtmiWQNCjhM/okTvdCo0G+lMMm1rMYbk4IK4x1X+kgQ== - dependencies: - good-listener "^1.2.2" - select "^1.1.2" - tiny-emitter "^2.0.0" - cliui@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" @@ -10718,6 +10843,11 @@ csstype@^2.5.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.14.tgz#004822a4050345b55ad4dcc00be1d9cf2f4296de" integrity sha512-2mSc+VEpGPblzAxyeR+vZhJKgYg0Og0nnRi7pmRXFYYxSfnOnW8A5wwQb4n4cE2nIOzqKOAzLCaEX6aBmNEv8A== +csstype@^3.0.2: + version "3.0.7" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.7.tgz#2a5fb75e1015e84dd15692f71e89a1450290950b" + integrity sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g== + cucumber-expressions@^5.0.13: version "5.0.18" resolved "https://registry.yarnpkg.com/cucumber-expressions/-/cucumber-expressions-5.0.18.tgz#6c70779efd3aebc5e9e7853938b1110322429596" @@ -11589,11 +11719,6 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= -delegate@^3.1.2: - version "3.2.0" - resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" - integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw== - delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" @@ -13489,9 +13614,9 @@ fast-redact@^3.0.0: integrity sha512-a/S/Hp6aoIjx7EmugtzLqXmcNsyFszqbt6qQ99BdG61QjBZF6shNis0BYR6TsZOQ1twYc0FN2Xdhwwbv6+KD0w== fast-safe-stringify@2.x.x, fast-safe-stringify@^2.0.4, fast-safe-stringify@^2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" - integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== + version "2.0.8" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.8.tgz#dc2af48c46cf712b683e849b2bbd446b32de936f" + integrity sha512-lXatBjf3WPjmWD6DpIZxkeSsCOwqI0maYMpgDlx8g4U2qi4lbjA9oH/HD2a87G+KfsUmo5WbJFmqBZlPxtptag== fast-shallow-equal@^1.0.0: version "1.0.0" @@ -14789,13 +14914,6 @@ gonzales-pe@^4.3.0: dependencies: minimist "^1.2.5" -good-listener@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" - integrity sha1-1TswzfkxPf+33JoNR3CWqm0UXFA= - dependencies: - delegate "^3.1.2" - got@5.6.0: version "5.6.0" resolved "https://registry.yarnpkg.com/got/-/got-5.6.0.tgz#bb1d7ee163b78082bbc8eb836f3f395004ea6fbf" @@ -15410,7 +15528,7 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.5, hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.5, hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -22172,8 +22290,6 @@ prismjs@1.24.0, prismjs@^1.22.0, prismjs@~1.23.0: version "1.24.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.24.0.tgz#0409c30068a6c52c89ef7f1089b3ca4de56be2ac" integrity sha512-SqV5GRsNqnzCL8k5dfAjCNhUrF3pR0A9lTDSCUZeh/LIshheXJEaP0hwLz2t4XHivd2J/v2HR+gRnigzeKe3cQ== - optionalDependencies: - clipboard "^2.0.0" private@^0.1.8, private@~0.1.5: version "0.1.8" @@ -23088,10 +23204,10 @@ react-popper@^2.2.4: react-fast-compare "^3.0.1" warning "^4.0.2" -react-query@^3.13.10: - version "3.13.10" - resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.13.10.tgz#b6a05e22a5debb6e2df79ada588179771cbd7df8" - integrity sha512-wFvKhEDnOVL5bFL+9KPgNsiOOei1Ad+l6l1awCBuoX7xMG+SXXKDOF2uuZFsJe0w6gdthdWN+00021yepTR31g== +react-query@^3.18.1: + version "3.18.1" + resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.18.1.tgz#893b5475a7b4add099e007105317446f7a2cd310" + integrity sha512-17lv3pQxU9n+cB5acUv0/cxNTjo9q8G+RsedC6Ax4V9D8xEM7Q5xf9xAbCPdEhDrrnzPjTls9fQEABKRSi7OJA== dependencies: "@babel/runtime" "^7.5.5" broadcast-channel "^3.4.1" @@ -24791,11 +24907,6 @@ select-hose@^2.0.0: resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= -select@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" - integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0= - selenium-webdriver@^4.0.0-alpha.7: version "4.0.0-alpha.7" resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.0.0-alpha.7.tgz#e3879d8457fd7ad8e4424094b7dc0540d99e6797" @@ -26207,6 +26318,11 @@ stylis@^3.5.0: resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.4.tgz#f665f25f5e299cf3d64654ab949a57c768b73fbe" integrity sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q== +stylis@^4.0.3: + version "4.0.7" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.7.tgz#412a90c28079417f3d27c028035095e4232d2904" + integrity sha512-OFFeUXFgwnGOKvEXaSv0D0KQ5ADP0n6g3SVONx6I/85JzNZ3u50FRwB3lVIk1QO2HNdI75tbVzc4Z66Gdp9voA== + subarg@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2" @@ -26828,11 +26944,6 @@ timsort@^0.3.0, timsort@~0.3.0: resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= -tiny-emitter@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.0.2.tgz#82d27468aca5ade8e5fd1e6d22b57dd43ebdfb7c" - integrity sha512-2NM0auVBGft5tee/OxP4PI3d8WItkDM+fPnaRAVo6xTDI2knbz9eC5ArWGqtGlYqiH3RU5yMpdyTTO7MguC4ow== - tiny-inflate@^1.0.0, tiny-inflate@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4"