diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 53270e45171921..dccfe48d8f5284 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -57,6 +57,13 @@ /x-pack/test/functional/services/transform_ui/ @elastic/ml-ui /x-pack/test/functional/services/transform.ts @elastic/ml-ui +# Maps +/x-pack/legacy/plugins/maps/ @elastic/kibana-gis +/x-pack/test/api_integration/apis/maps/ @elastic/kibana-gis +/x-pack/test/functional/apps/maps/ @elastic/kibana-gis +/x-pack/test/functional/es_archives/maps/ @elastic/kibana-gis +/x-pack/test/visual_regression/tests/maps/index.js @elastic/kibana-gis + # Operations /src/dev/ @elastic/kibana-operations /src/setup_node_env/ @elastic/kibana-operations diff --git a/.github/workflows/pr-project-assigner.yml b/.github/workflows/pr-project-assigner.yml index e2e5ce1a4d963d..59123731dce666 100644 --- a/.github/workflows/pr-project-assigner.yml +++ b/.github/workflows/pr-project-assigner.yml @@ -11,5 +11,11 @@ jobs: uses: elastic/github-actions/project-assigner@v1.0.0 id: project_assigner with: - issue-mappings: '[{"label": "Team:AppArch", "projectName": "kibana-app-arch", "columnId": 6173897}, {"label": "Feature:Lens", "projectName": "Lens", "columnId": 6219362}]' + issue-mappings: | + [ + { "label": "Team:AppArch", "projectName": "kibana-app-arch", "columnId": 6173897 }, + { "label": "Feature:Lens", "projectName": "Lens", "columnId": 6219362 }, + { "label": "Team:Platform", "projectName": "kibana-platform", "columnId": 5514360 }, + {"label": "Team:Canvas", "projectName": "canvas", "columnId": 6187580} + ] ghToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/project-assigner.yml b/.github/workflows/project-assigner.yml index 6fd6fd645b5ee9..aec3bf88f0ee20 100644 --- a/.github/workflows/project-assigner.yml +++ b/.github/workflows/project-assigner.yml @@ -11,7 +11,7 @@ jobs: uses: elastic/github-actions/project-assigner@v1.0.0 id: project_assigner with: - issue-mappings: '[{"label": "Team:AppArch", "projectName": "kibana-app-arch", "columnId": 6173895}, {"label": "Feature:Lens", "projectName": "Lens", "columnId": 6219363}]' + issue-mappings: '[{"label": "Team:AppArch", "projectName": "kibana-app-arch", "columnId": 6173895}, {"label": "Feature:Lens", "projectName": "Lens", "columnId": 6219363}, {"label": "Team:Canvas", "projectName": "canvas", "columnId": 6187593}]' ghToken: ${{ secrets.GITHUB_TOKEN }} - + diff --git a/docs/apm/advanced-queries.asciidoc b/docs/apm/advanced-queries.asciidoc index 1f064c1cad3fdc..942882f8c4dfb3 100644 --- a/docs/apm/advanced-queries.asciidoc +++ b/docs/apm/advanced-queries.asciidoc @@ -1,9 +1,7 @@ [[advanced-queries]] === Advanced queries -When querying, you're simply searching and selecting data from fields in Elasticsearch documents. -It may be helpful to view some of your documents in {kibana-ref}/discover.html[Discover] to better understand how APM data is stored in Elasticsearch. - +When querying in the APM app, you're simply searching and selecting data from fields in Elasticsearch documents. Queries entered into the query bar are also added as parameters to the URL, so it's easy to share a specific query or view with others. @@ -13,11 +11,48 @@ In the screenshot below, you can begin to see some of the transaction fields ava image::apm/images/apm-query-bar.png[Example of the Kibana Query bar in APM app in Kibana] [float] -==== Example queries +==== Example APM app queries * Exclude response times slower than 2000 ms: `transaction.duration.us > 2000000` * Filter by response status code: `context.response.status_code >= 400` * Filter by single user ID: `context.user.id : 12` -* View _all_ transactions for an endpoint, instead of just a sample - `processor.event: "transaction" AND transaction.name: ""` TIP: Read the {kibana-ref}/kuery-query.html[Kibana Query Language Enhancements] documentation to learn more about the capabilities of the {kib} query language. + +[float] +[[discover-advanced-queries]] +=== Querying in the Discover app + +It may also be helpful to view your APM data in the {kibana-ref}/discover.html[Discover app]. +Querying documents in Discover works the same way as querying in the APM app, +and all of the example queries listed above can also be used in the Discover app. + +[float] +==== Example Discover app query + +One example where you may want to make use of the Discover app, +is for viewing _all_ transactions for an endpoint, instead of just a sample. + +TIP: Starting in v7.6, you can view 10 samples per bucket in the APM app, instead of just one. + +Use the APM app to find a transaction name and time bucket that you're interested in learning more about. +Then, switch to the Discover app and make a search: + +["source","sh"] +----- +processor.event: "transaction" AND transaction.name: "" and transaction.duration.us > 13000 and transaction.duration.us < 14000` +----- + +In this example, we're interested in viewing all of the `APIRestController#customers` transactions +that took between 13 and 14 milliseconds. Here's what Discover returns: + +[role="screenshot"] +image::apm/images/advanced-discover.png[View all transactions in bucket] + +You can now explore the data until you find a specific transaction that you're interested in. +Copy that transaction's `transaction.id`, and paste it into the APM app to view the data in the context of the APM app: + +[role="screenshot"] +image::apm/images/specific-transaction-search.png[View specific transaction in apm app] +[role="screenshot"] +image::apm/images/specific-transaction.png[View specific transaction in apm app] diff --git a/docs/apm/images/advanced-discover.png b/docs/apm/images/advanced-discover.png new file mode 100644 index 00000000000000..56ba58b2c1d413 Binary files /dev/null and b/docs/apm/images/advanced-discover.png differ diff --git a/docs/apm/images/specific-transaction-search.png b/docs/apm/images/specific-transaction-search.png new file mode 100644 index 00000000000000..4ed548f0157134 Binary files /dev/null and b/docs/apm/images/specific-transaction-search.png differ diff --git a/docs/apm/images/specific-transaction.png b/docs/apm/images/specific-transaction.png new file mode 100644 index 00000000000000..9911dbd879f412 Binary files /dev/null and b/docs/apm/images/specific-transaction.png differ diff --git a/docs/canvas/canvas-elements.asciidoc b/docs/canvas/canvas-elements.asciidoc index c5c6f116ee34e6..dc605a47de383f 100644 --- a/docs/canvas/canvas-elements.asciidoc +++ b/docs/canvas/canvas-elements.asciidoc @@ -20,24 +20,24 @@ When you add elements to your workpad, you can: [[add-canvas-element]] === Add elements to your workpad -Choose the elements to display on your workpad, then familiarize yourself with the element using the preconfigured demo data. +Choose the elements to display on your workpad, then familiarize yourself with the element using the preconfigured demo data. By default, every element you add to a workpad uses demo data until you change the data source. The demo data includes a small sample data set that you can use to experiment with your element. . Click *Add element*. -. In the *Elements* window, select the element you want to use. +. In the *Elements* window, select the element you want to use. + [role="screenshot"] image::images/canvas-element-select.gif[Canvas elements] -. Play around with the default settings and see what the element can do. +. Play around with the default settings and see what the element can do. -TIP: Want to use a different element? You can delete the element by selecting it, clicking the *Element options* icon in the top right corner, then selecting *Delete*. +TIP: Want to use a different element? You can delete the element by selecting it, clicking the *Element options* icon in the top right, then selecting *Delete*. [float] [[connect-element-data]] === Connect the element to your data -When you are ready to move on from the demo data, connect the element to your own data. +When you have finished using the demo data, connect the element to a data source. . Make sure that the element is selected, then select *Data*. @@ -45,55 +45,51 @@ When you are ready to move on from the demo data, connect the element to your ow [float] [[elasticsearch-sql-data-source]] -==== Connect to Elasticsearch SQL +==== Connect to {es} SQL -Access your data in Elasticsearch using the Elasticsearch SQL syntax. +Access your data in {es} using SQL syntax. For information about SQL syntax, refer to {ref}/sql-spec.html[SQL language]. -Unfamiliar with writing Elasticsearch SQL queries? For more information, refer to {ref}/sql-spec.html[SQL language]. +. Click *{es} SQL*. -. Click *Elasticsearch SQL*. +. In the *{es} SQL query* box, enter your query, then *Preview* it. -. In the *Elasticearch SQL query* box, enter your query, then *Preview* it. - -. If everything looks correct, *Save* it. +. If everything looks correct, *Save* it. [float] [[elasticsearch-raw-doc-data-source]] -==== Connect to Elasticsearch raw data +==== Connect to {es} raw data -Use the Lucene query syntax to use your raw data in Elasticsearch. +Access your raw data in {es} without the use of aggregations. Use {es} raw data when you have low volume datasets, or to plot exact, non-aggregated values. -For for more information about the Lucene query string sytax, refer to <>. +To use targeted queries, you can enter a query using the <>. -. Click *Elasticsearch raw documents*. +. Click *{es} raw documents*. -. In the *Index* field, enter the index pattern that you want to display. +. In the *Index* field, enter the index pattern that you want to display. . From the *Fields* dropdown, select the associated fields you want to display. . To sort the data, select an option from the *Sort Field* and *Sort Order* dropdowns. -. For more targeted queries, enter a *Query* using the Lucene query string syntax. +. For more targeted queries, enter a *Query* using the Lucene query string syntax. -. *Preview* the query. +. *Preview* the query. -. If your query looks correct, *Save* it. +. If your query looks correct, *Save* it. [float] [[timelion-data-source]] ==== Connect to Timelion -Use <> queries to use your time series data. +Access your time series data using <> queries. To use Timelion queries, you can enter a query using the <>. . Click *Timelion*. -. Enter a *Query* using the Lucene query string syntax. -+ -For for more information about the Lucene query string syntax, refer to <>. +. Enter a *Query* using the Lucene query string syntax. . Enter the *Interval*, then *Preview* the query. -. If your query looks correct, *Save* it. +. If your query looks correct, *Save* it. [float] [[configure-display-options]] @@ -109,7 +105,7 @@ When you connect your element to a data source, the element often appears as a w . Click *Display* -. Change the display options for the element. +. Change the display options for the element. [float] [[element-display-container]] @@ -122,7 +118,7 @@ Further define the appearance of the element container and border. . Expand *Container style*. . Change the *Appearance* and *Border* options. - + [float] [[apply-element-styles]] ==== Apply a set of styles @@ -155,7 +151,7 @@ Increase or decrease how often your data refreshes on your workpad. [role="screenshot"] image::images/canvas-refresh-interval.png[Element data refresh interval] -TIP: To manually refresh the data, click the *Refresh data* icon. +TIP: To manually refresh the data, click the *Refresh data* icon. [float] [[organize-element]] @@ -223,7 +219,7 @@ Change the order of how the elements are displayed on your workpad. . Select an element. -. In the top right corder, click the *Element options* icon. +. In the top right corder, click the *Element options* icon. . Select *Order*, then select the order that you want the element to appear. @@ -262,7 +258,7 @@ When you have run out of room on your workpad page, add more pages. . Click *Page 1*, then click *+*. -. On the *Page* editor panel on the right, select the page transition from the *Transition* dropdown. +. On the *Page* editor panel on the right, select the page transition from the *Transition* dropdown. + [role="screenshot"] image::images/canvas-add-pages.gif[Add pages] diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.md b/docs/development/core/public/kibana-plugin-public.httpservicebase.md deleted file mode 100644 index 9ea77c95b343eb..00000000000000 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.md +++ /dev/null @@ -1,37 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) - -## HttpServiceBase interface - - -Signature: - -```typescript -export interface HttpServiceBase -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [anonymousPaths](./kibana-plugin-public.httpservicebase.anonymouspaths.md) | IAnonymousPaths | APIs for denoting certain paths for not requiring authentication | -| [basePath](./kibana-plugin-public.httpservicebase.basepath.md) | IBasePath | APIs for manipulating the basePath on URL segments. | -| [delete](./kibana-plugin-public.httpservicebase.delete.md) | HttpHandler | Makes an HTTP request with the DELETE method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | -| [fetch](./kibana-plugin-public.httpservicebase.fetch.md) | HttpHandler | Makes an HTTP request. Defaults to a GET request unless overriden. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | -| [get](./kibana-plugin-public.httpservicebase.get.md) | HttpHandler | Makes an HTTP request with the GET method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | -| [head](./kibana-plugin-public.httpservicebase.head.md) | HttpHandler | Makes an HTTP request with the HEAD method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | -| [options](./kibana-plugin-public.httpservicebase.options.md) | HttpHandler | Makes an HTTP request with the OPTIONS method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | -| [patch](./kibana-plugin-public.httpservicebase.patch.md) | HttpHandler | Makes an HTTP request with the PATCH method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | -| [post](./kibana-plugin-public.httpservicebase.post.md) | HttpHandler | Makes an HTTP request with the POST method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | -| [put](./kibana-plugin-public.httpservicebase.put.md) | HttpHandler | Makes an HTTP request with the PUT method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | - -## Methods - -| Method | Description | -| --- | --- | -| [addLoadingCount(countSource$)](./kibana-plugin-public.httpservicebase.addloadingcount.md) | Adds a new source of loading counts. Used to show the global loading indicator when sum of all observed counts are more than 0. | -| [getLoadingCount$()](./kibana-plugin-public.httpservicebase.getloadingcount_.md) | Get the sum of all loading count sources as a single Observable. | -| [intercept(interceptor)](./kibana-plugin-public.httpservicebase.intercept.md) | Adds a new [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) to the global HTTP client. | -| [removeAllInterceptors()](./kibana-plugin-public.httpservicebase.removeallinterceptors.md) | Removes all configured interceptors. | - diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.removeallinterceptors.md b/docs/development/core/public/kibana-plugin-public.httpservicebase.removeallinterceptors.md deleted file mode 100644 index 0432ec29a22b67..00000000000000 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.removeallinterceptors.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [removeAllInterceptors](./kibana-plugin-public.httpservicebase.removeallinterceptors.md) - -## HttpServiceBase.removeAllInterceptors() method - -Removes all configured interceptors. - -Signature: - -```typescript -removeAllInterceptors(): void; -``` -Returns: - -`void` - diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.addloadingcount.md b/docs/development/core/public/kibana-plugin-public.httpsetup.addloadingcountsource.md similarity index 62% rename from docs/development/core/public/kibana-plugin-public.httpservicebase.addloadingcount.md rename to docs/development/core/public/kibana-plugin-public.httpsetup.addloadingcountsource.md index e984fea48625df..a2fe66bb55c77b 100644 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.addloadingcount.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.addloadingcountsource.md @@ -1,15 +1,15 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [addLoadingCount](./kibana-plugin-public.httpservicebase.addloadingcount.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) > [addLoadingCountSource](./kibana-plugin-public.httpsetup.addloadingcountsource.md) -## HttpServiceBase.addLoadingCount() method +## HttpSetup.addLoadingCountSource() method Adds a new source of loading counts. Used to show the global loading indicator when sum of all observed counts are more than 0. Signature: ```typescript -addLoadingCount(countSource$: Observable): void; +addLoadingCountSource(countSource$: Observable): void; ``` ## Parameters diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.anonymouspaths.md b/docs/development/core/public/kibana-plugin-public.httpsetup.anonymouspaths.md similarity index 57% rename from docs/development/core/public/kibana-plugin-public.httpservicebase.anonymouspaths.md rename to docs/development/core/public/kibana-plugin-public.httpsetup.anonymouspaths.md index e94757c5eb031a..a9268ca1d8ed68 100644 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.anonymouspaths.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.anonymouspaths.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [anonymousPaths](./kibana-plugin-public.httpservicebase.anonymouspaths.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) > [anonymousPaths](./kibana-plugin-public.httpsetup.anonymouspaths.md) -## HttpServiceBase.anonymousPaths property +## HttpSetup.anonymousPaths property APIs for denoting certain paths for not requiring authentication diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.basepath.md b/docs/development/core/public/kibana-plugin-public.httpsetup.basepath.md similarity index 57% rename from docs/development/core/public/kibana-plugin-public.httpservicebase.basepath.md rename to docs/development/core/public/kibana-plugin-public.httpsetup.basepath.md index 6c5f690a5c607d..6b0726dc8ef2be 100644 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.basepath.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.basepath.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [basePath](./kibana-plugin-public.httpservicebase.basepath.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) > [basePath](./kibana-plugin-public.httpsetup.basepath.md) -## HttpServiceBase.basePath property +## HttpSetup.basePath property APIs for manipulating the basePath on URL segments. diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.delete.md b/docs/development/core/public/kibana-plugin-public.httpsetup.delete.md similarity index 63% rename from docs/development/core/public/kibana-plugin-public.httpservicebase.delete.md rename to docs/development/core/public/kibana-plugin-public.httpsetup.delete.md index 73022ef4f29463..565f0eb336d4fe 100644 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.delete.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.delete.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [delete](./kibana-plugin-public.httpservicebase.delete.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) > [delete](./kibana-plugin-public.httpsetup.delete.md) -## HttpServiceBase.delete property +## HttpSetup.delete property Makes an HTTP request with the DELETE method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.fetch.md b/docs/development/core/public/kibana-plugin-public.httpsetup.fetch.md similarity index 64% rename from docs/development/core/public/kibana-plugin-public.httpservicebase.fetch.md rename to docs/development/core/public/kibana-plugin-public.httpsetup.fetch.md index 3a1ae4892a3dd3..2d6447363fa9b7 100644 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.fetch.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.fetch.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [fetch](./kibana-plugin-public.httpservicebase.fetch.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) > [fetch](./kibana-plugin-public.httpsetup.fetch.md) -## HttpServiceBase.fetch property +## HttpSetup.fetch property Makes an HTTP request. Defaults to a GET request unless overriden. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.get.md b/docs/development/core/public/kibana-plugin-public.httpsetup.get.md similarity index 63% rename from docs/development/core/public/kibana-plugin-public.httpservicebase.get.md rename to docs/development/core/public/kibana-plugin-public.httpsetup.get.md index a61b3dd140e504..0c484e33e9b58e 100644 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.get.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.get.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [get](./kibana-plugin-public.httpservicebase.get.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) > [get](./kibana-plugin-public.httpsetup.get.md) -## HttpServiceBase.get property +## HttpSetup.get property Makes an HTTP request with the GET method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.getloadingcount_.md b/docs/development/core/public/kibana-plugin-public.httpsetup.getloadingcount_.md similarity index 59% rename from docs/development/core/public/kibana-plugin-public.httpservicebase.getloadingcount_.md rename to docs/development/core/public/kibana-plugin-public.httpsetup.getloadingcount_.md index 0b2129330cd015..628b62b2ffc272 100644 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.getloadingcount_.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.getloadingcount_.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [getLoadingCount$](./kibana-plugin-public.httpservicebase.getloadingcount_.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) > [getLoadingCount$](./kibana-plugin-public.httpsetup.getloadingcount_.md) -## HttpServiceBase.getLoadingCount$() method +## HttpSetup.getLoadingCount$() method Get the sum of all loading count sources as a single Observable. diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.head.md b/docs/development/core/public/kibana-plugin-public.httpsetup.head.md similarity index 63% rename from docs/development/core/public/kibana-plugin-public.httpservicebase.head.md rename to docs/development/core/public/kibana-plugin-public.httpsetup.head.md index 4624d95f03bf3d..e4d49c843e5720 100644 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.head.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.head.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [head](./kibana-plugin-public.httpservicebase.head.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) > [head](./kibana-plugin-public.httpsetup.head.md) -## HttpServiceBase.head property +## HttpSetup.head property Makes an HTTP request with the HEAD method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.intercept.md b/docs/development/core/public/kibana-plugin-public.httpsetup.intercept.md similarity index 72% rename from docs/development/core/public/kibana-plugin-public.httpservicebase.intercept.md rename to docs/development/core/public/kibana-plugin-public.httpsetup.intercept.md index 8cf5bf813df093..1bda0c6166e652 100644 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.intercept.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.intercept.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [intercept](./kibana-plugin-public.httpservicebase.intercept.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) > [intercept](./kibana-plugin-public.httpsetup.intercept.md) -## HttpServiceBase.intercept() method +## HttpSetup.intercept() method Adds a new [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) to the global HTTP client. diff --git a/docs/development/core/public/kibana-plugin-public.httpsetup.md b/docs/development/core/public/kibana-plugin-public.httpsetup.md index 7ef037ea7abd10..8a14d26c57ca36 100644 --- a/docs/development/core/public/kibana-plugin-public.httpsetup.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.md @@ -2,12 +2,35 @@ [Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) -## HttpSetup type +## HttpSetup interface -See [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) Signature: ```typescript -export declare type HttpSetup = HttpServiceBase; +export interface HttpSetup ``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [anonymousPaths](./kibana-plugin-public.httpsetup.anonymouspaths.md) | IAnonymousPaths | APIs for denoting certain paths for not requiring authentication | +| [basePath](./kibana-plugin-public.httpsetup.basepath.md) | IBasePath | APIs for manipulating the basePath on URL segments. | +| [delete](./kibana-plugin-public.httpsetup.delete.md) | HttpHandler | Makes an HTTP request with the DELETE method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | +| [fetch](./kibana-plugin-public.httpsetup.fetch.md) | HttpHandler | Makes an HTTP request. Defaults to a GET request unless overriden. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | +| [get](./kibana-plugin-public.httpsetup.get.md) | HttpHandler | Makes an HTTP request with the GET method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | +| [head](./kibana-plugin-public.httpsetup.head.md) | HttpHandler | Makes an HTTP request with the HEAD method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | +| [options](./kibana-plugin-public.httpsetup.options.md) | HttpHandler | Makes an HTTP request with the OPTIONS method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | +| [patch](./kibana-plugin-public.httpsetup.patch.md) | HttpHandler | Makes an HTTP request with the PATCH method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | +| [post](./kibana-plugin-public.httpsetup.post.md) | HttpHandler | Makes an HTTP request with the POST method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | +| [put](./kibana-plugin-public.httpsetup.put.md) | HttpHandler | Makes an HTTP request with the PUT method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | + +## Methods + +| Method | Description | +| --- | --- | +| [addLoadingCountSource(countSource$)](./kibana-plugin-public.httpsetup.addloadingcountsource.md) | Adds a new source of loading counts. Used to show the global loading indicator when sum of all observed counts are more than 0. | +| [getLoadingCount$()](./kibana-plugin-public.httpsetup.getloadingcount_.md) | Get the sum of all loading count sources as a single Observable. | +| [intercept(interceptor)](./kibana-plugin-public.httpsetup.intercept.md) | Adds a new [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) to the global HTTP client. | + diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.options.md b/docs/development/core/public/kibana-plugin-public.httpsetup.options.md similarity index 62% rename from docs/development/core/public/kibana-plugin-public.httpservicebase.options.md rename to docs/development/core/public/kibana-plugin-public.httpsetup.options.md index 0820beb2752f2c..4ea5be8826bff9 100644 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.options.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.options.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [options](./kibana-plugin-public.httpservicebase.options.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) > [options](./kibana-plugin-public.httpsetup.options.md) -## HttpServiceBase.options property +## HttpSetup.options property Makes an HTTP request with the OPTIONS method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.patch.md b/docs/development/core/public/kibana-plugin-public.httpsetup.patch.md similarity index 63% rename from docs/development/core/public/kibana-plugin-public.httpservicebase.patch.md rename to docs/development/core/public/kibana-plugin-public.httpsetup.patch.md index 00e1ffc0e16bf6..ef1d50005b012b 100644 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.patch.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.patch.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [patch](./kibana-plugin-public.httpservicebase.patch.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) > [patch](./kibana-plugin-public.httpsetup.patch.md) -## HttpServiceBase.patch property +## HttpSetup.patch property Makes an HTTP request with the PATCH method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.post.md b/docs/development/core/public/kibana-plugin-public.httpsetup.post.md similarity index 63% rename from docs/development/core/public/kibana-plugin-public.httpservicebase.post.md rename to docs/development/core/public/kibana-plugin-public.httpsetup.post.md index 3771a7c910895e..1c19c35ac30382 100644 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.post.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.post.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [post](./kibana-plugin-public.httpservicebase.post.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) > [post](./kibana-plugin-public.httpsetup.post.md) -## HttpServiceBase.post property +## HttpSetup.post property Makes an HTTP request with the POST method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.put.md b/docs/development/core/public/kibana-plugin-public.httpsetup.put.md similarity index 63% rename from docs/development/core/public/kibana-plugin-public.httpservicebase.put.md rename to docs/development/core/public/kibana-plugin-public.httpsetup.put.md index 6e43aafa916bc1..e5243d8c80daed 100644 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.put.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.put.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [put](./kibana-plugin-public.httpservicebase.put.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) > [put](./kibana-plugin-public.httpsetup.put.md) -## HttpServiceBase.put property +## HttpSetup.put property Makes an HTTP request with the PUT method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. diff --git a/docs/development/core/public/kibana-plugin-public.httpstart.md b/docs/development/core/public/kibana-plugin-public.httpstart.md index bb9247c63897a8..9abf319acf00dd 100644 --- a/docs/development/core/public/kibana-plugin-public.httpstart.md +++ b/docs/development/core/public/kibana-plugin-public.httpstart.md @@ -4,10 +4,10 @@ ## HttpStart type -See [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) +See [HttpSetup](./kibana-plugin-public.httpsetup.md) Signature: ```typescript -export declare type HttpStart = HttpServiceBase; +export declare type HttpStart = HttpSetup; ``` diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index 2c43f36ede09e6..e2c2866b57b6b0 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -56,7 +56,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [HttpHeadersInit](./kibana-plugin-public.httpheadersinit.md) | | | [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) | An object that may define global interceptor functions for different parts of the request and response lifecycle. See [IHttpInterceptController](./kibana-plugin-public.ihttpinterceptcontroller.md). | | [HttpRequestInit](./kibana-plugin-public.httprequestinit.md) | Fetch API options available to [HttpHandler](./kibana-plugin-public.httphandler.md)s. | -| [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) | | +| [HttpSetup](./kibana-plugin-public.httpsetup.md) | | | [I18nStart](./kibana-plugin-public.i18nstart.md) | I18nStart.Context is required by any localizable React component from @kbn/i18n and @elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. | | [IAnonymousPaths](./kibana-plugin-public.ianonymouspaths.md) | APIs for denoting paths as not requiring authentication | | [IBasePath](./kibana-plugin-public.ibasepath.md) | APIs for manipulating the basePath on URL segments. | @@ -118,8 +118,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [HandlerContextType](./kibana-plugin-public.handlercontexttype.md) | Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md) to represent the type of the context. | | [HandlerFunction](./kibana-plugin-public.handlerfunction.md) | A function that accepts a context object and an optional number of additional arguments. Used for the generic types in [IContextContainer](./kibana-plugin-public.icontextcontainer.md) | | [HandlerParameters](./kibana-plugin-public.handlerparameters.md) | Extracts the types of the additional arguments of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md), excluding the [HandlerContextType](./kibana-plugin-public.handlercontexttype.md). | -| [HttpSetup](./kibana-plugin-public.httpsetup.md) | See [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) | -| [HttpStart](./kibana-plugin-public.httpstart.md) | See [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) | +| [HttpStart](./kibana-plugin-public.httpstart.md) | See [HttpSetup](./kibana-plugin-public.httpsetup.md) | | [IContextProvider](./kibana-plugin-public.icontextprovider.md) | A function that returns a context value for a specific key of given context type. | | [IToasts](./kibana-plugin-public.itoasts.md) | Methods for adding and removing global toast messages. See [ToastsApi](./kibana-plugin-public.toastsapi.md). | | [MountPoint](./kibana-plugin-public.mountpoint.md) | A function that should mount DOM content inside the provided container element and return a handler to unmount it. | diff --git a/docs/maps/vector-style-properties.asciidoc b/docs/maps/vector-style-properties.asciidoc index f51632218add13..5656a7f04d0e31 100644 --- a/docs/maps/vector-style-properties.asciidoc +++ b/docs/maps/vector-style-properties.asciidoc @@ -8,32 +8,52 @@ Point, polygon, and line features support different styling properties. [[point-style-properties]] ==== Point style properties +You can add text labels to your Point features by configuring label style properties. + +[cols="2*"] +|=== +|*Label* +|Specifies label content. +|*Label color* +|The text color. +|*Label size* +|The size of the text font, in pixels. +|=== + You can symbolize Point features as *Circle markers* or *Icons*. Use *Circle marker* to symbolize Points as circles. -*Fill color*:: The fill color of the point features. - -*Border color*:: The border color of the point features. - -*Border width*:: The border width of the point features. - -*Symbol size*:: The radius of the symbol size, in pixels. +[cols="2*"] +|=== +|*Border color* +|The border color of the point features. +|*Border width* +|The border width of the point features. +|*Fill color* +|The fill color of the point features. +|*Symbol size* +|The radius of the symbol size, in pixels. +|=== Use *Icon* to symbolize Points as icons. -*Fill color*:: The fill color of the point features. - -*Border color*:: The border color of the point features. - -*Border width*:: The border width of the point features. +[cols="2*"] +|=== +|*Border color* +|The border color of the point features. +|*Border width* +|The border width of the point features. +|*Fill color* +|The fill color of the point features. +|*Symbol orientation* +|The symbol orientation rotating the icon clockwise. +|*Symbol size* +|The radius of the symbol size, in pixels. +|=== -*Symbol orientation*:: The symbol orientation rotating the icon clockwise. - -*Symbol size*:: The radius of the symbol size, in pixels. -+ Available icons -+ + [role="screenshot"] image::maps/images/maki-icons.png[] @@ -42,17 +62,25 @@ image::maps/images/maki-icons.png[] [[polygon-style-properties]] ==== Polygon style properties -*Fill color*:: The fill color of the polygon features. - -*Border color*:: The border color of the polygon features. - -*Border width*:: The border width of the polygon features. +[cols="2*"] +|=== +|*Border color* +|The border color of the polygon features. +|*Border width* +|The border width of the polygon features. +|*Fill color* +|The fill color of the polygon features. +|=== [float] [[line-style-properties]] ==== Line style properties -*Border color*:: The color of the line features. - -*Border width*:: The width of the line features. +[cols="2*"] +|=== +|*Border color* +|The color of the line features. +|*Border width* +|The width of the line features. +|=== diff --git a/docs/user/reporting/reporting-troubleshooting.asciidoc b/docs/user/reporting/reporting-troubleshooting.asciidoc index 92464c24b45eac..ca7fa6abcc9d9c 100644 --- a/docs/user/reporting/reporting-troubleshooting.asciidoc +++ b/docs/user/reporting/reporting-troubleshooting.asciidoc @@ -17,6 +17,7 @@ dependencies for Chromium. Make sure Kibana server OS has the appropriate packages installed for the distribution. If you are using CentOS/RHEL systems, install the following packages: + * `ipa-gothic-fonts` * `xorg-x11-fonts-100dpi` * `xorg-x11-fonts-75dpi` @@ -28,6 +29,7 @@ If you are using CentOS/RHEL systems, install the following packages: * `freetype` If you are using Ubuntu/Debian systems, install the following packages: + * `fonts-liberation` * `libfontconfig1` @@ -105,9 +107,10 @@ has its own command-line method to generate its own debug logs, which can someti caused by Kibana or Chromium. See more at https://github.com/GoogleChrome/puppeteer/blob/v1.19.0/README.md#debugging-tips Using Puppeteer's debug method when launching Kibana would look like: -> Enable verbose logging - internal DevTools protocol traffic will be logged via the debug module under the puppeteer namespace. -> ``` -> env DEBUG="puppeteer:*" ./bin/kibana -> ``` +``` +env DEBUG="puppeteer:*" ./bin/kibana +``` +The internal DevTools protocol traffic will be logged via the `debug` module under the `puppeteer` namespace. + The Puppeteer logs are very verbose and could possibly contain sensitive information. Handle the generated output with care. diff --git a/docs/user/reporting/watch-example.asciidoc b/docs/user/reporting/watch-example.asciidoc index 4c769c85975c48..627e31017230c9 100644 --- a/docs/user/reporting/watch-example.asciidoc +++ b/docs/user/reporting/watch-example.asciidoc @@ -56,7 +56,16 @@ report from the Kibana UI. //For more information, see <>. //<>. -NOTE: Reporting is integrated with Watcher only as an email attachment type. +[NOTE] +==== +Reporting is integrated with Watcher only as an email attachment type. + +The report Generation URL might contain date-math expressions +that cause the watch to fail with a `parse_exception`. +Remove curly braces `{` `}` from date-math expressions and +URL-encode characters to avoid this. +For example: `...(range:(%27@timestamp%27:(gte:now-15m%2Fd,lte:now%2Fd))))...` For more information about configuring watches, see {ref}/how-watcher-works.html[How Watcher works]. +==== diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index abc4c144356e8e..2a9dca96062dc9 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -211,7 +211,7 @@ export class CoreSystem { const injectedMetadata = await this.injectedMetadata.start(); const uiSettings = await this.uiSettings.start(); const docLinks = await this.docLinks.start({ injectedMetadata }); - const http = await this.http.start({ injectedMetadata, fatalErrors: this.fatalErrorsSetup }); + const http = await this.http.start({ injectedMetadata, fatalErrors: this.fatalErrorsSetup! }); const savedObjects = await this.savedObjects.start({ http }); const i18n = await this.i18n.start(); const application = await this.application.start({ http, injectedMetadata }); diff --git a/src/core/public/http/__snapshots__/http_service.test.ts.snap b/src/core/public/http/__snapshots__/http_service.test.ts.snap deleted file mode 100644 index 3d0309476365d5..00000000000000 --- a/src/core/public/http/__snapshots__/http_service.test.ts.snap +++ /dev/null @@ -1,38 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`addLoadingCount() adds a fatal error if source observable emits a negative number 1`] = ` -Array [ - Array [ - [Error: Observables passed to loadingCount.add() must only emit positive numbers], - ], -] -`; - -exports[`addLoadingCount() adds a fatal error if source observables emit an error 1`] = ` -Array [ - Array [ - [Error: foo bar], - ], -] -`; - -exports[`getLoadingCount$() emits 0 initially, the right count when sources emit their own count, and ends with zero 1`] = ` -Array [ - 0, - 100, - 110, - 111, - 11, - 21, - 20, - 0, -] -`; - -exports[`getLoadingCount$() only emits when loading count changes 1`] = ` -Array [ - 0, - 1, - 0, -] -`; diff --git a/src/core/public/http/anonymous_paths.test.ts b/src/core/public/http/anonymous_paths.test.ts deleted file mode 100644 index bf9212f625f1e4..00000000000000 --- a/src/core/public/http/anonymous_paths.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { AnonymousPaths } from './anonymous_paths'; -import { BasePath } from './base_path_service'; - -describe('#register', () => { - it(`allows paths that don't start with /`, () => { - const basePath = new BasePath('/foo'); - const anonymousPaths = new AnonymousPaths(basePath); - anonymousPaths.register('bar'); - }); - - it(`allows paths that end with '/'`, () => { - const basePath = new BasePath('/foo'); - const anonymousPaths = new AnonymousPaths(basePath); - anonymousPaths.register('/bar/'); - }); -}); - -describe('#isAnonymous', () => { - it('returns true for registered paths', () => { - const basePath = new BasePath('/foo'); - const anonymousPaths = new AnonymousPaths(basePath); - anonymousPaths.register('/bar'); - expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true); - }); - - it('returns true for paths registered with a trailing slash, but call "isAnonymous" with no trailing slash', () => { - const basePath = new BasePath('/foo'); - const anonymousPaths = new AnonymousPaths(basePath); - anonymousPaths.register('/bar/'); - expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true); - }); - - it('returns true for paths registered without a trailing slash, but call "isAnonymous" with a trailing slash', () => { - const basePath = new BasePath('/foo'); - const anonymousPaths = new AnonymousPaths(basePath); - anonymousPaths.register('/bar'); - expect(anonymousPaths.isAnonymous('/foo/bar/')).toBe(true); - }); - - it('returns true for paths registered without a starting slash', () => { - const basePath = new BasePath('/foo'); - const anonymousPaths = new AnonymousPaths(basePath); - anonymousPaths.register('bar'); - expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true); - }); - - it('returns true for paths registered with a starting slash', () => { - const basePath = new BasePath('/foo'); - const anonymousPaths = new AnonymousPaths(basePath); - anonymousPaths.register('/bar'); - expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true); - }); - - it('when there is no basePath and calling "isAnonymous" without a starting slash, returns true for paths registered with a starting slash', () => { - const basePath = new BasePath('/'); - const anonymousPaths = new AnonymousPaths(basePath); - anonymousPaths.register('/bar'); - expect(anonymousPaths.isAnonymous('bar')).toBe(true); - }); - - it('when there is no basePath and calling "isAnonymous" with a starting slash, returns true for paths registered with a starting slash', () => { - const basePath = new BasePath('/'); - const anonymousPaths = new AnonymousPaths(basePath); - anonymousPaths.register('/bar'); - expect(anonymousPaths.isAnonymous('/bar')).toBe(true); - }); - - it('returns true for paths whose capitalization is different', () => { - const basePath = new BasePath('/foo'); - const anonymousPaths = new AnonymousPaths(basePath); - anonymousPaths.register('/BAR'); - expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true); - }); - - it('returns false for other paths', () => { - const basePath = new BasePath('/foo'); - const anonymousPaths = new AnonymousPaths(basePath); - anonymousPaths.register('/bar'); - expect(anonymousPaths.isAnonymous('/foo/foo')).toBe(false); - }); - - it('returns false for sub-paths of registered paths', () => { - const basePath = new BasePath('/foo'); - const anonymousPaths = new AnonymousPaths(basePath); - anonymousPaths.register('/bar'); - expect(anonymousPaths.isAnonymous('/foo/bar/baz')).toBe(false); - }); -}); diff --git a/src/core/public/http/anonymous_paths.ts b/src/core/public/http/anonymous_paths.ts deleted file mode 100644 index 300c4d64df353c..00000000000000 --- a/src/core/public/http/anonymous_paths.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { IAnonymousPaths, IBasePath } from 'src/core/public'; - -export class AnonymousPaths implements IAnonymousPaths { - private readonly paths = new Set(); - - constructor(private basePath: IBasePath) {} - - public isAnonymous(path: string): boolean { - const pathWithoutBasePath = this.basePath.remove(path); - return this.paths.has(this.normalizePath(pathWithoutBasePath)); - } - - public register(path: string) { - this.paths.add(this.normalizePath(path)); - } - - private normalizePath(path: string) { - // always lower-case it - let normalized = path.toLowerCase(); - - // remove the slash from the end - if (normalized.endsWith('/')) { - normalized = normalized.slice(0, normalized.length - 1); - } - - // put a slash at the start - if (!normalized.startsWith('/')) { - normalized = `/${normalized}`; - } - - // it's normalized!!! - return normalized; - } -} diff --git a/src/core/public/http/anonymous_paths_service.test.ts b/src/core/public/http/anonymous_paths_service.test.ts new file mode 100644 index 00000000000000..515715d9a613db --- /dev/null +++ b/src/core/public/http/anonymous_paths_service.test.ts @@ -0,0 +1,109 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AnonymousPathsService } from './anonymous_paths_service'; +import { BasePath } from './base_path'; + +describe('#setup()', () => { + describe('#register', () => { + it(`allows paths that don't start with /`, () => { + const basePath = new BasePath('/foo'); + const anonymousPaths = new AnonymousPathsService().setup({ basePath }); + anonymousPaths.register('bar'); + }); + + it(`allows paths that end with '/'`, () => { + const basePath = new BasePath('/foo'); + const anonymousPaths = new AnonymousPathsService().setup({ basePath }); + anonymousPaths.register('/bar/'); + }); + }); + + describe('#isAnonymous', () => { + it('returns true for registered paths', () => { + const basePath = new BasePath('/foo'); + const anonymousPaths = new AnonymousPathsService().setup({ basePath }); + anonymousPaths.register('/bar'); + expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true); + }); + + it('returns true for paths registered with a trailing slash, but call "isAnonymous" with no trailing slash', () => { + const basePath = new BasePath('/foo'); + const anonymousPaths = new AnonymousPathsService().setup({ basePath }); + anonymousPaths.register('/bar/'); + expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true); + }); + + it('returns true for paths registered without a trailing slash, but call "isAnonymous" with a trailing slash', () => { + const basePath = new BasePath('/foo'); + const anonymousPaths = new AnonymousPathsService().setup({ basePath }); + anonymousPaths.register('/bar'); + expect(anonymousPaths.isAnonymous('/foo/bar/')).toBe(true); + }); + + it('returns true for paths registered without a starting slash', () => { + const basePath = new BasePath('/foo'); + const anonymousPaths = new AnonymousPathsService().setup({ basePath }); + anonymousPaths.register('bar'); + expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true); + }); + + it('returns true for paths registered with a starting slash', () => { + const basePath = new BasePath('/foo'); + const anonymousPaths = new AnonymousPathsService().setup({ basePath }); + anonymousPaths.register('/bar'); + expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true); + }); + + it('when there is no basePath and calling "isAnonymous" without a starting slash, returns true for paths registered with a starting slash', () => { + const basePath = new BasePath('/'); + const anonymousPaths = new AnonymousPathsService().setup({ basePath }); + anonymousPaths.register('/bar'); + expect(anonymousPaths.isAnonymous('bar')).toBe(true); + }); + + it('when there is no basePath and calling "isAnonymous" with a starting slash, returns true for paths registered with a starting slash', () => { + const basePath = new BasePath('/'); + const anonymousPaths = new AnonymousPathsService().setup({ basePath }); + anonymousPaths.register('/bar'); + expect(anonymousPaths.isAnonymous('/bar')).toBe(true); + }); + + it('returns true for paths whose capitalization is different', () => { + const basePath = new BasePath('/foo'); + const anonymousPaths = new AnonymousPathsService().setup({ basePath }); + anonymousPaths.register('/BAR'); + expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true); + }); + + it('returns false for other paths', () => { + const basePath = new BasePath('/foo'); + const anonymousPaths = new AnonymousPathsService().setup({ basePath }); + anonymousPaths.register('/bar'); + expect(anonymousPaths.isAnonymous('/foo/foo')).toBe(false); + }); + + it('returns false for sub-paths of registered paths', () => { + const basePath = new BasePath('/foo'); + const anonymousPaths = new AnonymousPathsService().setup({ basePath }); + anonymousPaths.register('/bar'); + expect(anonymousPaths.isAnonymous('/foo/bar/baz')).toBe(false); + }); + }); +}); diff --git a/src/core/public/http/anonymous_paths_service.ts b/src/core/public/http/anonymous_paths_service.ts new file mode 100644 index 00000000000000..ee9b3578c02708 --- /dev/null +++ b/src/core/public/http/anonymous_paths_service.ts @@ -0,0 +1,68 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IAnonymousPaths, IBasePath } from 'src/core/public'; +import { CoreService } from '../../types'; + +interface Deps { + basePath: IBasePath; +} + +export class AnonymousPathsService implements CoreService { + private readonly paths = new Set(); + + public setup({ basePath }: Deps) { + return { + isAnonymous: (path: string): boolean => { + const pathWithoutBasePath = basePath.remove(path); + return this.paths.has(normalizePath(pathWithoutBasePath)); + }, + + register: (path: string) => { + this.paths.add(normalizePath(path)); + }, + + normalizePath, + }; + } + + public start(deps: Deps) { + return this.setup(deps); + } + + public stop() {} +} + +const normalizePath = (path: string) => { + // always lower-case it + let normalized = path.toLowerCase(); + + // remove the slash from the end + if (normalized.endsWith('/')) { + normalized = normalized.slice(0, normalized.length - 1); + } + + // put a slash at the start + if (!normalized.startsWith('/')) { + normalized = `/${normalized}`; + } + + // it's normalized!!! + return normalized; +}; diff --git a/src/core/public/http/base_path_service.test.ts b/src/core/public/http/base_path.test.ts similarity index 98% rename from src/core/public/http/base_path_service.test.ts rename to src/core/public/http/base_path.test.ts index 65403c906e614e..63b7fa61cee846 100644 --- a/src/core/public/http/base_path_service.test.ts +++ b/src/core/public/http/base_path.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { BasePath } from './base_path_service'; +import { BasePath } from './base_path'; describe('BasePath', () => { describe('#get()', () => { diff --git a/src/core/public/http/base_path_service.ts b/src/core/public/http/base_path.ts similarity index 100% rename from src/core/public/http/base_path_service.ts rename to src/core/public/http/base_path.ts diff --git a/src/core/public/http/fetch.test.ts b/src/core/public/http/fetch.test.ts new file mode 100644 index 00000000000000..adb3d696a962fd --- /dev/null +++ b/src/core/public/http/fetch.test.ts @@ -0,0 +1,569 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// @ts-ignore +import fetchMock from 'fetch-mock/es5/client'; +import { readFileSync } from 'fs'; +import { join } from 'path'; + +import { Fetch } from './fetch'; +import { BasePath } from './base_path'; +import { IHttpResponse } from './types'; + +function delay(duration: number) { + return new Promise(r => setTimeout(r, duration)); +} + +describe('Fetch', () => { + const fetchInstance = new Fetch({ + basePath: new BasePath('http://localhost/myBase'), + kibanaVersion: 'VERSION', + }); + afterEach(() => { + fetchMock.restore(); + }); + + describe('http requests', () => { + it('should use supplied request method', async () => { + fetchMock.post('*', {}); + await fetchInstance.fetch('/my/path', { method: 'POST' }); + + expect(fetchMock.lastOptions()!.method).toBe('POST'); + }); + + it('should use supplied Content-Type', async () => { + fetchMock.get('*', {}); + await fetchInstance.fetch('/my/path', { headers: { 'Content-Type': 'CustomContentType' } }); + + expect(fetchMock.lastOptions()!.headers).toMatchObject({ + 'content-type': 'CustomContentType', + }); + }); + + it('should use supplied pathname and querystring', async () => { + fetchMock.get('*', {}); + await fetchInstance.fetch('/my/path', { query: { a: 'b' } }); + + expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path?a=b'); + }); + + it('should use supplied headers', async () => { + fetchMock.get('*', {}); + await fetchInstance.fetch('/my/path', { + headers: { myHeader: 'foo' }, + }); + + expect(fetchMock.lastOptions()!.headers).toEqual({ + 'content-type': 'application/json', + 'kbn-version': 'VERSION', + myheader: 'foo', + }); + }); + + it('should return response', async () => { + fetchMock.get('*', { foo: 'bar' }); + const json = await fetchInstance.fetch('/my/path'); + expect(json).toEqual({ foo: 'bar' }); + }); + + it('should prepend url with basepath by default', async () => { + fetchMock.get('*', {}); + await fetchInstance.fetch('/my/path'); + expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path'); + }); + + it('should not prepend url with basepath when disabled', async () => { + fetchMock.get('*', {}); + await fetchInstance.fetch('my/path', { prependBasePath: false }); + expect(fetchMock.lastUrl()).toBe('/my/path'); + }); + + it('should not include undefined query params', async () => { + fetchMock.get('*', {}); + await fetchInstance.fetch('/my/path', { query: { a: undefined } }); + expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path'); + }); + + it('should make request with defaults', async () => { + fetchMock.get('*', {}); + await fetchInstance.fetch('/my/path'); + + const lastCall = fetchMock.lastCall(); + + expect(lastCall!.request.credentials).toBe('same-origin'); + expect(lastCall![1]).toMatchObject({ + method: 'GET', + headers: { + 'content-type': 'application/json', + 'kbn-version': 'VERSION', + }, + }); + }); + + it('should expose detailed response object when asResponse = true', async () => { + fetchMock.get('*', { foo: 'bar' }); + + const response = await fetchInstance.fetch('/my/path', { asResponse: true }); + + expect(response.request).toBeInstanceOf(Request); + expect(response.response).toBeInstanceOf(Response); + expect(response.body).toEqual({ foo: 'bar' }); + }); + + it('should reject on network error', async () => { + expect.assertions(1); + fetchMock.get('*', { status: 500 }); + + await expect(fetchInstance.fetch('/my/path')).rejects.toThrow(/Internal Server Error/); + }); + + it('should contain error message when throwing response', async () => { + fetchMock.get('*', { status: 404, body: { foo: 'bar' } }); + + await expect(fetchInstance.fetch('/my/path')).rejects.toMatchObject({ + message: 'Not Found', + body: { + foo: 'bar', + }, + response: { + status: 404, + url: 'http://localhost/myBase/my/path', + }, + }); + }); + + it('should support get() helper', async () => { + fetchMock.get('*', {}); + await fetchInstance.get('/my/path', { method: 'POST' }); + + expect(fetchMock.lastOptions()!.method).toBe('GET'); + }); + + it('should support head() helper', async () => { + fetchMock.head('*', {}); + await fetchInstance.head('/my/path', { method: 'GET' }); + + expect(fetchMock.lastOptions()!.method).toBe('HEAD'); + }); + + it('should support post() helper', async () => { + fetchMock.post('*', {}); + await fetchInstance.post('/my/path', { method: 'GET', body: '{}' }); + + expect(fetchMock.lastOptions()!.method).toBe('POST'); + }); + + it('should support put() helper', async () => { + fetchMock.put('*', {}); + await fetchInstance.put('/my/path', { method: 'GET', body: '{}' }); + + expect(fetchMock.lastOptions()!.method).toBe('PUT'); + }); + + it('should support patch() helper', async () => { + fetchMock.patch('*', {}); + await fetchInstance.patch('/my/path', { method: 'GET', body: '{}' }); + + expect(fetchMock.lastOptions()!.method).toBe('PATCH'); + }); + + it('should support delete() helper', async () => { + fetchMock.delete('*', {}); + await fetchInstance.delete('/my/path', { method: 'GET' }); + + expect(fetchMock.lastOptions()!.method).toBe('DELETE'); + }); + + it('should support options() helper', async () => { + fetchMock.mock('*', { method: 'OPTIONS' }); + await fetchInstance.options('/my/path', { method: 'GET' }); + + expect(fetchMock.lastOptions()!.method).toBe('OPTIONS'); + }); + + it('should make requests for NDJSON content', async () => { + const content = readFileSync(join(__dirname, '_import_objects.ndjson'), { + encoding: 'utf-8', + }); + const body = new FormData(); + + body.append('file', content); + fetchMock.post('*', { + body: content, + headers: { 'Content-Type': 'application/ndjson' }, + }); + + const data = await fetchInstance.post('/my/path', { + body, + headers: { + 'Content-Type': undefined, + }, + }); + + expect(data).toBeInstanceOf(Blob); + + const ndjson = await new Response(data).text(); + + expect(ndjson).toEqual(content); + }); + }); + + describe('interception', () => { + beforeEach(async () => { + fetchMock.get('*', { foo: 'bar' }); + }); + + afterEach(() => { + fetchMock.restore(); + fetchInstance.removeAllInterceptors(); + }); + + it('should make request and receive response', async () => { + fetchInstance.intercept({}); + + const body = await fetchInstance.fetch('/my/path'); + + expect(fetchMock.called()).toBe(true); + expect(body).toEqual({ foo: 'bar' }); + }); + + it('should be able to manipulate request instance', async () => { + fetchInstance.intercept({ + request(request) { + request.headers.set('Content-Type', 'CustomContentType'); + }, + }); + fetchInstance.intercept({ + request(request) { + return new Request('/my/route', request); + }, + }); + + const body = await fetchInstance.fetch('/my/path'); + + expect(fetchMock.called()).toBe(true); + expect(body).toEqual({ foo: 'bar' }); + expect(fetchMock.lastOptions()!.headers).toMatchObject({ + 'content-type': 'CustomContentType', + }); + expect(fetchMock.lastUrl()).toBe('/my/route'); + }); + + it('should call interceptors in correct order', async () => { + const order: string[] = []; + + fetchInstance.intercept({ + request() { + order.push('Request 1'); + }, + response() { + order.push('Response 1'); + }, + }); + fetchInstance.intercept({ + request() { + order.push('Request 2'); + }, + response() { + order.push('Response 2'); + }, + }); + fetchInstance.intercept({ + request() { + order.push('Request 3'); + }, + response() { + order.push('Response 3'); + }, + }); + + const body = await fetchInstance.fetch('/my/path'); + + expect(fetchMock.called()).toBe(true); + expect(body).toEqual({ foo: 'bar' }); + expect(order).toEqual([ + 'Request 3', + 'Request 2', + 'Request 1', + 'Response 1', + 'Response 2', + 'Response 3', + ]); + }); + + it('should skip remaining interceptors when controller halts during request', async () => { + const usedSpy = jest.fn(); + const unusedSpy = jest.fn(); + + fetchInstance.intercept({ request: unusedSpy, response: unusedSpy }); + fetchInstance.intercept({ + request(request, controller) { + controller.halt(); + }, + response: unusedSpy, + }); + fetchInstance.intercept({ + request: usedSpy, + response: unusedSpy, + }); + + fetchInstance.fetch('/my/path').then(unusedSpy, unusedSpy); + await delay(1000); + + expect(unusedSpy).toHaveBeenCalledTimes(0); + expect(usedSpy).toHaveBeenCalledTimes(1); + expect(fetchMock.called()).toBe(false); + }); + + it('should skip remaining interceptors when controller halts during response', async () => { + const usedSpy = jest.fn(); + const unusedSpy = jest.fn(); + + fetchInstance.intercept({ + request: usedSpy, + response(response, controller) { + controller.halt(); + }, + }); + fetchInstance.intercept({ request: usedSpy, response: unusedSpy }); + fetchInstance.intercept({ request: usedSpy, response: unusedSpy }); + + fetchInstance.fetch('/my/path').then(unusedSpy, unusedSpy); + await delay(1000); + + expect(fetchMock.called()).toBe(true); + expect(usedSpy).toHaveBeenCalledTimes(3); + expect(unusedSpy).toHaveBeenCalledTimes(0); + }); + + it('should skip remaining interceptors when controller halts during responseError', async () => { + fetchMock.post('*', 401); + + const unusedSpy = jest.fn(); + + fetchInstance.intercept({ + responseError(response, controller) { + controller.halt(); + }, + }); + fetchInstance.intercept({ response: unusedSpy, responseError: unusedSpy }); + + fetchInstance.post('/my/path').then(unusedSpy, unusedSpy); + await delay(1000); + + expect(fetchMock.called()).toBe(true); + expect(unusedSpy).toHaveBeenCalledTimes(0); + }); + + it('should not fetch if exception occurs during request interception', async () => { + const usedSpy = jest.fn(); + const unusedSpy = jest.fn(); + + fetchInstance.intercept({ + request: unusedSpy, + requestError: usedSpy, + response: unusedSpy, + responseError: unusedSpy, + }); + fetchInstance.intercept({ + request() { + throw new Error('Interception Error'); + }, + response: unusedSpy, + responseError: unusedSpy, + }); + fetchInstance.intercept({ request: usedSpy, response: unusedSpy, responseError: unusedSpy }); + + await expect(fetchInstance.fetch('/my/path')).rejects.toThrow(/Interception Error/); + expect(fetchMock.called()).toBe(false); + expect(unusedSpy).toHaveBeenCalledTimes(0); + expect(usedSpy).toHaveBeenCalledTimes(2); + }); + + it('should succeed if request throws but caught by interceptor', async () => { + const usedSpy = jest.fn(); + const unusedSpy = jest.fn(); + + fetchInstance.intercept({ + request: unusedSpy, + requestError({ request }) { + return new Request('/my/route', request); + }, + response: usedSpy, + }); + fetchInstance.intercept({ + request() { + throw new Error('Interception Error'); + }, + response: usedSpy, + }); + fetchInstance.intercept({ request: usedSpy, response: usedSpy }); + + await expect(fetchInstance.fetch('/my/route')).resolves.toEqual({ foo: 'bar' }); + expect(fetchMock.called()).toBe(true); + expect(unusedSpy).toHaveBeenCalledTimes(0); + expect(usedSpy).toHaveBeenCalledTimes(4); + }); + + it('should accumulate request information', async () => { + const routes = ['alpha', 'beta', 'gamma']; + const createRequest = jest.fn( + (request: Request) => new Request(`/api/${routes.shift()}`, request) + ); + + fetchInstance.intercept({ + request: createRequest, + }); + fetchInstance.intercept({ + requestError(httpErrorRequest) { + return httpErrorRequest.request; + }, + }); + fetchInstance.intercept({ + request(request) { + throw new Error('Invalid'); + }, + }); + fetchInstance.intercept({ + request: createRequest, + }); + fetchInstance.intercept({ + request: createRequest, + }); + + await expect(fetchInstance.fetch('/my/route')).resolves.toEqual({ foo: 'bar' }); + expect(fetchMock.called()).toBe(true); + expect(routes.length).toBe(0); + expect(createRequest.mock.calls[0][0].url).toContain('/my/route'); + expect(createRequest.mock.calls[1][0].url).toContain('/api/alpha'); + expect(createRequest.mock.calls[2][0].url).toContain('/api/beta'); + expect(fetchMock.lastCall()!.request.url).toContain('/api/gamma'); + }); + + it('should accumulate response information', async () => { + const bodies = ['alpha', 'beta', 'gamma']; + const createResponse = jest.fn((httpResponse: IHttpResponse) => ({ + body: bodies.shift(), + })); + + fetchInstance.intercept({ + response: createResponse, + }); + fetchInstance.intercept({ + response: createResponse, + }); + fetchInstance.intercept({ + response(httpResponse) { + throw new Error('Invalid'); + }, + }); + fetchInstance.intercept({ + responseError({ error, ...httpResponse }) { + return httpResponse; + }, + }); + fetchInstance.intercept({ + response: createResponse, + }); + + await expect(fetchInstance.fetch('/my/route')).resolves.toEqual('gamma'); + expect(fetchMock.called()).toBe(true); + expect(bodies.length).toBe(0); + expect(createResponse.mock.calls[0][0].body).toEqual({ foo: 'bar' }); + expect(createResponse.mock.calls[1][0].body).toBe('alpha'); + expect(createResponse.mock.calls[2][0].body).toBe('beta'); + }); + + describe('request availability during interception', () => { + it('should be available to responseError when response throws', async () => { + let spiedRequest: Request | undefined; + + fetchInstance.intercept({ + response() { + throw new Error('Internal Server Error'); + }, + }); + fetchInstance.intercept({ + responseError({ request }) { + spiedRequest = request; + }, + }); + + await expect(fetchInstance.fetch('/my/path')).rejects.toThrow(); + expect(fetchMock.called()).toBe(true); + expect(spiedRequest).toBeDefined(); + }); + }); + + describe('response availability during interception', () => { + it('should be available to responseError when network request fails', async () => { + fetchMock.restore(); + fetchMock.get('*', { status: 500 }); + + let spiedResponse: Response | undefined; + + fetchInstance.intercept({ + responseError({ response }) { + spiedResponse = response; + }, + }); + + await expect(fetchInstance.fetch('/my/path')).rejects.toThrow(); + expect(spiedResponse).toBeDefined(); + }); + }); + + it('should actually halt request interceptors in reverse order', async () => { + const unusedSpy = jest.fn(); + + fetchInstance.intercept({ request: unusedSpy }); + fetchInstance.intercept({ + request(request, controller) { + controller.halt(); + }, + }); + + fetchInstance.fetch('/my/path'); + await delay(500); + + expect(unusedSpy).toHaveBeenCalledTimes(0); + }); + + it('should recover from failing request interception via request error interceptor', async () => { + const usedSpy = jest.fn(); + + fetchInstance.intercept({ + requestError(httpErrorRequest) { + return httpErrorRequest.request; + }, + response: usedSpy, + }); + + fetchInstance.intercept({ + request(request, controller) { + throw new Error('Request Error'); + }, + response: usedSpy, + }); + + await expect(fetchInstance.fetch('/my/path')).resolves.toEqual({ foo: 'bar' }); + expect(usedSpy).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/src/core/public/http/fetch.ts b/src/core/public/http/fetch.ts index 472b617cacd7f1..b86f1f5c08029b 100644 --- a/src/core/public/http/fetch.ts +++ b/src/core/public/http/fetch.ts @@ -35,20 +35,30 @@ interface Params { const JSON_CONTENT = /^(application\/(json|x-javascript)|text\/(x-)?javascript|x-json)(;.*)?$/; const NDJSON_CONTENT = /^(application\/ndjson)(;.*)?$/; -export class FetchService { +export class Fetch { private readonly interceptors = new Set(); constructor(private readonly params: Params) {} public intercept(interceptor: HttpInterceptor) { this.interceptors.add(interceptor); - return () => this.interceptors.delete(interceptor); + return () => { + this.interceptors.delete(interceptor); + }; } public removeAllInterceptors() { this.interceptors.clear(); } + public readonly delete = this.shorthand('DELETE'); + public readonly get = this.shorthand('GET'); + public readonly head = this.shorthand('HEAD'); + public readonly options = this.shorthand('options'); + public readonly patch = this.shorthand('PATCH'); + public readonly post = this.shorthand('POST'); + public readonly put = this.shorthand('PUT'); + public fetch: HttpHandler = async ( path: string, options: HttpFetchOptions = {} @@ -152,4 +162,9 @@ export class FetchService { return new HttpResponse({ request, response, body }); } + + private shorthand(method: string) { + return (path: string, options: HttpFetchOptions = {}) => + this.fetch(path, { ...options, method }); + } } diff --git a/src/core/public/http/http_service.mock.ts b/src/core/public/http/http_service.mock.ts index 5887e7b3e96d00..1111fd39ec78e6 100644 --- a/src/core/public/http/http_service.mock.ts +++ b/src/core/public/http/http_service.mock.ts @@ -20,7 +20,7 @@ import { HttpService } from './http_service'; import { HttpSetup } from './types'; import { BehaviorSubject } from 'rxjs'; -import { BasePath } from './base_path_service'; +import { BasePath } from './base_path'; export type HttpSetupMock = jest.Mocked & { basePath: BasePath; @@ -41,15 +41,13 @@ const createServiceMock = ({ basePath = '' } = {}): HttpSetupMock => ({ register: jest.fn(), isAnonymous: jest.fn(), }, - addLoadingCount: jest.fn(), + addLoadingCountSource: jest.fn(), getLoadingCount$: jest.fn().mockReturnValue(new BehaviorSubject(0)), - stop: jest.fn(), intercept: jest.fn(), - removeAllInterceptors: jest.fn(), }); const createMock = ({ basePath = '' } = {}) => { - const mocked: jest.Mocked> = { + const mocked: jest.Mocked> = { setup: jest.fn(), start: jest.fn(), stop: jest.fn(), diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/get_vis_data.js b/src/core/public/http/http_service.test.mocks.ts similarity index 71% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/get_vis_data.js rename to src/core/public/http/http_service.test.mocks.ts index 055d4477825c7d..e60dad0509699e 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/get_vis_data.js +++ b/src/core/public/http/http_service.test.mocks.ts @@ -17,14 +17,9 @@ * under the License. */ -import _ from 'lodash'; -import { getPanelData } from './vis_data/get_panel_data'; +import { loadingCountServiceMock } from './loading_count_service.mock'; -export function getVisData(req) { - const promises = req.payload.panels.map(getPanelData(req)); - return Promise.all(promises).then(res => { - return res.reduce((acc, data) => { - return _.assign(acc, data); - }, {}); - }); -} +export const loadingServiceMock = loadingCountServiceMock.create(); +jest.doMock('./loading_count_service', () => ({ + LoadingCountService: jest.fn(() => loadingServiceMock), +})); diff --git a/src/core/public/http/http_service.test.ts b/src/core/public/http/http_service.test.ts index 09f3cca419e4d8..f95d25d116976f 100644 --- a/src/core/public/http/http_service.test.ts +++ b/src/core/public/http/http_service.test.ts @@ -17,692 +17,22 @@ * under the License. */ -import * as Rx from 'rxjs'; -import { toArray } from 'rxjs/operators'; // @ts-ignore import fetchMock from 'fetch-mock/es5/client'; -import { readFileSync } from 'fs'; -import { join } from 'path'; -import { setup, SetupTap } from '../../../test_utils/public/http_test_setup'; -import { IHttpResponse } from './types'; - -function delay(duration: number) { - return new Promise(r => setTimeout(r, duration)); -} - -const setupFakeBasePath: SetupTap = injectedMetadata => { - injectedMetadata.getBasePath.mockReturnValue('/foo/bar'); -}; - -describe('basePath.get()', () => { - it('returns an empty string if no basePath is injected', () => { - const { http } = setup(injectedMetadata => { - injectedMetadata.getBasePath.mockReturnValue(undefined as any); - }); - - expect(http.basePath.get()).toBe(''); - }); - - it('returns the injected basePath', () => { - const { http } = setup(setupFakeBasePath); - - expect(http.basePath.get()).toBe('/foo/bar'); - }); -}); - -describe('http requests', () => { - afterEach(() => { - fetchMock.restore(); - }); - - it('should use supplied request method', async () => { - const { http } = setup(); - - fetchMock.post('*', {}); - await http.fetch('/my/path', { method: 'POST' }); - - expect(fetchMock.lastOptions()!.method).toBe('POST'); - }); - - it('should use supplied Content-Type', async () => { - const { http } = setup(); - - fetchMock.get('*', {}); - await http.fetch('/my/path', { headers: { 'Content-Type': 'CustomContentType' } }); - - expect(fetchMock.lastOptions()!.headers).toMatchObject({ - 'content-type': 'CustomContentType', - }); - }); - - it('should use supplied pathname and querystring', async () => { - const { http } = setup(); - - fetchMock.get('*', {}); - await http.fetch('/my/path', { query: { a: 'b' } }); - - expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path?a=b'); - }); - - it('should use supplied headers', async () => { - const { http } = setup(); - - fetchMock.get('*', {}); - await http.fetch('/my/path', { - headers: { myHeader: 'foo' }, - }); - - expect(fetchMock.lastOptions()!.headers).toEqual({ - 'content-type': 'application/json', - 'kbn-version': 'kibanaVersion', - myheader: 'foo', - }); - }); - - it('should return response', async () => { - const { http } = setup(); - fetchMock.get('*', { foo: 'bar' }); - const json = await http.fetch('/my/path'); - expect(json).toEqual({ foo: 'bar' }); - }); - - it('should prepend url with basepath by default', async () => { - const { http } = setup(); - fetchMock.get('*', {}); - await http.fetch('/my/path'); - expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path'); - }); - - it('should not prepend url with basepath when disabled', async () => { - const { http } = setup(); - fetchMock.get('*', {}); - await http.fetch('my/path', { prependBasePath: false }); - expect(fetchMock.lastUrl()).toBe('/my/path'); - }); - - it('should not include undefined query params', async () => { - const { http } = setup(); - fetchMock.get('*', {}); - await http.fetch('/my/path', { query: { a: undefined } }); - expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path'); - }); - - it('should make request with defaults', async () => { - const { http } = setup(); - - fetchMock.get('*', {}); - await http.fetch('/my/path'); - - const lastCall = fetchMock.lastCall(); - - expect(lastCall!.request.credentials).toBe('same-origin'); - expect(lastCall![1]).toMatchObject({ - method: 'GET', - headers: { - 'content-type': 'application/json', - 'kbn-version': 'kibanaVersion', - }, - }); - }); - - it('should expose detailed response object when asResponse = true', async () => { - const { http } = setup(); - - fetchMock.get('*', { foo: 'bar' }); - - const response = await http.fetch('/my/path', { asResponse: true }); - - expect(response.request).toBeInstanceOf(Request); - expect(response.response).toBeInstanceOf(Response); - expect(response.body).toEqual({ foo: 'bar' }); - }); - - it('should reject on network error', async () => { - const { http } = setup(); - - expect.assertions(1); - fetchMock.get('*', { status: 500 }); - - await expect(http.fetch('/my/path')).rejects.toThrow(/Internal Server Error/); - }); - - it('should contain error message when throwing response', async () => { - const { http } = setup(); - - fetchMock.get('*', { status: 404, body: { foo: 'bar' } }); - - await expect(http.fetch('/my/path')).rejects.toMatchObject({ - message: 'Not Found', - body: { - foo: 'bar', - }, - response: { - status: 404, - url: 'http://localhost/myBase/my/path', - }, - }); - }); - - it('should support get() helper', async () => { - const { http } = setup(); - - fetchMock.get('*', {}); - await http.get('/my/path', { method: 'POST' }); - - expect(fetchMock.lastOptions()!.method).toBe('GET'); - }); - - it('should support head() helper', async () => { - const { http } = setup(); - - fetchMock.head('*', {}); - await http.head('/my/path', { method: 'GET' }); - - expect(fetchMock.lastOptions()!.method).toBe('HEAD'); - }); - - it('should support post() helper', async () => { - const { http } = setup(); - - fetchMock.post('*', {}); - await http.post('/my/path', { method: 'GET', body: '{}' }); - - expect(fetchMock.lastOptions()!.method).toBe('POST'); - }); - - it('should support put() helper', async () => { - const { http } = setup(); - - fetchMock.put('*', {}); - await http.put('/my/path', { method: 'GET', body: '{}' }); - - expect(fetchMock.lastOptions()!.method).toBe('PUT'); - }); - - it('should support patch() helper', async () => { - const { http } = setup(); - - fetchMock.patch('*', {}); - await http.patch('/my/path', { method: 'GET', body: '{}' }); - - expect(fetchMock.lastOptions()!.method).toBe('PATCH'); - }); - - it('should support delete() helper', async () => { - const { http } = setup(); - - fetchMock.delete('*', {}); - await http.delete('/my/path', { method: 'GET' }); - - expect(fetchMock.lastOptions()!.method).toBe('DELETE'); - }); - - it('should support options() helper', async () => { - const { http } = setup(); - - fetchMock.mock('*', { method: 'OPTIONS' }); - await http.options('/my/path', { method: 'GET' }); - - expect(fetchMock.lastOptions()!.method).toBe('OPTIONS'); - }); - - it('should make requests for NDJSON content', async () => { - const { http } = setup(); - const content = readFileSync(join(__dirname, '_import_objects.ndjson'), { encoding: 'utf-8' }); - const body = new FormData(); - - body.append('file', content); - fetchMock.post('*', { - body: content, - headers: { 'Content-Type': 'application/ndjson' }, - }); - - const data = await http.post('/my/path', { - body, - headers: { - 'Content-Type': undefined, - }, - }); - - expect(data).toBeInstanceOf(Blob); - - const ndjson = await new Response(data).text(); - - expect(ndjson).toEqual(content); - }); -}); - -describe('interception', () => { - const { http } = setup(); - - beforeEach(() => { - fetchMock.get('*', { foo: 'bar' }); - }); - - afterEach(() => { - fetchMock.restore(); - http.removeAllInterceptors(); - }); - - it('should make request and receive response', async () => { - http.intercept({}); - - const body = await http.fetch('/my/path'); - - expect(fetchMock.called()).toBe(true); - expect(body).toEqual({ foo: 'bar' }); - }); - - it('should be able to manipulate request instance', async () => { - http.intercept({ - request(request) { - request.headers.set('Content-Type', 'CustomContentType'); - }, - }); - http.intercept({ - request(request) { - return new Request('/my/route', request); - }, - }); - - const body = await http.fetch('/my/path'); - - expect(fetchMock.called()).toBe(true); - expect(body).toEqual({ foo: 'bar' }); - expect(fetchMock.lastOptions()!.headers).toMatchObject({ - 'content-type': 'CustomContentType', - }); - expect(fetchMock.lastUrl()).toBe('/my/route'); - }); - - it('should call interceptors in correct order', async () => { - const order: string[] = []; - - http.intercept({ - request() { - order.push('Request 1'); - }, - response() { - order.push('Response 1'); - }, - }); - http.intercept({ - request() { - order.push('Request 2'); - }, - response() { - order.push('Response 2'); - }, - }); - http.intercept({ - request() { - order.push('Request 3'); - }, - response() { - order.push('Response 3'); - }, - }); - - const body = await http.fetch('/my/path'); - - expect(fetchMock.called()).toBe(true); - expect(body).toEqual({ foo: 'bar' }); - expect(order).toEqual([ - 'Request 3', - 'Request 2', - 'Request 1', - 'Response 1', - 'Response 2', - 'Response 3', - ]); - }); - - it('should skip remaining interceptors when controller halts during request', async () => { - const usedSpy = jest.fn(); - const unusedSpy = jest.fn(); - - http.intercept({ request: unusedSpy, response: unusedSpy }); - http.intercept({ - request(request, controller) { - controller.halt(); - }, - response: unusedSpy, - }); - http.intercept({ - request: usedSpy, - response: unusedSpy, - }); - - http.fetch('/my/path').then(unusedSpy, unusedSpy); - await delay(1000); - - expect(unusedSpy).toHaveBeenCalledTimes(0); - expect(usedSpy).toHaveBeenCalledTimes(1); - expect(fetchMock.called()).toBe(false); - }); - - it('should skip remaining interceptors when controller halts during response', async () => { - const usedSpy = jest.fn(); - const unusedSpy = jest.fn(); - - http.intercept({ - request: usedSpy, - response(response, controller) { - controller.halt(); - }, - }); - http.intercept({ request: usedSpy, response: unusedSpy }); - http.intercept({ request: usedSpy, response: unusedSpy }); - - http.fetch('/my/path').then(unusedSpy, unusedSpy); - await delay(1000); - - expect(fetchMock.called()).toBe(true); - expect(usedSpy).toHaveBeenCalledTimes(3); - expect(unusedSpy).toHaveBeenCalledTimes(0); - }); - - it('should skip remaining interceptors when controller halts during responseError', async () => { - fetchMock.post('*', 401); - - const unusedSpy = jest.fn(); - - http.intercept({ - responseError(response, controller) { - controller.halt(); - }, - }); - http.intercept({ response: unusedSpy, responseError: unusedSpy }); - - http.post('/my/path').then(unusedSpy, unusedSpy); - await delay(1000); - - expect(fetchMock.called()).toBe(true); - expect(unusedSpy).toHaveBeenCalledTimes(0); - }); - - it('should not fetch if exception occurs during request interception', async () => { - const usedSpy = jest.fn(); - const unusedSpy = jest.fn(); - - http.intercept({ - request: unusedSpy, - requestError: usedSpy, - response: unusedSpy, - responseError: unusedSpy, - }); - http.intercept({ - request() { - throw new Error('Interception Error'); - }, - response: unusedSpy, - responseError: unusedSpy, - }); - http.intercept({ request: usedSpy, response: unusedSpy, responseError: unusedSpy }); - - await expect(http.fetch('/my/path')).rejects.toThrow(/Interception Error/); - expect(fetchMock.called()).toBe(false); - expect(unusedSpy).toHaveBeenCalledTimes(0); - expect(usedSpy).toHaveBeenCalledTimes(2); - }); - - it('should succeed if request throws but caught by interceptor', async () => { - const usedSpy = jest.fn(); - const unusedSpy = jest.fn(); - - http.intercept({ - request: unusedSpy, - requestError({ request }) { - return new Request('/my/route', request); - }, - response: usedSpy, - }); - http.intercept({ - request() { - throw new Error('Interception Error'); - }, - response: usedSpy, - }); - http.intercept({ request: usedSpy, response: usedSpy }); - - await expect(http.fetch('/my/route')).resolves.toEqual({ foo: 'bar' }); - expect(fetchMock.called()).toBe(true); - expect(unusedSpy).toHaveBeenCalledTimes(0); - expect(usedSpy).toHaveBeenCalledTimes(4); - }); - - it('should accumulate request information', async () => { - const routes = ['alpha', 'beta', 'gamma']; - const createRequest = jest.fn( - (request: Request) => new Request(`/api/${routes.shift()}`, request) - ); - - http.intercept({ - request: createRequest, - }); - http.intercept({ - requestError(httpErrorRequest) { - return httpErrorRequest.request; - }, - }); - http.intercept({ - request(request) { - throw new Error('Invalid'); - }, - }); - http.intercept({ - request: createRequest, - }); - http.intercept({ - request: createRequest, - }); - - await expect(http.fetch('/my/route')).resolves.toEqual({ foo: 'bar' }); - expect(fetchMock.called()).toBe(true); - expect(routes.length).toBe(0); - expect(createRequest.mock.calls[0][0].url).toContain('/my/route'); - expect(createRequest.mock.calls[1][0].url).toContain('/api/alpha'); - expect(createRequest.mock.calls[2][0].url).toContain('/api/beta'); - expect(fetchMock.lastCall()!.request.url).toContain('/api/gamma'); - }); - - it('should accumulate response information', async () => { - const bodies = ['alpha', 'beta', 'gamma']; - const createResponse = jest.fn((httpResponse: IHttpResponse) => ({ - body: bodies.shift(), - })); - - http.intercept({ - response: createResponse, - }); - http.intercept({ - response: createResponse, - }); - http.intercept({ - response(httpResponse) { - throw new Error('Invalid'); - }, - }); - http.intercept({ - responseError({ error, ...httpResponse }) { - return httpResponse; - }, - }); - http.intercept({ - response: createResponse, - }); - - await expect(http.fetch('/my/route')).resolves.toEqual('gamma'); - expect(fetchMock.called()).toBe(true); - expect(bodies.length).toBe(0); - expect(createResponse.mock.calls[0][0].body).toEqual({ foo: 'bar' }); - expect(createResponse.mock.calls[1][0].body).toBe('alpha'); - expect(createResponse.mock.calls[2][0].body).toBe('beta'); - }); - - describe('request availability during interception', () => { - it('should be available to responseError when response throws', async () => { - let spiedRequest: Request | undefined; - - http.intercept({ - response() { - throw new Error('Internal Server Error'); - }, - }); - http.intercept({ - responseError({ request }) { - spiedRequest = request; - }, - }); - - await expect(http.fetch('/my/path')).rejects.toThrow(); - expect(fetchMock.called()).toBe(true); - expect(spiedRequest).toBeDefined(); - }); - }); - - describe('response availability during interception', () => { - it('should be available to responseError when network request fails', async () => { - fetchMock.restore(); - fetchMock.get('*', { status: 500 }); - - let spiedResponse: Response | undefined; - - http.intercept({ - responseError({ response }) { - spiedResponse = response; - }, - }); - - await expect(http.fetch('/my/path')).rejects.toThrow(); - expect(spiedResponse).toBeDefined(); - }); - }); - - it('should actually halt request interceptors in reverse order', async () => { - const unusedSpy = jest.fn(); - - http.intercept({ request: unusedSpy }); - http.intercept({ - request(request, controller) { - controller.halt(); - }, - }); - - http.fetch('/my/path'); - await delay(500); - - expect(unusedSpy).toHaveBeenCalledTimes(0); - }); - - it('should recover from failing request interception via request error interceptor', async () => { - const usedSpy = jest.fn(); - - http.intercept({ - requestError(httpErrorRequest) { - return httpErrorRequest.request; - }, - response: usedSpy, - }); - - http.intercept({ - request(request, controller) { - throw new Error('Request Error'); - }, - response: usedSpy, - }); - - await expect(http.fetch('/my/path')).resolves.toEqual({ foo: 'bar' }); - expect(usedSpy).toHaveBeenCalledTimes(2); - }); -}); - -describe('addLoadingCount()', () => { - it('subscribes to passed in sources, unsubscribes on stop', () => { - const { httpService, http } = setup(); - - const unsubA = jest.fn(); - const subA = jest.fn().mockReturnValue(unsubA); - http.addLoadingCount(new Rx.Observable(subA)); - expect(subA).toHaveBeenCalledTimes(1); - expect(unsubA).not.toHaveBeenCalled(); - - const unsubB = jest.fn(); - const subB = jest.fn().mockReturnValue(unsubB); - http.addLoadingCount(new Rx.Observable(subB)); - expect(subB).toHaveBeenCalledTimes(1); - expect(unsubB).not.toHaveBeenCalled(); - +import { loadingServiceMock } from './http_service.test.mocks'; + +import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock'; +import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; +import { HttpService } from './http_service'; + +describe('#stop()', () => { + it('calls loadingCount.stop()', () => { + const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); + const fatalErrors = fatalErrorsServiceMock.createSetupContract(); + const httpService = new HttpService(); + httpService.setup({ fatalErrors, injectedMetadata }); + httpService.start({ fatalErrors, injectedMetadata }); httpService.stop(); - - expect(subA).toHaveBeenCalledTimes(1); - expect(unsubA).toHaveBeenCalledTimes(1); - expect(subB).toHaveBeenCalledTimes(1); - expect(unsubB).toHaveBeenCalledTimes(1); - }); - - it('adds a fatal error if source observables emit an error', async () => { - const { http, fatalErrors } = setup(); - - http.addLoadingCount(Rx.throwError(new Error('foo bar'))); - expect(fatalErrors.add.mock.calls).toMatchSnapshot(); - }); - - it('adds a fatal error if source observable emits a negative number', async () => { - const { http, fatalErrors } = setup(); - - http.addLoadingCount(Rx.of(1, 2, 3, 4, -9)); - expect(fatalErrors.add.mock.calls).toMatchSnapshot(); - }); -}); - -describe('getLoadingCount$()', () => { - it('emits 0 initially, the right count when sources emit their own count, and ends with zero', async () => { - const { httpService, http } = setup(); - - const countA$ = new Rx.Subject(); - const countB$ = new Rx.Subject(); - const countC$ = new Rx.Subject(); - const promise = http - .getLoadingCount$() - .pipe(toArray()) - .toPromise(); - - http.addLoadingCount(countA$); - http.addLoadingCount(countB$); - http.addLoadingCount(countC$); - - countA$.next(100); - countB$.next(10); - countC$.next(1); - countA$.complete(); - countB$.next(20); - countC$.complete(); - countB$.next(0); - - httpService.stop(); - expect(await promise).toMatchSnapshot(); - }); - - it('only emits when loading count changes', async () => { - const { httpService, http } = setup(); - - const count$ = new Rx.Subject(); - const promise = http - .getLoadingCount$() - .pipe(toArray()) - .toPromise(); - - http.addLoadingCount(count$); - count$.next(0); - count$.next(0); - count$.next(0); - count$.next(0); - count$.next(0); - count$.next(1); - count$.next(1); - httpService.stop(); - - expect(await promise).toMatchSnapshot(); + expect(loadingServiceMock.stop).toHaveBeenCalled(); }); }); diff --git a/src/core/public/http/http_service.ts b/src/core/public/http/http_service.ts index 477bcd6152d44c..567cdd310cbdfc 100644 --- a/src/core/public/http/http_service.ts +++ b/src/core/public/http/http_service.ts @@ -17,32 +17,52 @@ * under the License. */ -import { HttpSetup, HttpStart, HttpServiceBase } from './types'; -import { setup } from './http_setup'; +import { HttpSetup, HttpStart } from './types'; import { InjectedMetadataSetup } from '../injected_metadata'; import { FatalErrorsSetup } from '../fatal_errors'; +import { BasePath } from './base_path'; +import { AnonymousPathsService } from './anonymous_paths_service'; +import { LoadingCountService } from './loading_count_service'; +import { Fetch } from './fetch'; +import { CoreService } from '../../types'; interface HttpDeps { injectedMetadata: InjectedMetadataSetup; - fatalErrors: FatalErrorsSetup | null; + fatalErrors: FatalErrorsSetup; } /** @internal */ -export class HttpService { - private service!: HttpServiceBase; +export class HttpService implements CoreService { + private readonly anonymousPaths = new AnonymousPathsService(); + private readonly loadingCount = new LoadingCountService(); - public setup(deps: HttpDeps): HttpSetup { - this.service = setup(deps.injectedMetadata, deps.fatalErrors); - return this.service; + public setup({ injectedMetadata, fatalErrors }: HttpDeps): HttpSetup { + const kibanaVersion = injectedMetadata.getKibanaVersion(); + const basePath = new BasePath(injectedMetadata.getBasePath()); + const fetchService = new Fetch({ basePath, kibanaVersion }); + const loadingCount = this.loadingCount.setup({ fatalErrors }); + + return { + basePath, + anonymousPaths: this.anonymousPaths.setup({ basePath }), + intercept: fetchService.intercept.bind(fetchService), + fetch: fetchService.fetch.bind(fetchService), + delete: fetchService.delete.bind(fetchService), + get: fetchService.get.bind(fetchService), + head: fetchService.head.bind(fetchService), + options: fetchService.options.bind(fetchService), + patch: fetchService.patch.bind(fetchService), + post: fetchService.post.bind(fetchService), + put: fetchService.put.bind(fetchService), + ...loadingCount, + }; } - public start(deps: HttpDeps): HttpStart { - return this.service || this.setup(deps); + public start(deps: HttpDeps) { + return this.setup(deps); } public stop() { - if (this.service) { - this.service.stop(); - } + this.loadingCount.stop(); } } diff --git a/src/core/public/http/http_setup.ts b/src/core/public/http/http_setup.ts deleted file mode 100644 index c63750849f13af..00000000000000 --- a/src/core/public/http/http_setup.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { BehaviorSubject, Observable, Subject } from 'rxjs'; -import { - distinctUntilChanged, - endWith, - map, - pairwise, - startWith, - takeUntil, - tap, -} from 'rxjs/operators'; -import { InjectedMetadataSetup } from '../injected_metadata'; -import { FatalErrorsSetup } from '../fatal_errors'; -import { HttpFetchOptions, HttpServiceBase } from './types'; -import { HttpInterceptController } from './http_intercept_controller'; -import { HttpInterceptHaltError } from './http_intercept_halt_error'; -import { BasePath } from './base_path_service'; -import { AnonymousPaths } from './anonymous_paths'; -import { FetchService } from './fetch'; - -export function checkHalt(controller: HttpInterceptController, error?: Error) { - if (error instanceof HttpInterceptHaltError) { - throw error; - } else if (controller.halted) { - throw new HttpInterceptHaltError(); - } -} - -export const setup = ( - injectedMetadata: InjectedMetadataSetup, - fatalErrors: FatalErrorsSetup | null -): HttpServiceBase => { - const loadingCount$ = new BehaviorSubject(0); - const stop$ = new Subject(); - const kibanaVersion = injectedMetadata.getKibanaVersion(); - const basePath = new BasePath(injectedMetadata.getBasePath()); - const anonymousPaths = new AnonymousPaths(basePath); - - const fetchService = new FetchService({ basePath, kibanaVersion }); - - function shorthand(method: string) { - return (path: string, options: HttpFetchOptions = {}) => - fetchService.fetch(path, { ...options, method }); - } - - function stop() { - stop$.next(); - loadingCount$.complete(); - } - - function addLoadingCount(count$: Observable) { - count$ - .pipe( - distinctUntilChanged(), - - tap(count => { - if (count < 0) { - throw new Error( - 'Observables passed to loadingCount.add() must only emit positive numbers' - ); - } - }), - - // use takeUntil() so that we can finish each stream on stop() the same way we do when they complete, - // by removing the previous count from the total - takeUntil(stop$), - endWith(0), - startWith(0), - pairwise(), - map(([prev, next]) => next - prev) - ) - .subscribe({ - next: delta => { - loadingCount$.next(loadingCount$.getValue() + delta); - }, - error: error => { - if (fatalErrors) { - fatalErrors.add(error); - } - }, - }); - } - - function getLoadingCount$() { - return loadingCount$.pipe(distinctUntilChanged()); - } - - return { - stop, - basePath, - anonymousPaths, - intercept: fetchService.intercept.bind(fetchService), - removeAllInterceptors: fetchService.removeAllInterceptors.bind(fetchService), - fetch: fetchService.fetch.bind(fetchService), - delete: shorthand('DELETE'), - get: shorthand('GET'), - head: shorthand('HEAD'), - options: shorthand('OPTIONS'), - patch: shorthand('PATCH'), - post: shorthand('POST'), - put: shorthand('PUT'), - addLoadingCount, - getLoadingCount$, - }; -}; diff --git a/src/core/public/http/loading_count_service.mock.ts b/src/core/public/http/loading_count_service.mock.ts new file mode 100644 index 00000000000000..79928aa4b160de --- /dev/null +++ b/src/core/public/http/loading_count_service.mock.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { LoadingCountSetup, LoadingCountService } from './loading_count_service'; +import { BehaviorSubject } from 'rxjs'; + +const createSetupContractMock = () => { + const setupContract: jest.Mocked = { + addLoadingCountSource: jest.fn(), + getLoadingCount$: jest.fn(), + }; + setupContract.getLoadingCount$.mockReturnValue(new BehaviorSubject(0)); + return setupContract; +}; + +type LoadingCountServiceContract = PublicMethodsOf; +const createServiceMock = () => { + const mocked: jest.Mocked = { + setup: jest.fn(), + start: jest.fn(), + stop: jest.fn(), + }; + + mocked.setup.mockReturnValue(createSetupContractMock()); + mocked.start.mockReturnValue(createSetupContractMock()); + + return mocked; +}; + +export const loadingCountServiceMock = { + create: createServiceMock, + createSetupContract: createSetupContractMock, + createStartContract: createSetupContractMock, +}; diff --git a/src/core/public/http/loading_count_service.test.ts b/src/core/public/http/loading_count_service.test.ts new file mode 100644 index 00000000000000..3ba4d315178cc5 --- /dev/null +++ b/src/core/public/http/loading_count_service.test.ts @@ -0,0 +1,152 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable, throwError, of, Subject } from 'rxjs'; +import { toArray } from 'rxjs/operators'; + +import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock'; +import { LoadingCountService } from './loading_count_service'; + +describe('LoadingCountService', () => { + const setup = () => { + const fatalErrors = fatalErrorsServiceMock.createSetupContract(); + const service = new LoadingCountService(); + const loadingCount = service.setup({ fatalErrors }); + return { fatalErrors, loadingCount, service }; + }; + + describe('addLoadingCountSource()', () => { + it('subscribes to passed in sources, unsubscribes on stop', () => { + const { service, loadingCount } = setup(); + + const unsubA = jest.fn(); + const subA = jest.fn().mockReturnValue(unsubA); + loadingCount.addLoadingCountSource(new Observable(subA)); + expect(subA).toHaveBeenCalledTimes(1); + expect(unsubA).not.toHaveBeenCalled(); + + const unsubB = jest.fn(); + const subB = jest.fn().mockReturnValue(unsubB); + loadingCount.addLoadingCountSource(new Observable(subB)); + expect(subB).toHaveBeenCalledTimes(1); + expect(unsubB).not.toHaveBeenCalled(); + + service.stop(); + + expect(subA).toHaveBeenCalledTimes(1); + expect(unsubA).toHaveBeenCalledTimes(1); + expect(subB).toHaveBeenCalledTimes(1); + expect(unsubB).toHaveBeenCalledTimes(1); + }); + + it('adds a fatal error if source observables emit an error', () => { + const { loadingCount, fatalErrors } = setup(); + + loadingCount.addLoadingCountSource(throwError(new Error('foo bar'))); + expect(fatalErrors.add.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: foo bar], + ], + ] + `); + }); + + it('adds a fatal error if source observable emits a negative number', () => { + const { loadingCount, fatalErrors } = setup(); + + loadingCount.addLoadingCountSource(of(1, 2, 3, 4, -9)); + expect(fatalErrors.add.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: Observables passed to loadingCount.add() must only emit positive numbers], + ], + ] + `); + }); + }); + + describe('getLoadingCount$()', () => { + it('emits 0 initially, the right count when sources emit their own count, and ends with zero', async () => { + const { service, loadingCount } = setup(); + + const countA$ = new Subject(); + const countB$ = new Subject(); + const countC$ = new Subject(); + const promise = loadingCount + .getLoadingCount$() + .pipe(toArray()) + .toPromise(); + + loadingCount.addLoadingCountSource(countA$); + loadingCount.addLoadingCountSource(countB$); + loadingCount.addLoadingCountSource(countC$); + + countA$.next(100); + countB$.next(10); + countC$.next(1); + countA$.complete(); + countB$.next(20); + countC$.complete(); + countB$.next(0); + + service.stop(); + expect(await promise).toMatchInlineSnapshot(` + Array [ + 0, + 100, + 110, + 111, + 11, + 21, + 20, + 0, + ] + `); + }); + + it('only emits when loading count changes', async () => { + const { service, loadingCount } = setup(); + + const count$ = new Subject(); + const promise = loadingCount + .getLoadingCount$() + .pipe(toArray()) + .toPromise(); + + loadingCount.addLoadingCountSource(count$); + count$.next(0); + count$.next(0); + count$.next(0); + count$.next(0); + count$.next(0); + count$.next(1); + count$.next(1); + service.stop(); + + expect(await promise).toMatchInlineSnapshot(` + Array [ + 0, + 1, + 0, + ] + `); + }); + }); +}); diff --git a/src/core/public/http/loading_count_service.ts b/src/core/public/http/loading_count_service.ts new file mode 100644 index 00000000000000..14b945e0801ca4 --- /dev/null +++ b/src/core/public/http/loading_count_service.ts @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { BehaviorSubject, Observable, Subject } from 'rxjs'; +import { + distinctUntilChanged, + endWith, + map, + pairwise, + startWith, + takeUntil, + tap, +} from 'rxjs/operators'; +import { FatalErrorsSetup } from '../fatal_errors'; +import { CoreService } from '../../types'; + +/** @public */ +export interface LoadingCountSetup { + addLoadingCountSource(countSource$: Observable): void; + + getLoadingCount$(): Observable; +} + +/** + * See {@link LoadingCountSetup}. + * @public + */ +export type LoadingCountStart = LoadingCountSetup; + +/** @internal */ +export class LoadingCountService implements CoreService { + private readonly stop$ = new Subject(); + private readonly loadingCount$ = new BehaviorSubject(0); + + public setup({ fatalErrors }: { fatalErrors: FatalErrorsSetup }) { + return { + getLoadingCount$: () => this.loadingCount$.pipe(distinctUntilChanged()), + addLoadingCountSource: (count$: Observable) => { + count$ + .pipe( + distinctUntilChanged(), + + tap(count => { + if (count < 0) { + throw new Error( + 'Observables passed to loadingCount.add() must only emit positive numbers' + ); + } + }), + + // use takeUntil() so that we can finish each stream on stop() the same way we do when they complete, + // by removing the previous count from the total + takeUntil(this.stop$), + endWith(0), + startWith(0), + pairwise(), + map(([prev, next]) => next - prev) + ) + .subscribe({ + next: delta => { + this.loadingCount$.next(this.loadingCount$.getValue() + delta); + }, + error: error => fatalErrors.add(error), + }); + }, + }; + } + + public start({ fatalErrors }: { fatalErrors: FatalErrorsSetup }) { + return this.setup({ fatalErrors }); + } + + public stop() { + this.stop$.next(); + this.loadingCount$.complete(); + } +} diff --git a/src/core/public/http/types.ts b/src/core/public/http/types.ts index 48385a72325db7..27ffddc79cf653 100644 --- a/src/core/public/http/types.ts +++ b/src/core/public/http/types.ts @@ -20,10 +20,7 @@ import { Observable } from 'rxjs'; /** @public */ -export interface HttpServiceBase { - /** @internal */ - stop(): void; - +export interface HttpSetup { /** * APIs for manipulating the basePath on URL segments. */ @@ -41,11 +38,6 @@ export interface HttpServiceBase { */ intercept(interceptor: HttpInterceptor): () => void; - /** - * Removes all configured interceptors. - */ - removeAllInterceptors(): void; - /** Makes an HTTP request. Defaults to a GET request unless overriden. See {@link HttpHandler} for options. */ fetch: HttpHandler; /** Makes an HTTP request with the DELETE method. See {@link HttpHandler} for options. */ @@ -68,7 +60,7 @@ export interface HttpServiceBase { * more than 0. * @param countSource$ an Observable to subscribe to for loading count updates. */ - addLoadingCount(countSource$: Observable): void; + addLoadingCountSource(countSource$: Observable): void; /** * Get the sum of all loading count sources as a single Observable. @@ -76,6 +68,12 @@ export interface HttpServiceBase { getLoadingCount$(): Observable; } +/** + * See {@link HttpSetup} + * @public + */ +export type HttpStart = HttpSetup; + /** * APIs for manipulating the basePath on URL segments. * @public @@ -112,18 +110,6 @@ export interface IAnonymousPaths { register(path: string): void; } -/** - * See {@link HttpServiceBase} - * @public - */ -export type HttpSetup = HttpServiceBase; - -/** - * See {@link HttpServiceBase} - * @public - */ -export type HttpStart = HttpServiceBase; - /** @public */ export interface HttpHeadersInit { [name: string]: any; diff --git a/src/core/public/index.ts b/src/core/public/index.ts index f83ca2564de8ee..7488f9b973b712 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -121,7 +121,6 @@ export { } from './saved_objects'; export { - HttpServiceBase, HttpHeadersInit, HttpRequestInit, HttpFetchOptions, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 83b4e67c1cb158..dfbb6b4a6fbf54 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -544,8 +544,8 @@ export interface HttpRequestInit { } // @public (undocumented) -export interface HttpServiceBase { - addLoadingCount(countSource$: Observable): void; +export interface HttpSetup { + addLoadingCountSource(countSource$: Observable): void; anonymousPaths: IAnonymousPaths; basePath: IBasePath; delete: HttpHandler; @@ -558,16 +558,10 @@ export interface HttpServiceBase { patch: HttpHandler; post: HttpHandler; put: HttpHandler; - removeAllInterceptors(): void; - // @internal (undocumented) - stop(): void; } // @public -export type HttpSetup = HttpServiceBase; - -// @public -export type HttpStart = HttpServiceBase; +export type HttpStart = HttpSetup; // @public export interface I18nStart { @@ -877,7 +871,7 @@ export interface SavedObjectsBulkUpdateOptions { // @public export class SavedObjectsClient { // @internal - constructor(http: HttpServiceBase); + constructor(http: HttpSetup); bulkCreate: (objects?: SavedObjectsBulkCreateObject[], options?: SavedObjectsBulkCreateOptions) => Promise>; bulkGet: (objects?: { id: string; diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index c71fe51956c28f..dab98ee66cdb10 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -36,7 +36,7 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../legacy/ui/public/error_auto_create_index/error_auto_create_index'; import { SimpleSavedObject } from './simple_saved_object'; -import { HttpFetchOptions, HttpServiceBase } from '../http'; +import { HttpFetchOptions, HttpSetup } from '../http'; type SavedObjectsFindOptions = Omit; @@ -158,7 +158,7 @@ export type SavedObjectsClientContract = PublicMethodsOf; * @public */ export class SavedObjectsClient { - private http: HttpServiceBase; + private http: HttpSetup; private batchQueue: BatchQueueEntry[]; /** @@ -194,7 +194,7 @@ export class SavedObjectsClient { ); /** @internal */ - constructor(http: HttpServiceBase) { + constructor(http: HttpSetup) { this.http = http; this.batchQueue = []; } diff --git a/src/core/public/ui_settings/ui_settings_service.test.ts b/src/core/public/ui_settings/ui_settings_service.test.ts index afb68c4844901a..2747a78d93fa65 100644 --- a/src/core/public/ui_settings/ui_settings_service.test.ts +++ b/src/core/public/ui_settings/ui_settings_service.test.ts @@ -38,7 +38,7 @@ describe('#stop', () => { it('stops the uiSettingsClient and uiSettingsApi', async () => { const service = new UiSettingsService(); let loadingCount$: Rx.Observable; - defaultDeps.http.addLoadingCount.mockImplementation(obs$ => (loadingCount$ = obs$)); + defaultDeps.http.addLoadingCountSource.mockImplementation(obs$ => (loadingCount$ = obs$)); const client = service.setup(defaultDeps); service.stop(); diff --git a/src/core/public/ui_settings/ui_settings_service.ts b/src/core/public/ui_settings/ui_settings_service.ts index 5a03cd1cfeedc9..1e01d15fa337b1 100644 --- a/src/core/public/ui_settings/ui_settings_service.ts +++ b/src/core/public/ui_settings/ui_settings_service.ts @@ -38,7 +38,7 @@ export class UiSettingsService { public setup({ http, injectedMetadata }: UiSettingsServiceDeps): IUiSettingsClient { this.uiSettingsApi = new UiSettingsApi(http); - http.addLoadingCount(this.uiSettingsApi.getLoadingCount$()); + http.addLoadingCountSource(this.uiSettingsApi.getLoadingCount$()); // TODO: Migrate away from legacyMetadata https://github.com/elastic/kibana/issues/22779 const legacyMetadata = injectedMetadata.getLegacyMetadata(); diff --git a/src/core/types/core_service.ts b/src/core/types/core_service.ts index b99c419b796ba8..5056396f1f9f0a 100644 --- a/src/core/types/core_service.ts +++ b/src/core/types/core_service.ts @@ -19,7 +19,7 @@ /** @internal */ export interface CoreService { - setup(...params: any[]): Promise; - start(...params: any[]): Promise; - stop(): Promise; + setup(...params: any[]): TSetup | Promise; + start(...params: any[]): TStart | Promise; + stop(): void | Promise; } diff --git a/src/fixtures/stubbed_logstash_index_pattern.js b/src/fixtures/stubbed_logstash_index_pattern.js index 22fbf0ab5a5f89..e20d1b5cd77173 100644 --- a/src/fixtures/stubbed_logstash_index_pattern.js +++ b/src/fixtures/stubbed_logstash_index_pattern.js @@ -21,7 +21,7 @@ import StubIndexPattern from 'test_utils/stub_index_pattern'; import stubbedLogstashFields from 'fixtures/logstash_fields'; import { getKbnFieldType } from '../plugins/data/common'; -import { mockUiSettings } from '../legacy/ui/public/new_platform/new_platform.karma_mock'; +import { npSetup } from '../legacy/ui/public/new_platform/new_platform.karma_mock'; export default function stubbedLogstashIndexPatternService() { const mockLogstashFields = stubbedLogstashFields(); @@ -41,13 +41,8 @@ export default function stubbedLogstashIndexPatternService() { }; }); - const indexPattern = new StubIndexPattern( - 'logstash-*', - cfg => cfg, - 'time', - fields, - mockUiSettings - ); + const indexPattern = new StubIndexPattern('logstash-*', cfg => cfg, 'time', fields, npSetup.core); + indexPattern.id = 'logstash-*'; indexPattern.isTimeNanosBased = () => false; diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/__tests__/lib/rows_headers.js b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/__tests__/lib/rows_headers.js index 677a067e7b3fea..012f2b6061ee42 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/__tests__/lib/rows_headers.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/__tests__/lib/rows_headers.js @@ -50,14 +50,18 @@ describe('Doc Table', function() { // Stub `getConverterFor` for a field in the indexPattern to return mock data. // Returns `val` if provided, otherwise generates fake data for the field. fakeRowVals = getFakeRowVals('formatted', 0, mapping); - stubFieldFormatConverter = function($root, field, val = null) { - $root.indexPattern.fields.getByName(field).format.getConverterFor = () => (...args) => { + stubFieldFormatConverter = function($root, field, val) { + const convertFn = (value, type, options) => { if (val) { return val; } - const fieldName = _.get(args, '[1].name', null); + const fieldName = _.get(options, 'field.name', null); + return fakeRowVals[fieldName] || ''; }; + + $root.indexPattern.fields.getByName(field).format.convert = convertFn; + $root.indexPattern.fields.getByName(field).format.getConverterFor = () => convertFn; }; }) ); diff --git a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/creation/manager.ts b/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/creation/manager.ts index 605ffdd6f21349..e7fa13409ab046 100644 --- a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/creation/manager.ts +++ b/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/creation/manager.ts @@ -17,13 +17,13 @@ * under the License. */ -import { HttpServiceBase } from '../../../../../../../../core/public'; +import { HttpSetup } from '../../../../../../../../core/public'; import { IndexPatternCreationConfig, UrlHandler, IndexPatternCreationOption } from './config'; export class IndexPatternCreationManager { private configs: IndexPatternCreationConfig[]; - constructor(private readonly httpClient: HttpServiceBase) { + constructor(private readonly httpClient: HttpSetup) { this.configs = []; } diff --git a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/index_pattern_management_service.ts b/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/index_pattern_management_service.ts index b9e07564a324cb..b421024b60f4be 100644 --- a/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/index_pattern_management_service.ts +++ b/src/legacy/core_plugins/management/public/np_ready/services/index_pattern_management/index_pattern_management_service.ts @@ -17,12 +17,12 @@ * under the License. */ -import { HttpServiceBase } from '../../../../../../../core/public'; +import { HttpSetup } from '../../../../../../../core/public'; import { IndexPatternCreationManager, IndexPatternCreationConfig } from './creation'; import { IndexPatternListManager, IndexPatternListConfig } from './list'; interface SetupDependencies { - httpClient: HttpServiceBase; + httpClient: HttpSetup; } /** diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_controller.test.ts b/src/legacy/core_plugins/vis_type_table/public/table_vis_controller.test.ts index a2f98b8c64e538..1f6600ea56a128 100644 --- a/src/legacy/core_plugins/vis_type_table/public/table_vis_controller.test.ts +++ b/src/legacy/core_plugins/vis_type_table/public/table_vis_controller.test.ts @@ -110,7 +110,7 @@ describe('Table Vis - Controller', () => { (cfg: any) => cfg, 'time', stubFields, - npStart.core.uiSettings + npStart.core ); }); diff --git a/src/legacy/core_plugins/vis_type_timeseries/index.ts b/src/legacy/core_plugins/vis_type_timeseries/index.ts index eb20c0f23965a8..9ca14b28e19b2a 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/index.ts +++ b/src/legacy/core_plugins/vis_type_timeseries/index.ts @@ -19,13 +19,9 @@ import { resolve } from 'path'; import { Legacy } from 'kibana'; -import { PluginInitializerContext } from 'src/core/server'; -import { CoreSetup } from 'src/core/server'; - -import { plugin } from './server/'; -import { CustomCoreSetup } from './server/plugin'; import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types'; +import { VisTypeTimeseriesSetup } from '../../../plugins/vis_type_timeseries/server'; const metricsPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => new Plugin({ @@ -38,10 +34,9 @@ const metricsPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPlu injectDefaultVars: server => ({}), }, init: (server: Legacy.Server) => { - const initializerContext = {} as PluginInitializerContext; - const core = { http: { server } } as CoreSetup & CustomCoreSetup; - - plugin(initializerContext).setup(core); + const visTypeTimeSeriesPlugin = server.newPlatform.setup.plugins + .metrics as VisTypeTimeseriesSetup; + visTypeTimeSeriesPlugin.__legacy.registerLegacyAPI({ server }); }, config(Joi: any) { return Joi.object({ diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/index.ts b/src/legacy/core_plugins/vis_type_timeseries/server/index.ts index 14047c31f2dcdb..c010628ca04bfd 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/index.ts +++ b/src/legacy/core_plugins/vis_type_timeseries/server/index.ts @@ -17,9 +17,5 @@ * under the License. */ -import { PluginInitializerContext } from 'kibana/server'; -import { MetricsServerPlugin as Plugin } from './plugin'; - -export function plugin(initializerContext: PluginInitializerContext) { - return new Plugin(initializerContext); -} +export { init } from './init'; +export { getVisData, GetVisData, GetVisDataOptions } from './lib/get_vis_data'; diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/plugin.ts b/src/legacy/core_plugins/vis_type_timeseries/server/init.ts similarity index 59% rename from src/legacy/core_plugins/vis_type_timeseries/server/plugin.ts rename to src/legacy/core_plugins/vis_type_timeseries/server/init.ts index ce4ab64ffa07a0..7b42ae8098016d 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/plugin.ts +++ b/src/legacy/core_plugins/vis_type_timeseries/server/init.ts @@ -17,9 +17,6 @@ * under the License. */ -import { Legacy } from 'kibana'; -import { PluginInitializerContext, CoreSetup } from 'kibana/server'; - // @ts-ignore import { fieldsRoutes } from './routes/fields'; // @ts-ignore @@ -28,30 +25,15 @@ import { visDataRoutes } from './routes/vis'; import { SearchStrategiesRegister } from './lib/search_strategies/search_strategies_register'; // @ts-ignore import { getVisData } from './lib/get_vis_data'; +import { Framework } from '../../../../plugins/vis_type_timeseries/server'; -// TODO: Remove as CoreSetup is completed. -export interface CustomCoreSetup { - http: { - server: Legacy.Server; - }; -} - -export class MetricsServerPlugin { - public initializerContext: PluginInitializerContext; - - constructor(initializerContext: PluginInitializerContext) { - this.initializerContext = initializerContext; - } - - public setup(core: CoreSetup & CustomCoreSetup) { - const { http } = core; - - fieldsRoutes(http.server); - visDataRoutes(http.server); +export const init = async (framework: Framework, __LEGACY: any) => { + const { core } = framework; + const router = core.http.createRouter(); - // Expose getVisData to allow plugins to use TSVB's backend for metrics - http.server.expose('getVisData', getVisData); + visDataRoutes(router, framework); - SearchStrategiesRegister.init(http.server); - } -} + // [LEGACY_TODO] + fieldsRoutes(__LEGACY.server); + SearchStrategiesRegister.init(__LEGACY.server); +}; diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/get_vis_data.ts b/src/legacy/core_plugins/vis_type_timeseries/server/lib/get_vis_data.ts new file mode 100644 index 00000000000000..58e624fa134429 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/get_vis_data.ts @@ -0,0 +1,101 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { RequestHandlerContext } from 'src/core/server'; +import _ from 'lodash'; +import { first, map } from 'rxjs/operators'; +import { getPanelData } from './vis_data/get_panel_data'; +import { Framework } from '../../../../../plugins/vis_type_timeseries/server'; + +interface GetVisDataResponse { + [key: string]: GetVisDataPanel; +} + +interface GetVisDataPanel { + id: string; + series: GetVisDataSeries[]; +} + +interface GetVisDataSeries { + id: string; + label: string; + data: GetVisDataDataPoint[]; +} + +type GetVisDataDataPoint = [number, number]; + +export interface GetVisDataOptions { + timerange?: any; + panels?: any; + filters?: any; + state?: any; + query?: any; +} + +export type GetVisData = ( + requestContext: RequestHandlerContext, + options: GetVisDataOptions, + framework: Framework +) => Promise; + +export function getVisData( + requestContext: RequestHandlerContext, + options: GetVisDataOptions, + framework: Framework +): Promise { + // NOTE / TODO: This facade has been put in place to make migrating to the New Platform easier. It + // removes the need to refactor many layers of dependencies on "req", and instead just augments the top + // level object passed from here. The layers should be refactored fully at some point, but for now + // this works and we are still using the New Platform services for these vis data portions. + const reqFacade: any = { + payload: options, + getUiSettingsService: () => requestContext.core.uiSettings.client, + getSavedObjectsClient: () => requestContext.core.savedObjects.client, + server: { + plugins: { + elasticsearch: { + getCluster: () => { + return { + callWithRequest: async (req: any, endpoint: string, params: any) => { + return await requestContext.core.elasticsearch.dataClient.callAsCurrentUser( + endpoint, + params + ); + }, + }; + }, + }, + }, + }, + getEsShardTimeout: async () => { + return await framework.globalConfig$ + .pipe( + first(), + map(config => config.elasticsearch.shardTimeout.asMilliseconds()) + ) + .toPromise(); + }, + }; + const promises = reqFacade.payload.panels.map(getPanelData(reqFacade)); + return Promise.all(promises).then(res => { + return res.reduce((acc, data) => { + return _.assign(acc as any, data); + }, {}); + }) as Promise; +} diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_es_shard_timeout.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_es_shard_timeout.js index 2b53fc5a93b7b8..b19f6a32415971 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_es_shard_timeout.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/__tests__/helpers/get_es_shard_timeout.js @@ -17,20 +17,14 @@ * under the License. */ -import moment from 'moment'; -import { of } from 'rxjs'; import { expect } from 'chai'; import { getEsShardTimeout } from '../../helpers/get_es_shard_timeout'; describe('getEsShardTimeout', () => { it('should return the elasticsearch.shardTimeout', async () => { const req = { - server: { - newPlatform: { - __internals: { - elasticsearch: { legacy: { config$: of({ shardTimeout: moment.duration(12345) }) } }, - }, - }, + getEsShardTimeout: async () => { + return 12345; }, }; diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/get_panel_data.d.ts b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/get_panel_data.d.ts new file mode 100644 index 00000000000000..2f86236fefedcb --- /dev/null +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/get_panel_data.d.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function getPanelData(req: any): any; diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js index e3745bd6963e0e..b4eb9e6b108ff7 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js @@ -48,7 +48,6 @@ export async function getSeriesData(req, panel) { const data = await searchRequest.search(searches); const series = data.map(handleResponseBody(panel)); - let annotations = null; if (panel.annotations && panel.annotations.length) { diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_es_shard_timeout.js b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_es_shard_timeout.js index c8acfa730af671..4eb3075e9b658d 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_es_shard_timeout.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_es_shard_timeout.js @@ -16,13 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { first, map } from 'rxjs/operators'; export async function getEsShardTimeout(req) { - return await req.server.newPlatform.__internals.elasticsearch.legacy.config$ - .pipe( - first(), - map(config => config.shardTimeout.asMilliseconds()) - ) - .toPromise(); + return await req.getEsShardTimeout(); } diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/routes/vis.js b/src/legacy/core_plugins/vis_type_timeseries/server/routes/vis.js index 15ff7b9a6c90b1..d2ded81309ffab 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/routes/vis.js +++ b/src/legacy/core_plugins/vis_type_timeseries/server/routes/vis.js @@ -17,23 +17,28 @@ * under the License. */ +import { schema } from '@kbn/config-schema'; import { getVisData } from '../lib/get_vis_data'; -import Boom from 'boom'; -export const visDataRoutes = server => { - server.route({ - path: '/api/metrics/vis/data', - method: 'POST', - handler: async req => { - try { - return await getVisData(req); - } catch (err) { - if (err.isBoom && err.status === 401) { - return err; - } +const escapeHatch = schema.object({}, { allowUnknowns: true }); - throw Boom.boomify(err, { statusCode: 500 }); - } +export const visDataRoutes = (router, framework) => { + router.post( + { + path: '/api/metrics/vis/data', + validate: { + body: escapeHatch, + }, }, - }); + async (requestContext, request, response) => { + try { + const results = await getVisData(requestContext, request.body, framework); + return response.ok({ body: results }); + } catch (error) { + return response.internalError({ + body: error.message, + }); + } + } + ); }; diff --git a/src/legacy/ui/public/agg_types/buckets/terms.ts b/src/legacy/ui/public/agg_types/buckets/terms.ts index e38f7ca4cc038e..ef9ceb96b005de 100644 --- a/src/legacy/ui/public/agg_types/buckets/terms.ts +++ b/src/legacy/ui/public/agg_types/buckets/terms.ts @@ -17,7 +17,6 @@ * under the License. */ -import chrome from 'ui/chrome'; import { noop } from 'lodash'; import { i18n } from '@kbn/i18n'; import { SearchSource, getRequestInspectorStats, getResponseInspectorStats } from '../../courier'; @@ -80,14 +79,8 @@ export const termsBucketAgg = new BucketAggType({ if (val === '__missing__') { return bucket.params.missingBucketLabel; } - const parsedUrl = { - origin: window.location.origin, - pathname: window.location.pathname, - basePath: chrome.getBasePath(), - }; - const converter = bucket.params.field.format.getConverterFor(type); - return converter(val, undefined, undefined, parsedUrl); + return bucket.params.field.format.convert(val, type); }; }, } as FieldFormat; diff --git a/src/legacy/ui/public/chrome/api/loading_count.js b/src/legacy/ui/public/chrome/api/loading_count.js index cafabbc5f7529d..71d1e354773d5b 100644 --- a/src/legacy/ui/public/chrome/api/loading_count.js +++ b/src/legacy/ui/public/chrome/api/loading_count.js @@ -24,7 +24,7 @@ const newPlatformHttp = npSetup.core.http; export function initLoadingCountApi(chrome) { const manualCount$ = new Rx.BehaviorSubject(0); - newPlatformHttp.addLoadingCount(manualCount$); + newPlatformHttp.addLoadingCountSource(manualCount$); chrome.loadingCount = new (class ChromeLoadingCountApi { /** diff --git a/src/legacy/ui/public/legacy_compat/angular_config.tsx b/src/legacy/ui/public/legacy_compat/angular_config.tsx index 6e9f5c85aa1b2a..7002b1c365eff0 100644 --- a/src/legacy/ui/public/legacy_compat/angular_config.tsx +++ b/src/legacy/ui/public/legacy_compat/angular_config.tsx @@ -173,7 +173,7 @@ const capture$httpLoadingCount = (newPlatform: CoreStart) => ( $rootScope: IRootScopeService, $http: IHttpService ) => { - newPlatform.http.addLoadingCount( + newPlatform.http.addLoadingCountSource( new Rx.Observable(observer => { const unwatch = $rootScope.$watch(() => { const reqs = $http.pendingRequests || []; diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index 3b2a77cefb7305..3d4292cef27f4c 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -45,11 +45,18 @@ export const mockUiSettings = { 'format:defaultTypeMap': {}, }; -export const npSetup = { - core: { - chrome: {}, - uiSettings: mockUiSettings, +const mockCore = { + chrome: {}, + uiSettings: mockUiSettings, + http: { + basePath: { + get: sinon.fake.returns(''), + }, }, +}; + +export const npSetup = { + core: mockCore, plugins: { usageCollection: { allowTrackUserAgent: sinon.fake(), @@ -95,7 +102,7 @@ export const npSetup = { getSavedQueryCount: sinon.fake(), }, }, - fieldFormats: getFieldFormatsRegistry(mockUiSettings), + fieldFormats: getFieldFormatsRegistry(mockCore), }, share: { register: () => {}, @@ -224,7 +231,7 @@ export const npStart = { history: sinon.fake(), }, }, - fieldFormats: getFieldFormatsRegistry(mockUiSettings), + fieldFormats: getFieldFormatsRegistry(mockCore), }, share: { toggleShareContextMenu: () => {}, diff --git a/src/legacy/ui/public/saved_objects/__tests__/saved_object.js b/src/legacy/ui/public/saved_objects/__tests__/saved_object.js index 30fb0c0c38aef2..d9622ac3dc6d21 100644 --- a/src/legacy/ui/public/saved_objects/__tests__/saved_object.js +++ b/src/legacy/ui/public/saved_objects/__tests__/saved_object.js @@ -26,7 +26,7 @@ import { createSavedObjectClass } from '../saved_object'; import StubIndexPattern from 'test_utils/stub_index_pattern'; import { npStart } from 'ui/new_platform'; import { InvalidJSONProperty } from '../../../../../plugins/kibana_utils/public'; -import { mockUiSettings } from '../../new_platform/new_platform.karma_mock'; +import { npSetup } from '../../new_platform/new_platform.karma_mock'; const getConfig = cfg => cfg; @@ -310,7 +310,7 @@ describe('Saved Object', function() { getConfig, null, [], - mockUiSettings + npSetup.core ); indexPattern.title = indexPattern.id; savedObject.searchSource.setField('index', indexPattern); @@ -700,7 +700,7 @@ describe('Saved Object', function() { getConfig, null, [], - mockUiSettings + npSetup.core ); indexPattern.title = indexPattern.id; savedObject.searchSource.setField('index', indexPattern); diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts index d754c1d3955955..0b99810a85afec 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts @@ -23,7 +23,7 @@ import { AggConfig, Vis } from 'ui/vis'; import { npStart } from 'ui/new_platform'; import { SerializedFieldFormat } from 'src/plugins/expressions/public'; -import { IFieldFormatId, FieldFormat } from '../../../../../../plugins/data/public'; +import { IFieldFormatId, FieldFormat, ContentType } from '../../../../../../plugins/data/public'; import { tabifyGetColumns } from '../../../agg_response/tabify/_get_columns'; import { DateRangeKey, convertDateRangeToString } from '../../../agg_types/buckets/date_range'; @@ -129,42 +129,23 @@ export const getFormat: FormatFactory = mapping => { }); return new IpRangeFormat(); } else if (isTermsFieldFormat(mapping) && mapping.params) { - const params = mapping.params; + const { params } = mapping; + const convert = (val: string, type: ContentType) => { + const format = getFieldFormat(params.id, mapping.params); + + if (val === '__other__') { + return params.otherBucketLabel; + } + if (val === '__missing__') { + return params.missingBucketLabel; + } + + return format.convert(val, type); + }; + return { - getConverterFor: (type: string) => { - const format = getFieldFormat(params.id, mapping.params); - return (val: string) => { - if (val === '__other__') { - return params.otherBucketLabel; - } - if (val === '__missing__') { - return params.missingBucketLabel; - } - const parsedUrl = { - origin: window.location.origin, - pathname: window.location.pathname, - basePath: npStart.core.http.basePath, - }; - // @ts-ignore - return format.convert(val, undefined, undefined, parsedUrl); - }; - }, - convert: (val: string, type: string) => { - const format = getFieldFormat(params.id, mapping.params); - if (val === '__other__') { - return params.otherBucketLabel; - } - if (val === '__missing__') { - return params.missingBucketLabel; - } - const parsedUrl = { - origin: window.location.origin, - pathname: window.location.pathname, - basePath: npStart.core.http.basePath, - }; - // @ts-ignore - return format.convert(val, type, undefined, parsedUrl); - }, + convert, + getConverterFor: (type: ContentType) => (val: string) => convert(val, type), } as FieldFormat; } else { return getFieldFormat(id, mapping.params); diff --git a/src/plugins/data/common/field_formats/content_types/html_content_type.ts b/src/plugins/data/common/field_formats/content_types/html_content_type.ts index ba2236c41790fa..1b6ee9e63fad16 100644 --- a/src/plugins/data/common/field_formats/content_types/html_content_type.ts +++ b/src/plugins/data/common/field_formats/content_types/html_content_type.ts @@ -26,7 +26,8 @@ const getConvertFn = ( format: IFieldFormat, convert?: HtmlContextTypeConvert ): HtmlContextTypeConvert => { - const fallbackHtml: HtmlContextTypeConvert = (value, field, hit) => { + const fallbackHtml: HtmlContextTypeConvert = (value, options = {}) => { + const { field, hit } = options; const formatted = escape(format.convert(value, 'text')); return !field || !hit || !hit.highlight || !hit.highlight[field.name] @@ -43,27 +44,23 @@ export const setup = ( ): HtmlContextTypeConvert => { const convert = getConvertFn(format, htmlContextTypeConvert); - const recurse: HtmlContextTypeConvert = (value, field, hit, meta) => { + const recurse: HtmlContextTypeConvert = (value, options = {}) => { if (value == null) { return asPrettyString(value); } if (!value || !isFunction(value.map)) { - return convert.call(format, value, field, hit, meta); + return convert.call(format, value, options); } - const subValues = value.map((v: any) => { - return recurse(v, field, hit, meta); - }); - const useMultiLine = subValues.some((sub: string) => { - return sub.indexOf('\n') > -1; - }); + const subValues = value.map((v: any) => recurse(v, options)); + const useMultiLine = subValues.some((sub: string) => sub.indexOf('\n') > -1); return subValues.join(',' + (useMultiLine ? '\n' : ' ')); }; - const wrap: HtmlContextTypeConvert = (value, field, hit, meta) => { - return `${recurse(value, field, hit, meta)}`; + const wrap: HtmlContextTypeConvert = (value, options) => { + return `${recurse(value, options)}`; }; return wrap; diff --git a/src/plugins/data/common/field_formats/converters/source.ts b/src/plugins/data/common/field_formats/converters/source.ts index c9906fb1360524..702e1579e945f2 100644 --- a/src/plugins/data/common/field_formats/converters/source.ts +++ b/src/plugins/data/common/field_formats/converters/source.ts @@ -42,7 +42,9 @@ export class SourceFormat extends FieldFormat { textConvert: TextContextTypeConvert = value => JSON.stringify(value); - htmlConvert: HtmlContextTypeConvert = (value, field, hit) => { + htmlConvert: HtmlContextTypeConvert = (value, options = {}) => { + const { field, hit } = options; + if (!field) { const converter = this.getConverterFor('text') as Function; diff --git a/src/plugins/data/common/field_formats/converters/url.test.ts b/src/plugins/data/common/field_formats/converters/url.test.ts index 66307cefe08f75..b1107d46179bff 100644 --- a/src/plugins/data/common/field_formats/converters/url.test.ts +++ b/src/plugins/data/common/field_formats/converters/url.test.ts @@ -169,64 +169,64 @@ describe('UrlFormat', () => { describe('whitelist', () => { test('should assume a relative url if the value is not in the whitelist without a base path', () => { - const url = new UrlFormat({}); const parsedUrl = { origin: 'http://kibana', basePath: '', }; + const url = new UrlFormat({ parsedUrl }); const converter = url.getConverterFor(HTML_CONTEXT_TYPE) as Function; - expect(converter('www.elastic.co', null, null, parsedUrl)).toBe( + expect(converter('www.elastic.co')).toBe( 'www.elastic.co' ); - expect(converter('elastic.co', null, null, parsedUrl)).toBe( + expect(converter('elastic.co')).toBe( 'elastic.co' ); - expect(converter('elastic', null, null, parsedUrl)).toBe( + expect(converter('elastic')).toBe( 'elastic' ); - expect(converter('ftp://elastic.co', null, null, parsedUrl)).toBe( + expect(converter('ftp://elastic.co')).toBe( 'ftp://elastic.co' ); }); test('should assume a relative url if the value is not in the whitelist with a basepath', () => { - const url = new UrlFormat({}); const parsedUrl = { origin: 'http://kibana', basePath: '/xyz', }; + const url = new UrlFormat({ parsedUrl }); const converter = url.getConverterFor(HTML_CONTEXT_TYPE) as Function; - expect(converter('www.elastic.co', null, null, parsedUrl)).toBe( + expect(converter('www.elastic.co')).toBe( 'www.elastic.co' ); - expect(converter('elastic.co', null, null, parsedUrl)).toBe( + expect(converter('elastic.co')).toBe( 'elastic.co' ); - expect(converter('elastic', null, null, parsedUrl)).toBe( + expect(converter('elastic')).toBe( 'elastic' ); - expect(converter('ftp://elastic.co', null, null, parsedUrl)).toBe( + expect(converter('ftp://elastic.co')).toBe( 'ftp://elastic.co' ); }); test('should rely on parsedUrl', () => { - const url = new UrlFormat({}); const parsedUrl = { origin: 'http://kibana.host.com', basePath: '/abc', }; + const url = new UrlFormat({ parsedUrl }); const converter = url.getConverterFor(HTML_CONTEXT_TYPE) as Function; - expect(converter('../app/kibana', null, null, parsedUrl)).toBe( + expect(converter('../app/kibana')).toBe( '../app/kibana' ); }); @@ -244,54 +244,52 @@ describe('UrlFormat', () => { }); test('should support multiple types of relative urls', () => { - const url = new UrlFormat({}); const parsedUrl = { origin: 'http://kibana.host.com', pathname: '/nbc/app/kibana#/discover', basePath: '/nbc', }; + const url = new UrlFormat({ parsedUrl }); const converter = url.getConverterFor(HTML_CONTEXT_TYPE) as Function; - expect(converter('#/foo', null, null, parsedUrl)).toBe( + expect(converter('#/foo')).toBe( '#/foo' ); - expect(converter('/nbc/app/kibana#/discover', null, null, parsedUrl)).toBe( + expect(converter('/nbc/app/kibana#/discover')).toBe( '/nbc/app/kibana#/discover' ); - expect(converter('../foo/bar', null, null, parsedUrl)).toBe( + expect(converter('../foo/bar')).toBe( '../foo/bar' ); }); test('should support multiple types of urls w/o basePath', () => { - const url = new UrlFormat({}); const parsedUrl = { origin: 'http://kibana.host.com', pathname: '/app/kibana', }; + const url = new UrlFormat({ parsedUrl }); const converter = url.getConverterFor(HTML_CONTEXT_TYPE) as Function; - expect(converter('10.22.55.66', null, null, parsedUrl)).toBe( + expect(converter('10.22.55.66')).toBe( '10.22.55.66' ); - expect( - converter('http://www.domain.name/app/kibana#/dashboard/', null, null, parsedUrl) - ).toBe( + expect(converter('http://www.domain.name/app/kibana#/dashboard/')).toBe( 'http://www.domain.name/app/kibana#/dashboard/' ); - expect(converter('/app/kibana', null, null, parsedUrl)).toBe( + expect(converter('/app/kibana')).toBe( '/app/kibana' ); - expect(converter('kibana#/dashboard/', null, null, parsedUrl)).toBe( + expect(converter('kibana#/dashboard/')).toBe( 'kibana#/dashboard/' ); - expect(converter('#/dashboard/', null, null, parsedUrl)).toBe( + expect(converter('#/dashboard/')).toBe( '#/dashboard/' ); }); diff --git a/src/plugins/data/common/field_formats/converters/url.ts b/src/plugins/data/common/field_formats/converters/url.ts index bd68dedf38a678..3c88511a4c63ee 100644 --- a/src/plugins/data/common/field_formats/converters/url.ts +++ b/src/plugins/data/common/field_formats/converters/url.ts @@ -134,7 +134,11 @@ export class UrlFormat extends FieldFormat { textConvert: TextContextTypeConvert = value => this.formatLabel(value); - htmlConvert: HtmlContextTypeConvert = (rawValue, field, hit, parsedUrl) => { + htmlConvert: HtmlContextTypeConvert = (rawValue, options = {}) => { + const { field, hit } = options; + const { parsedUrl } = this._params; + const { basePath, pathname, origin } = parsedUrl || {}; + const url = escape(this.formatUrl(rawValue)); const label = escape(this.formatLabel(rawValue, url)); @@ -170,17 +174,17 @@ export class UrlFormat extends FieldFormat { if (!inWhitelist) { // Handles urls like: `#/discover` if (url[0] === '#') { - prefix = `${parsedUrl.origin}${parsedUrl.pathname}`; + prefix = `${origin}${pathname}`; } // Handle urls like: `/app/kibana` or `/xyz/app/kibana` - else if (url.indexOf(parsedUrl.basePath || '/') === 0) { - prefix = `${parsedUrl.origin}`; + else if (url.indexOf(basePath || '/') === 0) { + prefix = `${origin}`; } // Handle urls like: `../app/kibana` else { const prefixEnd = url[0] === '/' ? '' : '/'; - prefix = `${parsedUrl.origin}${parsedUrl.basePath || ''}/app${prefixEnd}`; + prefix = `${origin}${basePath || ''}/app${prefixEnd}`; } } diff --git a/src/plugins/data/common/field_formats/field_format.ts b/src/plugins/data/common/field_formats/field_format.ts index 85d276767b5a79..50f07846a3cebd 100644 --- a/src/plugins/data/common/field_formats/field_format.ts +++ b/src/plugins/data/common/field_formats/field_format.ts @@ -24,6 +24,8 @@ import { FIELD_FORMAT_IDS, FieldFormatConvert, FieldFormatConvertFunction, + HtmlContextTypeOptions, + TextContextTypeOptions, } from './types'; import { htmlContentTypeSetup, @@ -63,8 +65,20 @@ export abstract class FieldFormat { */ convertObject: FieldFormatConvert | undefined; + /** + * @property {htmlConvert} + * @protected + * have to remove the protected because of + * https://github.com/Microsoft/TypeScript/issues/17293 + */ htmlConvert: HtmlContextTypeConvert | undefined; + /** + * @property {textConvert} + * @protected + * have to remove the protected because of + * https://github.com/Microsoft/TypeScript/issues/17293 + */ textConvert: TextContextTypeConvert | undefined; /** @@ -76,7 +90,7 @@ export abstract class FieldFormat { protected readonly _params: any; protected getConfig: Function | undefined; - constructor(_params: any = {}, getConfig?: Function) { + constructor(_params: Record = {}, getConfig?: Function) { this._params = _params; if (getConfig) { @@ -94,11 +108,15 @@ export abstract class FieldFormat { * injecting into the DOM or a DOM attribute * @public */ - convert(value: any, contentType: ContentType = DEFAULT_CONTEXT_TYPE): string { + convert( + value: any, + contentType: ContentType = DEFAULT_CONTEXT_TYPE, + options?: HtmlContextTypeOptions | TextContextTypeOptions + ): string { const converter = this.getConverterFor(contentType); if (converter) { - return converter.call(this, value); + return converter.call(this, value, options); } return value; diff --git a/src/plugins/data/common/field_formats/types.ts b/src/plugins/data/common/field_formats/types.ts index fc8e6e20a1a968..dce3c66b0f886b 100644 --- a/src/plugins/data/common/field_formats/types.ts +++ b/src/plugins/data/common/field_formats/types.ts @@ -24,15 +24,19 @@ export type ContentType = 'html' | 'text'; export { IFieldFormat } from './field_format'; /** @internal **/ -export type HtmlContextTypeConvert = ( - value: any, - field?: any, - hit?: Record, - meta?: any -) => string; +export interface HtmlContextTypeOptions { + field?: any; + hit?: Record; +} + +/** @internal **/ +export type HtmlContextTypeConvert = (value: any, options?: HtmlContextTypeOptions) => string; + +/** @internal **/ +export type TextContextTypeOptions = Record; /** @internal **/ -export type TextContextTypeConvert = (value: any) => string; +export type TextContextTypeConvert = (value: any, options?: TextContextTypeOptions) => string; /** @internal **/ export type FieldFormatConvertFunction = HtmlContextTypeConvert | TextContextTypeConvert; diff --git a/src/plugins/data/public/field_formats_provider/field_formats.test.ts b/src/plugins/data/public/field_formats_provider/field_formats.test.ts new file mode 100644 index 00000000000000..e58435fc71418d --- /dev/null +++ b/src/plugins/data/public/field_formats_provider/field_formats.test.ts @@ -0,0 +1,136 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { CoreSetup, IUiSettingsClient } from 'kibana/public'; + +import { FieldFormatRegisty } from './field_formats'; +import { + IFieldFormatType, + PercentFormat, + BoolFormat, + StringFormat, +} from '../../common/field_formats'; +import { coreMock } from '../../../../core/public/mocks'; + +const getValueOfPrivateField = (instance: any, field: string) => instance[field]; +const getUiSettingsMock = (data: any): IUiSettingsClient['get'] => () => data; + +describe('FieldFormatRegisty', () => { + let mockCoreSetup: CoreSetup; + let fieldFormatRegisty: FieldFormatRegisty; + + beforeEach(() => { + mockCoreSetup = coreMock.createSetup(); + fieldFormatRegisty = new FieldFormatRegisty(); + }); + + test('should allows to create an instance of "FieldFormatRegisty"', () => { + expect(fieldFormatRegisty).toBeDefined(); + expect(getValueOfPrivateField(fieldFormatRegisty, 'fieldFormats')).toBeDefined(); + expect(getValueOfPrivateField(fieldFormatRegisty, 'defaultMap')).toEqual({}); + }); + + describe('init', () => { + test('should provide an public "init" method', () => { + expect(fieldFormatRegisty.init).toBeDefined(); + expect(typeof fieldFormatRegisty.init).toBe('function'); + }); + + test('should set basePath value from "init" method', () => { + fieldFormatRegisty.init(mockCoreSetup); + + expect(getValueOfPrivateField(fieldFormatRegisty, 'basePath')).toBe( + mockCoreSetup.http.basePath.get() + ); + }); + + test('should populate the "defaultMap" object', () => { + const defaultMap = { + number: { id: 'number', params: {} }, + }; + + mockCoreSetup.uiSettings.get = getUiSettingsMock(defaultMap); + fieldFormatRegisty.init(mockCoreSetup); + expect(getValueOfPrivateField(fieldFormatRegisty, 'defaultMap')).toEqual(defaultMap); + }); + }); + + describe('register', () => { + test('should provide an public "register" method', () => { + expect(fieldFormatRegisty.register).toBeDefined(); + expect(typeof fieldFormatRegisty.register).toBe('function'); + }); + + test('should register field formats', () => { + fieldFormatRegisty.register([StringFormat, BoolFormat]); + + const registeredFieldFormatters: Map = getValueOfPrivateField( + fieldFormatRegisty, + 'fieldFormats' + ); + + expect(registeredFieldFormatters.size).toBe(2); + + expect(registeredFieldFormatters.get(BoolFormat.id)).toBe(BoolFormat); + expect(registeredFieldFormatters.get(StringFormat.id)).toBe(StringFormat); + expect(registeredFieldFormatters.get(PercentFormat.id)).toBeUndefined(); + }); + }); + + describe('getType', () => { + test('should provide an public "getType" method', () => { + expect(fieldFormatRegisty.getType).toBeDefined(); + expect(typeof fieldFormatRegisty.getType).toBe('function'); + }); + + test('should return the registered type of the field format by identifier', () => { + fieldFormatRegisty.register([StringFormat]); + + expect(fieldFormatRegisty.getType(StringFormat.id)).toBeDefined(); + }); + + test('should return void if the field format type has not been registered', () => { + fieldFormatRegisty.register([BoolFormat]); + + expect(fieldFormatRegisty.getType(StringFormat.id)).toBeUndefined(); + }); + }); + + describe('fieldFormatMetaParamsDecorator', () => { + test('should set meta params for all instances of FieldFormats', () => { + fieldFormatRegisty.register([StringFormat]); + + const ConcreteFormat = fieldFormatRegisty.getType(StringFormat.id); + + expect(ConcreteFormat).toBeDefined(); + + if (ConcreteFormat) { + const stringFormat = new ConcreteFormat({ + foo: 'foo', + }); + const params = getValueOfPrivateField(stringFormat, '_params'); + + expect(params).toHaveProperty('foo'); + expect(params).toHaveProperty('parsedUrl'); + expect(params.parsedUrl).toHaveProperty('origin'); + expect(params.parsedUrl).toHaveProperty('pathname'); + expect(params.parsedUrl).toHaveProperty('basePath'); + } + }); + }); +}); diff --git a/src/plugins/data/public/field_formats_provider/field_formats.ts b/src/plugins/data/public/field_formats_provider/field_formats.ts index 20e90b8e4a5458..3d60965f2e5322 100644 --- a/src/plugins/data/public/field_formats_provider/field_formats.ts +++ b/src/plugins/data/public/field_formats_provider/field_formats.ts @@ -18,7 +18,7 @@ */ import { forOwn, isFunction, memoize } from 'lodash'; -import { IUiSettingsClient } from 'kibana/public'; +import { IUiSettingsClient, CoreSetup } from 'kibana/public'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES, @@ -33,6 +33,7 @@ export class FieldFormatRegisty { private fieldFormats: Map; private uiSettings!: IUiSettingsClient; private defaultMap: Record; + private basePath?: string; constructor() { this.fieldFormats = new Map(); @@ -41,8 +42,9 @@ export class FieldFormatRegisty { getConfig = (key: string, override?: any) => this.uiSettings.get(key, override); - init(uiSettings: IUiSettingsClient) { + init({ uiSettings, http }: CoreSetup) { this.uiSettings = uiSettings; + this.basePath = http.basePath.get(); this.parseDefaultTypeMap(this.uiSettings.get('format:defaultTypeMap')); @@ -73,10 +75,14 @@ export class FieldFormatRegisty { * Get a derived FieldFormat class by its id. * * @param {IFieldFormatId} formatId - the format id - * @return {FieldFormat} + * @return {FieldFormat | void} */ - getType = (formatId: IFieldFormatId): IFieldFormatType | undefined => { - return this.fieldFormats.get(formatId); + getType = (formatId: IFieldFormatId): IFieldFormatType | void => { + const decoratedFieldFormat: any = this.fieldFormatMetaParamsDecorator(formatId); + + if (decoratedFieldFormat) { + return decoratedFieldFormat as IFieldFormatType; + } }; /** @@ -86,12 +92,12 @@ export class FieldFormatRegisty { * * @param {KBN_FIELD_TYPES} fieldType * @param {ES_FIELD_TYPES[]} esTypes - Array of ES data types - * @return {FieldFormat} + * @return {FieldFormat | void} */ getDefaultType = ( fieldType: KBN_FIELD_TYPES, esTypes: ES_FIELD_TYPES[] - ): IFieldFormatType | undefined => { + ): IFieldFormatType | void => { const config = this.getDefaultConfig(fieldType, esTypes); return this.getType(config.id); @@ -102,9 +108,9 @@ export class FieldFormatRegisty { * using the format:defaultTypeMap config map * * @param {ES_FIELD_TYPES[]} esTypes - Array of ES data types - * @return {ES_FIELD_TYPES} + * @return {ES_FIELD_TYPES | void} */ - getTypeNameByEsTypes = (esTypes: ES_FIELD_TYPES[] | undefined): ES_FIELD_TYPES | undefined => { + getTypeNameByEsTypes = (esTypes: ES_FIELD_TYPES[] | undefined): ES_FIELD_TYPES | void => { if (!Array.isArray(esTypes)) { return; } @@ -136,14 +142,14 @@ export class FieldFormatRegisty { * @return {FIELD_FORMATS_INSTANCES[number]} */ getInstance = memoize( - (formatId: IFieldFormatId): FieldFormat => { + (formatId: IFieldFormatId, params: Record = {}): FieldFormat => { const DerivedFieldFormat = this.getType(formatId); if (!DerivedFieldFormat) { throw new Error(`Field Format '${formatId}' not found!`); } - return new DerivedFieldFormat({}, this.getConfig); + return new DerivedFieldFormat(params, this.getConfig); } ); @@ -217,10 +223,33 @@ export class FieldFormatRegisty { } register = (fieldFormats: IFieldFormatType[]) => { - fieldFormats.forEach(fieldFormat => { - this.fieldFormats.set(fieldFormat.id, fieldFormat); - }); + fieldFormats.forEach(fieldFormat => this.fieldFormats.set(fieldFormat.id, fieldFormat)); return this; }; + + /** + * FieldFormat decorator - provide a one way to add meta-params for all field formatters + * + * @private + * @param {IFieldFormatId} formatId - the format id + * @return {FieldFormat | void} + */ + private fieldFormatMetaParamsDecorator = (formatId: IFieldFormatId): Function | void => { + const concreteFieldFormat = this.fieldFormats.get(formatId); + const decorateMetaParams = (customOptions: Record = {}) => ({ + parsedUrl: { + origin: window.location.origin, + pathname: window.location.pathname, + basePath: this.basePath, + }, + ...customOptions, + }); + + if (concreteFieldFormat) { + return function(params: Record = {}, getConfig?: Function) { + return new concreteFieldFormat(decorateMetaParams(params), getConfig); + }; + } + }; } diff --git a/src/plugins/data/public/field_formats_provider/field_formats_service.ts b/src/plugins/data/public/field_formats_provider/field_formats_service.ts index ea1a8af2930b01..42abeecc6fda00 100644 --- a/src/plugins/data/public/field_formats_provider/field_formats_service.ts +++ b/src/plugins/data/public/field_formats_provider/field_formats_service.ts @@ -17,7 +17,7 @@ * under the License. */ -import { IUiSettingsClient } from 'src/core/public'; +import { CoreSetup } from 'src/core/public'; import { FieldFormatRegisty } from './field_formats'; import { @@ -38,19 +38,11 @@ import { UrlFormat, } from '../../common/'; -/** - * Field Format Service - * @internal - */ -interface FieldFormatsServiceDependencies { - uiSettings: IUiSettingsClient; -} - export class FieldFormatsService { private readonly fieldFormats: FieldFormatRegisty = new FieldFormatRegisty(); - public setup({ uiSettings }: FieldFormatsServiceDependencies) { - this.fieldFormats.init(uiSettings); + public setup(core: CoreSetup) { + this.fieldFormats.init(core); this.fieldFormats.register([ BoolFormat, diff --git a/src/plugins/data/public/index_patterns/index_patterns/format_hit.ts b/src/plugins/data/public/index_patterns/index_patterns/format_hit.ts index 02d61f8b32c861..59ee18b3dcb509 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/format_hit.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/format_hit.ts @@ -19,6 +19,7 @@ import _ from 'lodash'; import { IndexPattern } from './index_pattern'; +import { ContentType } from '../../../common'; const formattedCache = new WeakMap(); const partialFormattedCache = new WeakMap(); @@ -26,14 +27,16 @@ const partialFormattedCache = new WeakMap(); // Takes a hit, merges it with any stored/scripted fields, and with the metaFields // returns a formatted version export function formatHitProvider(indexPattern: IndexPattern, defaultFormat: any) { - function convert(hit: Record, val: any, fieldName: string, type: string = 'html') { + function convert( + hit: Record, + val: any, + fieldName: string, + type: ContentType = 'html' + ) { const field = indexPattern.fields.getByName(fieldName); - if (!field) return defaultFormat.convert(val, type); - const parsedUrl = { - origin: window.location.origin, - pathname: window.location.pathname, - }; - return field.format.getConverterFor(type)(val, field, hit, parsedUrl); + const format = field ? field.format : defaultFormat; + + return format.convert(val, type, { field, hit }); } function formatHit(hit: Record, type: string = 'html') { diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts index 591290065d0245..919115d40b0684 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts @@ -19,7 +19,7 @@ // eslint-disable-next-line max-classes-per-file import { IndexPatterns } from './index_patterns'; -import { SavedObjectsClientContract, IUiSettingsClient, HttpServiceBase } from 'kibana/public'; +import { SavedObjectsClientContract, IUiSettingsClient, HttpSetup } from 'kibana/public'; jest.mock('./index_pattern', () => { class IndexPattern { @@ -49,7 +49,7 @@ describe('IndexPatterns', () => { beforeEach(() => { const savedObjectsClient = {} as SavedObjectsClientContract; const uiSettings = {} as IUiSettingsClient; - const http = {} as HttpServiceBase; + const http = {} as HttpSetup; indexPatterns = new IndexPatterns(uiSettings, savedObjectsClient, http); }); diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts index da58881b5b96e8..8d7dd0f0543662 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts @@ -21,7 +21,7 @@ import { SavedObjectsClientContract, SimpleSavedObject, IUiSettingsClient, - HttpServiceBase, + HttpStart, } from 'src/core/public'; import { createIndexPatternCache } from './_pattern_cache'; @@ -39,7 +39,7 @@ export class IndexPatterns { constructor( config: IUiSettingsClient, savedObjectsClient: SavedObjectsClientContract, - http: HttpServiceBase + http: HttpStart ) { this.apiClient = new IndexPatternsApiClient(http); this.config = config; diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts index 961e519338ac4d..52d18170168d47 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts @@ -17,7 +17,7 @@ * under the License. */ -import { HttpServiceBase } from 'src/core/public'; +import { HttpSetup } from 'src/core/public'; import { indexPatterns } from '../'; const API_BASE_URL: string = `/api/index_patterns/`; @@ -33,9 +33,9 @@ export interface GetFieldsOptions { export type IIndexPatternsApiClient = PublicMethodsOf; export class IndexPatternsApiClient { - private http: HttpServiceBase; + private http: HttpSetup; - constructor(http: HttpServiceBase) { + constructor(http: HttpSetup) { this.http = http; } diff --git a/src/plugins/data/public/suggestions_provider/value_suggestions.ts b/src/plugins/data/public/suggestions_provider/value_suggestions.ts index 68076cd43c336b..e64156c290db10 100644 --- a/src/plugins/data/public/suggestions_provider/value_suggestions.ts +++ b/src/plugins/data/public/suggestions_provider/value_suggestions.ts @@ -19,13 +19,13 @@ import { memoize } from 'lodash'; -import { IUiSettingsClient, HttpServiceBase } from 'src/core/public'; +import { IUiSettingsClient, HttpSetup } from 'src/core/public'; import { IGetSuggestions } from './types'; import { IFieldType } from '../../common'; export function getSuggestionsProvider( uiSettings: IUiSettingsClient, - http: HttpServiceBase + http: HttpSetup ): IGetSuggestions { const requestSuggestions = memoize( ( diff --git a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap index 4de883295bd8a6..2fce33793cd465 100644 --- a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap +++ b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap @@ -299,7 +299,7 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA }, }, "http": Object { - "addLoadingCount": [MockFunction], + "addLoadingCountSource": [MockFunction], "anonymousPaths": Object { "isAnonymous": [MockFunction], "register": [MockFunction], @@ -320,8 +320,6 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "patch": [MockFunction], "post": [MockFunction], "put": [MockFunction], - "removeAllInterceptors": [MockFunction], - "stop": [MockFunction], }, "i18n": Object { "Context": [MockFunction], @@ -920,7 +918,7 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA }, }, "http": Object { - "addLoadingCount": [MockFunction], + "addLoadingCountSource": [MockFunction], "anonymousPaths": Object { "isAnonymous": [MockFunction], "register": [MockFunction], @@ -941,8 +939,6 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "patch": [MockFunction], "post": [MockFunction], "put": [MockFunction], - "removeAllInterceptors": [MockFunction], - "stop": [MockFunction], }, "i18n": Object { "Context": [MockFunction], @@ -1529,7 +1525,7 @@ exports[`QueryStringInput Should pass the query language to the language switche }, }, "http": Object { - "addLoadingCount": [MockFunction], + "addLoadingCountSource": [MockFunction], "anonymousPaths": Object { "isAnonymous": [MockFunction], "register": [MockFunction], @@ -1550,8 +1546,6 @@ exports[`QueryStringInput Should pass the query language to the language switche "patch": [MockFunction], "post": [MockFunction], "put": [MockFunction], - "removeAllInterceptors": [MockFunction], - "stop": [MockFunction], }, "i18n": Object { "Context": [MockFunction], @@ -2147,7 +2141,7 @@ exports[`QueryStringInput Should pass the query language to the language switche }, }, "http": Object { - "addLoadingCount": [MockFunction], + "addLoadingCountSource": [MockFunction], "anonymousPaths": Object { "isAnonymous": [MockFunction], "register": [MockFunction], @@ -2168,8 +2162,6 @@ exports[`QueryStringInput Should pass the query language to the language switche "patch": [MockFunction], "post": [MockFunction], "put": [MockFunction], - "removeAllInterceptors": [MockFunction], - "stop": [MockFunction], }, "i18n": Object { "Context": [MockFunction], @@ -2756,7 +2748,7 @@ exports[`QueryStringInput Should render the given query 1`] = ` }, }, "http": Object { - "addLoadingCount": [MockFunction], + "addLoadingCountSource": [MockFunction], "anonymousPaths": Object { "isAnonymous": [MockFunction], "register": [MockFunction], @@ -2777,8 +2769,6 @@ exports[`QueryStringInput Should render the given query 1`] = ` "patch": [MockFunction], "post": [MockFunction], "put": [MockFunction], - "removeAllInterceptors": [MockFunction], - "stop": [MockFunction], }, "i18n": Object { "Context": [MockFunction], @@ -3374,7 +3364,7 @@ exports[`QueryStringInput Should render the given query 1`] = ` }, }, "http": Object { - "addLoadingCount": [MockFunction], + "addLoadingCountSource": [MockFunction], "anonymousPaths": Object { "isAnonymous": [MockFunction], "register": [MockFunction], @@ -3395,8 +3385,6 @@ exports[`QueryStringInput Should render the given query 1`] = ` "patch": [MockFunction], "post": [MockFunction], "put": [MockFunction], - "removeAllInterceptors": [MockFunction], - "stop": [MockFunction], }, "i18n": Object { "Context": [MockFunction], diff --git a/src/plugins/es_ui_shared/public/request/np_ready_request.ts b/src/plugins/es_ui_shared/public/request/np_ready_request.ts index 5a3f28ed76486c..b8f7db1463ab85 100644 --- a/src/plugins/es_ui_shared/public/request/np_ready_request.ts +++ b/src/plugins/es_ui_shared/public/request/np_ready_request.ts @@ -19,7 +19,7 @@ import { useEffect, useState, useRef } from 'react'; -import { HttpServiceBase, HttpFetchQuery } from '../../../../../src/core/public'; +import { HttpSetup, HttpFetchQuery } from '../../../../../src/core/public'; export interface SendRequestConfig { path: string; @@ -48,7 +48,7 @@ export interface UseRequestResponse { } export const sendRequest = async ( - httpClient: HttpServiceBase, + httpClient: HttpSetup, { path, method, body, query }: SendRequestConfig ): Promise => { try { @@ -67,7 +67,7 @@ export const sendRequest = async ( }; export const useRequest = ( - httpClient: HttpServiceBase, + httpClient: HttpSetup, { path, method, diff --git a/src/plugins/newsfeed/public/lib/api.test.ts b/src/plugins/newsfeed/public/lib/api.test.ts index 4383b9e0f7dabe..9d64dd26da047c 100644 --- a/src/plugins/newsfeed/public/lib/api.test.ts +++ b/src/plugins/newsfeed/public/lib/api.test.ts @@ -21,7 +21,7 @@ import { take, tap, toArray } from 'rxjs/operators'; import { interval, race } from 'rxjs'; import sinon, { stub } from 'sinon'; import moment from 'moment'; -import { HttpServiceBase } from 'src/core/public'; +import { HttpSetup } from 'src/core/public'; import { NEWSFEED_HASH_SET_STORAGE_KEY, NEWSFEED_LAST_FETCH_STORAGE_KEY } from '../../constants'; import { ApiItem, NewsfeedItem, NewsfeedPluginInjectedConfig } from '../../types'; import { NewsfeedApiDriver, getApi } from './api'; @@ -444,7 +444,7 @@ describe('getApi', () => { const mockHttpGet = jest.fn(); let httpMock = ({ fetch: mockHttpGet, - } as unknown) as HttpServiceBase; + } as unknown) as HttpSetup; const getHttpMockWithItems = (mockApiItems: ApiItem[]) => ( arg1: string, arg2: { method: string } @@ -478,7 +478,7 @@ describe('getApi', () => { }; httpMock = ({ fetch: mockHttpGet, - } as unknown) as HttpServiceBase; + } as unknown) as HttpSetup; }); it('creates a result', done => { diff --git a/src/plugins/newsfeed/public/lib/api.ts b/src/plugins/newsfeed/public/lib/api.ts index 6920dd9b2bccc2..bfeff4aa3e37bf 100644 --- a/src/plugins/newsfeed/public/lib/api.ts +++ b/src/plugins/newsfeed/public/lib/api.ts @@ -21,7 +21,7 @@ import * as Rx from 'rxjs'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { catchError, filter, mergeMap, tap } from 'rxjs/operators'; -import { HttpServiceBase } from 'src/core/public'; +import { HttpSetup } from 'src/core/public'; import { NEWSFEED_FALLBACK_LANGUAGE, NEWSFEED_LAST_FETCH_STORAGE_KEY, @@ -77,7 +77,7 @@ export class NewsfeedApiDriver { return { previous: old, current: updatedHashes }; } - fetchNewsfeedItems(http: HttpServiceBase, config: ApiConfig): Rx.Observable { + fetchNewsfeedItems(http: HttpSetup, config: ApiConfig): Rx.Observable { const urlPath = config.pathTemplate.replace('{VERSION}', this.kibanaVersion); const fullUrl = config.urlRoot + urlPath; @@ -166,7 +166,7 @@ export class NewsfeedApiDriver { * Computes hasNew value from new item hashes saved in localStorage */ export function getApi( - http: HttpServiceBase, + http: HttpSetup, config: NewsfeedPluginInjectedConfig['newsfeed'], kibanaVersion: string ): Rx.Observable { diff --git a/src/plugins/usage_collection/public/plugin.ts b/src/plugins/usage_collection/public/plugin.ts index 2ecc6c8bc2038f..7f80076a483b49 100644 --- a/src/plugins/usage_collection/public/plugin.ts +++ b/src/plugins/usage_collection/public/plugin.ts @@ -25,7 +25,7 @@ import { Plugin, CoreSetup, CoreStart, - HttpServiceBase, + HttpSetup, } from '../../../core/public'; interface PublicConfigType { @@ -41,7 +41,7 @@ export interface UsageCollectionSetup { METRIC_TYPE: typeof METRIC_TYPE; } -export function isUnauthenticated(http: HttpServiceBase) { +export function isUnauthenticated(http: HttpSetup) { const { anonymousPaths } = http; return anonymousPaths.isAnonymous(window.location.pathname); } diff --git a/src/plugins/usage_collection/public/services/create_reporter.ts b/src/plugins/usage_collection/public/services/create_reporter.ts index 6bc35de8972c32..304eb639d62ca6 100644 --- a/src/plugins/usage_collection/public/services/create_reporter.ts +++ b/src/plugins/usage_collection/public/services/create_reporter.ts @@ -18,12 +18,12 @@ */ import { Reporter, Storage } from '@kbn/analytics'; -import { HttpServiceBase } from 'kibana/public'; +import { HttpSetup } from 'kibana/public'; interface AnalyicsReporterConfig { localStorage: Storage; debug: boolean; - fetch: HttpServiceBase; + fetch: HttpSetup; } export function createReporter(config: AnalyicsReporterConfig): Reporter { diff --git a/src/plugins/vis_type_timeseries/kibana.json b/src/plugins/vis_type_timeseries/kibana.json new file mode 100644 index 00000000000000..f9a368e85ed491 --- /dev/null +++ b/src/plugins/vis_type_timeseries/kibana.json @@ -0,0 +1,6 @@ +{ + "id": "metrics", + "version": "8.0.0", + "kibanaVersion": "kibana", + "server": true +} \ No newline at end of file diff --git a/src/plugins/vis_type_timeseries/server/index.ts b/src/plugins/vis_type_timeseries/server/index.ts new file mode 100644 index 00000000000000..599726612a936a --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/index.ts @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginInitializerContext } from 'src/core/server'; +import { VisTypeTimeseriesPlugin } from './plugin'; +export { VisTypeTimeseriesSetup, Framework } from './plugin'; + +export const config = { + schema: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), +}; + +export type VisTypeTimeseriesConfig = TypeOf; + +export function plugin(initializerContext: PluginInitializerContext) { + return new VisTypeTimeseriesPlugin(initializerContext); +} diff --git a/src/plugins/vis_type_timeseries/server/plugin.ts b/src/plugins/vis_type_timeseries/server/plugin.ts new file mode 100644 index 00000000000000..f508aa250454fa --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/plugin.ts @@ -0,0 +1,96 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + RequestHandlerContext, + Logger, +} from 'src/core/server'; +import { Observable } from 'rxjs'; +import { Server } from 'hapi'; +import { once } from 'lodash'; +import { VisTypeTimeseriesConfig } from '.'; +import { + init, + getVisData, + GetVisData, + GetVisDataOptions, +} from '../../../legacy/core_plugins/vis_type_timeseries/server'; + +export interface LegacySetup { + server: Server; +} + +export interface VisTypeTimeseriesSetup { + /** @deprecated */ + __legacy: { + config$: Observable; + registerLegacyAPI: (__LEGACY: LegacySetup) => void; + }; + getVisData: ( + requestContext: RequestHandlerContext, + options: GetVisDataOptions + ) => ReturnType; +} + +export interface Framework { + core: CoreSetup; + plugins: any; + config$: Observable; + globalConfig$: PluginInitializerContext['config']['legacy']['globalConfig$']; + logger: Logger; +} + +export class VisTypeTimeseriesPlugin implements Plugin { + constructor(private readonly initializerContext: PluginInitializerContext) { + this.initializerContext = initializerContext; + } + + public setup(core: CoreSetup, plugins: any) { + const logger = this.initializerContext.logger.get('visTypeTimeseries'); + const config$ = this.initializerContext.config.create(); + // Global config contains things like the ES shard timeout + const globalConfig$ = this.initializerContext.config.legacy.globalConfig$; + + const framework: Framework = { + core, + plugins, + config$, + globalConfig$, + logger, + }; + + return { + __legacy: { + config$, + registerLegacyAPI: once((__LEGACY: LegacySetup) => { + init(framework, __LEGACY); + }), + }, + getVisData: async (requestContext: RequestHandlerContext, options: GetVisDataOptions) => { + return await getVisData(requestContext, options, framework); + }, + }; + } + + public start(core: CoreStart) {} +} diff --git a/src/test_utils/public/stub_field_formats.ts b/src/test_utils/public/stub_field_formats.ts index da1a31f1cc7a53..ea46710c0dc84c 100644 --- a/src/test_utils/public/stub_field_formats.ts +++ b/src/test_utils/public/stub_field_formats.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { IUiSettingsClient } from 'kibana/public'; +import { CoreSetup } from 'kibana/public'; import { FieldFormatRegisty, @@ -37,7 +37,7 @@ import { UrlFormat, } from '../../plugins/data/public/'; -export const getFieldFormatsRegistry = (uiSettings: IUiSettingsClient) => { +export const getFieldFormatsRegistry = (core: CoreSetup) => { const fieldFormats = new FieldFormatRegisty(); fieldFormats.register([ @@ -58,7 +58,7 @@ export const getFieldFormatsRegistry = (uiSettings: IUiSettingsClient) => { UrlFormat, ]); - fieldFormats.init(uiSettings); + fieldFormats.init(core); return fieldFormats; }; diff --git a/src/test_utils/public/stub_index_pattern.js b/src/test_utils/public/stub_index_pattern.js index 14931b938732c8..7807821ab09bd3 100644 --- a/src/test_utils/public/stub_index_pattern.js +++ b/src/test_utils/public/stub_index_pattern.js @@ -40,8 +40,8 @@ setFieldFormats({ import { getFieldFormatsRegistry } from './stub_field_formats'; -export default function StubIndexPattern(pattern, getConfig, timeField, fields, uiSettings) { - const registeredFieldFormats = getFieldFormatsRegistry(uiSettings); +export default function StubIndexPattern(pattern, getConfig, timeField, fields, core) { + const registeredFieldFormats = getFieldFormatsRegistry(core); this.id = pattern; this.title = pattern; diff --git a/test/functional/apps/dashboard/empty_dashboard.js b/test/functional/apps/dashboard/empty_dashboard.js index 0f10eabcba4341..d46daff183abf1 100644 --- a/test/functional/apps/dashboard/empty_dashboard.js +++ b/test/functional/apps/dashboard/empty_dashboard.js @@ -28,8 +28,7 @@ export default function({ getService, getPageObjects }) { const dashboardExpect = getService('dashboardExpect'); const PageObjects = getPageObjects(['common', 'dashboard']); - // FLAKY: https://github.com/elastic/kibana/issues/48236 - describe.skip('empty dashboard', () => { + describe('empty dashboard', () => { before(async () => { await esArchiver.load('dashboard/current/kibana'); await kibanaServer.uiSettings.replace({ @@ -57,11 +56,12 @@ export default function({ getService, getPageObjects }) { }); it('should add new visualization from dashboard', async () => { + await testSubjects.exists('addVisualizationButton'); await testSubjects.click('addVisualizationButton'); - await dashboardVisualizations.createAndAddMarkdown( - { name: 'Dashboard Test Markdown', markdown: 'Markdown text' }, - false - ); + await dashboardVisualizations.createAndAddMarkdown({ + name: 'Dashboard Test Markdown', + markdown: 'Markdown text', + }); await PageObjects.dashboard.waitForRenderComplete(); await dashboardExpect.markdownWithValuesExists(['Markdown text']); }); diff --git a/test/functional/services/dashboard/visualizations.js b/test/functional/services/dashboard/visualizations.js index 1facf49651b197..c4e7fe6ad3bd9f 100644 --- a/test/functional/services/dashboard/visualizations.js +++ b/test/functional/services/dashboard/visualizations.js @@ -19,6 +19,8 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }) { const log = getService('log'); + const find = getService('find'); + const retry = getService('retry'); const queryBar = getService('queryBar'); const testSubjects = getService('testSubjects'); const dashboardAddPanel = getService('dashboardAddPanel'); @@ -72,16 +74,38 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }) { await dashboardAddPanel.addSavedSearch(name); } - async createAndAddMarkdown({ name, markdown }, checkForAddPanel = true) { + async clickAddVisualizationButton() { + log.debug('DashboardVisualizations.clickAddVisualizationButton'); + await testSubjects.click('addVisualizationButton'); + } + + async isNewVisDialogShowing() { + log.debug('DashboardVisualizations.isNewVisDialogShowing'); + return await find.existsByCssSelector('.visNewVisDialog'); + } + + async ensureNewVisualizationDialogIsShowing() { + let isShowing = await this.isNewVisDialogShowing(); + log.debug(`DashboardVisualizations.ensureNewVisualizationDialogIsShowing:${isShowing}`); + if (!isShowing) { + await retry.try(async () => { + await this.clickAddVisualizationButton(); + isShowing = await this.isNewVisDialogShowing(); + log.debug(`DashboardVisualizations.ensureNewVisualizationDialogIsShowing:${isShowing}`); + if (!isShowing) { + throw new Error('New Vis Dialog still not open, trying again.'); + } + }); + } + } + + async createAndAddMarkdown({ name, markdown }) { log.debug(`createAndAddMarkdown(${markdown})`); const inViewMode = await PageObjects.dashboard.getIsInViewMode(); if (inViewMode) { await PageObjects.dashboard.switchToEditMode(); } - if (checkForAddPanel) { - await dashboardAddPanel.ensureAddPanelIsShowing(); - await dashboardAddPanel.clickAddNewEmbeddableLink('visualization'); - } + await this.ensureNewVisualizationDialogIsShowing(); await PageObjects.visualize.clickMarkdownWidget(); await PageObjects.visualize.setMarkdownTxt(markdown); await PageObjects.visualize.clickGo(); diff --git a/x-pack/legacy/plugins/actions/server/constants/plugin.ts b/x-pack/legacy/plugins/actions/server/constants/plugin.ts new file mode 100644 index 00000000000000..27cf0a8d2bf88d --- /dev/null +++ b/x-pack/legacy/plugins/actions/server/constants/plugin.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LICENSE_TYPE_BASIC, LicenseType } from '../../../../common/constants'; + +export const PLUGIN = { + ID: 'actions', + MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_BASIC as LicenseType, // TODO: supposed to be changed up on requirements + getI18nName: (i18n: any): string => + i18n.translate('xpack.actions.appName', { + defaultMessage: 'Actions', + }), +}; diff --git a/x-pack/legacy/plugins/actions/server/create_execute_function.test.ts b/x-pack/legacy/plugins/actions/server/create_execute_function.test.ts index c6817b3bc12f3d..c5496cd92cc9f8 100644 --- a/x-pack/legacy/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/legacy/plugins/actions/server/create_execute_function.test.ts @@ -138,6 +138,7 @@ describe('execute()', () => { id: '123', params: { baz: false }, spaceId: 'default', + apiKey: null, }); expect(getScopedSavedObjectsClient).toHaveBeenCalledWith({ getBasePath: expect.anything(), diff --git a/x-pack/legacy/plugins/actions/server/create_execute_function.ts b/x-pack/legacy/plugins/actions/server/create_execute_function.ts index 451be9354006ea..8ff12b8c3fa4b6 100644 --- a/x-pack/legacy/plugins/actions/server/create_execute_function.ts +++ b/x-pack/legacy/plugins/actions/server/create_execute_function.ts @@ -18,7 +18,7 @@ export interface ExecuteOptions { id: string; params: Record; spaceId: string; - apiKey?: string; + apiKey: string | null; } export function createExecuteFunction({ diff --git a/x-pack/legacy/plugins/actions/server/extend_route_with_license_check.test.ts b/x-pack/legacy/plugins/actions/server/extend_route_with_license_check.test.ts new file mode 100644 index 00000000000000..186c26f6100e4f --- /dev/null +++ b/x-pack/legacy/plugins/actions/server/extend_route_with_license_check.test.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { extendRouteWithLicenseCheck } from './extend_route_with_license_check'; +import { LicenseState } from './lib/license_state'; +jest.mock('./lib/license_state', () => ({ + verifyApiAccessFactory: () => {}, +})); + +describe('extendRouteWithLicenseCheck', () => { + describe('#actionsextendRouteWithLicenseCheck', () => { + let licenseState: jest.Mocked; + + test('extends route object with license, if config property already exists', () => { + const newRoute = extendRouteWithLicenseCheck( + { config: { someTestProperty: 'test' } }, + licenseState + ); + expect(newRoute.config.pre.length > 0); + }); + test('extends route object with license check uder options.pre', () => { + const newRoute = extendRouteWithLicenseCheck( + { options: { someProperty: 'test' } }, + licenseState + ); + expect(newRoute.options.pre.length > 0); + }); + }); +}); diff --git a/x-pack/legacy/plugins/actions/server/extend_route_with_license_check.ts b/x-pack/legacy/plugins/actions/server/extend_route_with_license_check.ts new file mode 100644 index 00000000000000..f39dc125071b4d --- /dev/null +++ b/x-pack/legacy/plugins/actions/server/extend_route_with_license_check.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; + * you may not use this file except in compliance with the Elastic License. + */ +import { LicenseState, verifyApiAccessFactory } from './lib/license_state'; + +export function extendRouteWithLicenseCheck(route: any, licenseState: LicenseState) { + const verifyApiAccessPreRouting = verifyApiAccessFactory(licenseState); + + const key = route.options ? 'options' : 'config'; + return { + ...route, + [key]: { + ...route[key], + pre: [verifyApiAccessPreRouting], + }, + }; +} diff --git a/x-pack/legacy/plugins/actions/server/lib/license_state.test.ts b/x-pack/legacy/plugins/actions/server/lib/license_state.test.ts new file mode 100644 index 00000000000000..e58c52f63c8cb0 --- /dev/null +++ b/x-pack/legacy/plugins/actions/server/lib/license_state.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { LicenseState } from './license_state'; +import { licensingMock } from '../../../../../plugins/licensing/server/mocks'; +import { LICENSE_CHECK_STATE } from '../../../../../plugins/licensing/server'; + +describe('license_state', () => { + let getRawLicense: any; + + beforeEach(() => { + getRawLicense = jest.fn(); + }); + + describe('status is LICENSE_STATUS_INVALID', () => { + beforeEach(() => { + const license = licensingMock.createLicense({ license: { status: 'invalid' } }); + license.check = jest.fn(() => ({ + state: LICENSE_CHECK_STATE.Invalid, + })); + getRawLicense.mockReturnValue(license); + }); + + it('check application link should be disabled', () => { + const licensing = licensingMock.createSetup(); + const licenseState = new LicenseState(licensing.license$); + const actionsLicenseInfo = licenseState.checkLicense(getRawLicense()); + expect(actionsLicenseInfo.enableAppLink).to.be(false); + }); + }); + + describe('status is LICENSE_STATUS_VALID', () => { + beforeEach(() => { + const license = licensingMock.createLicense({ license: { status: 'active' } }); + license.check = jest.fn(() => ({ + state: LICENSE_CHECK_STATE.Valid, + })); + getRawLicense.mockReturnValue(license); + }); + + it('check application link should be enabled', () => { + const licensing = licensingMock.createSetup(); + const licenseState = new LicenseState(licensing.license$); + const actionsLicenseInfo = licenseState.checkLicense(getRawLicense()); + expect(actionsLicenseInfo.showAppLink).to.be(true); + }); + }); +}); diff --git a/x-pack/legacy/plugins/actions/server/lib/license_state.ts b/x-pack/legacy/plugins/actions/server/lib/license_state.ts new file mode 100644 index 00000000000000..b4de23ef0a949d --- /dev/null +++ b/x-pack/legacy/plugins/actions/server/lib/license_state.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { i18n } from '@kbn/i18n'; +import { Observable, Subscription } from 'rxjs'; +import { ILicense, LICENSE_CHECK_STATE } from '../../../../../plugins/licensing/common/types'; +import { assertNever } from '../../../../../../src/core/utils'; +import { PLUGIN } from '../constants/plugin'; + +export interface ActionsLicenseInformation { + showAppLink: boolean; + enableAppLink: boolean; + message: string; +} + +export class LicenseState { + private licenseInformation: ActionsLicenseInformation = this.checkLicense(undefined); + private subscription: Subscription; + + constructor(license$: Observable) { + this.subscription = license$.subscribe(this.updateInformation.bind(this)); + } + + private updateInformation(license: ILicense | undefined) { + this.licenseInformation = this.checkLicense(license); + } + + public clean() { + this.subscription.unsubscribe(); + } + + public getLicenseInformation() { + return this.licenseInformation; + } + + public checkLicense(license: ILicense | undefined): ActionsLicenseInformation { + if (!license?.isAvailable) { + return { + showAppLink: true, + enableAppLink: false, + message: i18n.translate( + 'xpack.actions.serverSideErrors.unavailableLicenseInformationErrorMessage', + { + defaultMessage: + 'Actions is unavailable - license information is not available at this time.', + } + ), + }; + } + + const check = license.check(PLUGIN.ID, PLUGIN.MINIMUM_LICENSE_REQUIRED); + + switch (check.state) { + case LICENSE_CHECK_STATE.Expired: + return { + showAppLink: true, + enableAppLink: false, + message: check.message || '', + }; + case LICENSE_CHECK_STATE.Invalid: + case LICENSE_CHECK_STATE.Unavailable: + return { + showAppLink: false, + enableAppLink: false, + message: check.message || '', + }; + case LICENSE_CHECK_STATE.Valid: + return { + showAppLink: true, + enableAppLink: true, + message: '', + }; + default: + return assertNever(check.state); + } + } +} + +export function verifyApiAccessFactory(licenseState: LicenseState) { + function verifyApiAccess() { + const licenseCheckResults = licenseState.getLicenseInformation(); + + if (licenseCheckResults.showAppLink && licenseCheckResults.enableAppLink) { + return null; + } + + throw Boom.forbidden(licenseCheckResults.message); + } + return verifyApiAccess; +} diff --git a/x-pack/legacy/plugins/actions/server/plugin.ts b/x-pack/legacy/plugins/actions/server/plugin.ts index 6a41bf9a8b459c..d55b30b8e30ea2 100644 --- a/x-pack/legacy/plugins/actions/server/plugin.ts +++ b/x-pack/legacy/plugins/actions/server/plugin.ts @@ -33,6 +33,8 @@ import { listActionTypesRoute, getExecuteActionRoute, } from './routes'; +import { extendRouteWithLicenseCheck } from './extend_route_with_license_check'; +import { LicenseState } from './lib/license_state'; export interface PluginSetupContract { registerType: ActionTypeRegistry['register']; @@ -54,6 +56,7 @@ export class Plugin { private actionTypeRegistry?: ActionTypeRegistry; private actionExecutor?: ActionExecutor; private defaultKibanaIndex?: string; + private licenseState: LicenseState | null = null; constructor(initializerContext: ActionsPluginInitializerContext) { this.logger = initializerContext.logger.get('plugins', 'alerting'); @@ -69,6 +72,8 @@ export class Plugin { this.adminClient = await core.elasticsearch.adminClient$.pipe(first()).toPromise(); this.defaultKibanaIndex = (await this.kibana$.pipe(first()).toPromise()).index; + this.licenseState = new LicenseState(plugins.licensing.license$); + // Encrypted attributes // - `secrets` properties will be encrypted // - `config` will be included in AAD @@ -103,13 +108,15 @@ export class Plugin { }); // Routes - core.http.route(createActionRoute); - core.http.route(deleteActionRoute); - core.http.route(getActionRoute); - core.http.route(findActionRoute); - core.http.route(updateActionRoute); - core.http.route(listActionTypesRoute); - core.http.route(getExecuteActionRoute(actionExecutor)); + core.http.route(extendRouteWithLicenseCheck(createActionRoute, this.licenseState)); + core.http.route(extendRouteWithLicenseCheck(deleteActionRoute, this.licenseState)); + core.http.route(extendRouteWithLicenseCheck(getActionRoute, this.licenseState)); + core.http.route(extendRouteWithLicenseCheck(findActionRoute, this.licenseState)); + core.http.route(extendRouteWithLicenseCheck(updateActionRoute, this.licenseState)); + core.http.route(extendRouteWithLicenseCheck(listActionTypesRoute, this.licenseState)); + core.http.route( + extendRouteWithLicenseCheck(getExecuteActionRoute(actionExecutor), this.licenseState) + ); return { registerType: actionTypeRegistry.register.bind(actionTypeRegistry), @@ -176,4 +183,10 @@ export class Plugin { }, }; } + + public stop() { + if (this.licenseState) { + this.licenseState.clean(); + } + } } diff --git a/x-pack/legacy/plugins/actions/server/shim.ts b/x-pack/legacy/plugins/actions/server/shim.ts index 1fa8f755cc7ee2..3887e62c4c40a6 100644 --- a/x-pack/legacy/plugins/actions/server/shim.ts +++ b/x-pack/legacy/plugins/actions/server/shim.ts @@ -22,6 +22,7 @@ import { LoggerFactory, SavedObjectsLegacyService, } from '../../../../../src/core/server'; +import { LicensingPluginSetup } from '../../../../plugins/licensing/server'; // Extend PluginProperties to indicate which plugins are guaranteed to exist // due to being marked as dependencies @@ -76,6 +77,7 @@ export interface ActionsPluginsSetup { task_manager: TaskManagerSetupContract; xpack_main: XPackMainPluginSetupContract; encryptedSavedObjects: EncryptedSavedObjectsSetupContract; + licensing: LicensingPluginSetup; } export interface ActionsPluginsStart { security?: SecurityPluginStartContract; @@ -134,6 +136,7 @@ export function shim( xpack_main: server.plugins.xpack_main, encryptedSavedObjects: newPlatform.setup.plugins .encryptedSavedObjects as EncryptedSavedObjectsSetupContract, + licensing: newPlatform.setup.plugins.licensing as LicensingPluginSetup, }; const pluginsStart: ActionsPluginsStart = { diff --git a/x-pack/legacy/plugins/alerting/README.md b/x-pack/legacy/plugins/alerting/README.md index 0b4024be395488..33679cf6fa4227 100644 --- a/x-pack/legacy/plugins/alerting/README.md +++ b/x-pack/legacy/plugins/alerting/README.md @@ -200,7 +200,7 @@ Payload: |name|A name to reference and search in the future.|string| |tags|A list of keywords to reference and search in the future.|string[]| |alertTypeId|The id value of the alert type you want to call when the alert is scheduled to execute.|string| -|schedule|The schedule specifying when this alert should run, using one of the available schedule formats specified under _Schedule Formats_ below|object| +|schedule|The schedule specifying when this alert should be run, using one of the available schedule formats specified under _Schedule Formats_ below|object| |params|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object| |actions|Array of the following:
- `group` (string): We support grouping actions in the scenario of escalations or different types of alert instances. If you don't need this, feel free to use `default` as a value.
- `id` (string): The id of the action saved object to execute.
- `params` (object): The map to the `params` the action type will receive. In order to help apply context to strings, we handle them as mustache templates and pass in a default set of context. (see templating actions).|array| diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts index 340b1bf766e243..d11541e9378bb9 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts @@ -3,13 +3,15 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import uuid from 'uuid'; import { schema } from '@kbn/config-schema'; import { AlertsClient } from './alerts_client'; import { savedObjectsClientMock, loggingServiceMock } from '../../../../../src/core/server/mocks'; import { taskManagerMock } from '../../task_manager/task_manager.mock'; import { alertTypeRegistryMock } from './alert_type_registry.mock'; import { TaskStatus } from '../../task_manager'; +import { IntervalSchedule } from './types'; +import { resolvable } from './test_utils'; const taskManager = taskManagerMock.create(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -29,6 +31,7 @@ beforeEach(() => { jest.resetAllMocks(); alertsClientParams.createAPIKey.mockResolvedValue({ created: false }); alertsClientParams.getUserName.mockResolvedValue('elastic'); + taskManager.runNow.mockResolvedValue({ id: '' }); }); const mockedDate = new Date('2019-02-12T21:01:22.479Z'); @@ -183,8 +186,8 @@ describe('create()', () => { }, ], "alertTypeId": "123", - "apiKey": undefined, - "apiKeyOwner": undefined, + "apiKey": null, + "apiKeyOwner": null, "consumer": "bar", "createdBy": "elastic", "enabled": true, @@ -1955,6 +1958,228 @@ describe('update()', () => { `"params invalid: [param1]: expected value of type [string] but got [undefined]"` ); }); + + describe('updating an alert schedule', () => { + function mockApiCalls( + alertId: string, + taskId: string, + currentSchedule: IntervalSchedule, + updatedSchedule: IntervalSchedule + ) { + // mock return values from deps + alertTypeRegistry.get.mockReturnValueOnce({ + id: '123', + name: 'Test', + actionGroups: ['default'], + async executor() {}, + }); + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + savedObjectsClient.get.mockResolvedValueOnce({ + id: alertId, + type: 'alert', + attributes: { + enabled: true, + alertTypeId: '123', + schedule: currentSchedule, + scheduledTaskId: 'task-123', + }, + references: [], + version: '123', + }); + + taskManager.schedule.mockResolvedValueOnce({ + id: taskId, + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + savedObjectsClient.update.mockResolvedValueOnce({ + id: alertId, + type: 'alert', + attributes: { + enabled: true, + schedule: updatedSchedule, + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + scheduledTaskId: taskId, + }, + references: [ + { + name: 'action_0', + type: 'action', + id: alertId, + }, + ], + }); + + taskManager.runNow.mockReturnValueOnce(Promise.resolve({ id: alertId })); + } + + test('updating the alert schedule should rerun the task immediately', async () => { + const alertId = uuid.v4(); + const taskId = uuid.v4(); + const alertsClient = new AlertsClient(alertsClientParams); + + mockApiCalls(alertId, taskId, { interval: '60m' }, { interval: '10s' }); + + await alertsClient.update({ + id: alertId, + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + ], + }, + }); + + expect(taskManager.runNow).toHaveBeenCalledWith(taskId); + }); + + test('updating the alert without changing the schedule should not rerun the task', async () => { + const alertId = uuid.v4(); + const taskId = uuid.v4(); + const alertsClient = new AlertsClient(alertsClientParams); + + mockApiCalls(alertId, taskId, { interval: '10s' }, { interval: '10s' }); + + await alertsClient.update({ + id: alertId, + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + ], + }, + }); + + expect(taskManager.runNow).not.toHaveBeenCalled(); + }); + + test('updating the alert should not wait for the rerun the task to complete', async done => { + const alertId = uuid.v4(); + const taskId = uuid.v4(); + const alertsClient = new AlertsClient(alertsClientParams); + + mockApiCalls(alertId, taskId, { interval: '10s' }, { interval: '30s' }); + + const resolveAfterAlertUpdatedCompletes = resolvable<{ id: string }>(); + resolveAfterAlertUpdatedCompletes.then(() => done()); + + taskManager.runNow.mockReset(); + taskManager.runNow.mockReturnValue(resolveAfterAlertUpdatedCompletes); + + await alertsClient.update({ + id: alertId, + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + ], + }, + }); + + expect(taskManager.runNow).toHaveBeenCalled(); + + resolveAfterAlertUpdatedCompletes.resolve({ id: alertId }); + }); + + test('logs when the rerun of an alerts underlying task fails', async () => { + const alertId = uuid.v4(); + const taskId = uuid.v4(); + const alertsClient = new AlertsClient(alertsClientParams); + + mockApiCalls(alertId, taskId, { interval: '10s' }, { interval: '30s' }); + + taskManager.runNow.mockReset(); + taskManager.runNow.mockRejectedValue(new Error('Failed to run alert')); + + await alertsClient.update({ + id: alertId, + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + ], + }, + }); + + expect(taskManager.runNow).toHaveBeenCalled(); + + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + `Alert update failed to run its underlying task. TaskManager runNow failed with Error: Failed to run alert` + ); + }); + }); }); describe('updateApiKey()', () => { diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.ts index 578daa445b6ffe..70d2ff8ca30331 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.ts @@ -5,9 +5,14 @@ */ import Boom from 'boom'; -import { omit } from 'lodash'; +import { omit, isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { Logger, SavedObjectsClientContract, SavedObjectReference } from 'src/core/server'; +import { + Logger, + SavedObjectsClientContract, + SavedObjectReference, + SavedObject, +} from 'src/core/server'; import { Alert, RawAlert, @@ -126,7 +131,6 @@ export class AlertsClient { // Throws an error if alert type isn't registered const alertType = this.alertTypeRegistry.get(data.alertTypeId); const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.params); - const apiKey = await this.createAPIKey(); const username = await this.getUserName(); this.validateActions(alertType, data.actions); @@ -134,13 +138,10 @@ export class AlertsClient { const { references, actions } = await this.denormalizeActions(data.actions); const rawAlert: RawAlert = { ...data, + ...this.apiKeyAsAlertAttributes(await this.createAPIKey(), username), actions, createdBy: username, updatedBy: username, - apiKeyOwner: apiKey.created && username ? username : undefined, - apiKey: apiKey.created - ? Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64') - : undefined, params: validatedAlertTypeParams, muteAll: false, mutedInstanceIds: [], @@ -206,9 +207,28 @@ export class AlertsClient { } public async update({ id, data }: UpdateOptions) { - const { attributes, version } = await this.savedObjectsClient.get('alert', id); + const alert = await this.savedObjectsClient.get('alert', id); + const updateResult = await this.updateAlert({ id, data }, alert); + + if ( + updateResult.scheduledTaskId && + !isEqual(alert.attributes.schedule, updateResult.schedule) + ) { + this.taskManager.runNow(updateResult.scheduledTaskId).catch(err => { + this.logger.error( + `Alert update failed to run its underlying task. TaskManager runNow failed with Error: ${err.message}` + ); + }); + } + + return updateResult; + } + + private async updateAlert( + { id, data }: UpdateOptions, + { attributes, version }: SavedObject + ) { const alertType = this.alertTypeRegistry.get(attributes.alertTypeId); - const apiKey = await this.createAPIKey(); // Validate const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.params); @@ -216,19 +236,18 @@ export class AlertsClient { const { actions, references } = await this.denormalizeActions(data.actions); const username = await this.getUserName(); - const updatedObject = await this.savedObjectsClient.update( + const apiKeyAttributes = this.apiKeyAsAlertAttributes(await this.createAPIKey(), username); + + const updatedObject = await this.savedObjectsClient.update( 'alert', id, { ...attributes, ...data, + ...apiKeyAttributes, params: validatedAlertTypeParams, actions, updatedBy: username, - apiKeyOwner: apiKey.created ? username : null, - apiKey: apiKey.created - ? Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64') - : null, }, { version, @@ -238,21 +257,32 @@ export class AlertsClient { return this.getAlertFromRaw(id, updatedObject.attributes, updatedObject.references); } + private apiKeyAsAlertAttributes( + apiKey: CreateAPIKeyResult, + username: string | null + ): Pick { + return apiKey.created + ? { + apiKeyOwner: username, + apiKey: Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64'), + } + : { + apiKeyOwner: null, + apiKey: null, + }; + } + public async updateApiKey({ id }: { id: string }) { const { version, attributes } = await this.savedObjectsClient.get('alert', id); - const apiKey = await this.createAPIKey(); const username = await this.getUserName(); await this.savedObjectsClient.update( 'alert', id, { ...attributes, + ...this.apiKeyAsAlertAttributes(await this.createAPIKey(), username), updatedBy: username, - apiKeyOwner: apiKey.created ? username : null, - apiKey: apiKey.created - ? Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64') - : null, }, { version } ); @@ -261,7 +291,6 @@ export class AlertsClient { public async enable({ id }: { id: string }) { const { attributes, version } = await this.savedObjectsClient.get('alert', id); if (attributes.enabled === false) { - const apiKey = await this.createAPIKey(); const scheduledTask = await this.scheduleAlert(id, attributes.alertTypeId); const username = await this.getUserName(); await this.savedObjectsClient.update( @@ -270,12 +299,9 @@ export class AlertsClient { { ...attributes, enabled: true, + ...this.apiKeyAsAlertAttributes(await this.createAPIKey(), username), updatedBy: username, - apiKeyOwner: apiKey.created ? username : null, scheduledTaskId: scheduledTask.id, - apiKey: apiKey.created - ? Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64') - : null, }, { version } ); diff --git a/x-pack/legacy/plugins/alerting/server/constants/plugin.ts b/x-pack/legacy/plugins/alerting/server/constants/plugin.ts new file mode 100644 index 00000000000000..e3435b09829c61 --- /dev/null +++ b/x-pack/legacy/plugins/alerting/server/constants/plugin.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LICENSE_TYPE_BASIC, LicenseType } from '../../../../common/constants'; + +export const PLUGIN = { + ID: 'alerting', + MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_BASIC as LicenseType, // TODO: supposed to be changed up on requirements + getI18nName: (i18n: any): string => + i18n.translate('xpack.alerting.appName', { + defaultMessage: 'Alerting', + }), +}; diff --git a/x-pack/legacy/plugins/alerting/server/extend_route_with_license_check.test.ts b/x-pack/legacy/plugins/alerting/server/extend_route_with_license_check.test.ts new file mode 100644 index 00000000000000..05c8424881625a --- /dev/null +++ b/x-pack/legacy/plugins/alerting/server/extend_route_with_license_check.test.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { extendRouteWithLicenseCheck } from './extend_route_with_license_check'; +import { LicenseState } from './lib/license_state'; +jest.mock('./lib/license_state', () => ({ + verifyApiAccessFactory: () => {}, +})); + +describe('extendRouteWithLicenseCheck', () => { + describe('#actionsextendRouteWithLicenseCheck', () => { + let licenseState: jest.Mocked; + + test('extends route object with license, if config property already exists', () => { + const newRoute = extendRouteWithLicenseCheck( + { config: { someTestProperty: 'test' } }, + licenseState + ); + expect(newRoute.config.pre.length > 0); + }); + test('extends route object with license check under options.pre', () => { + const newRoute = extendRouteWithLicenseCheck( + { options: { someProperty: 'test' } }, + licenseState + ); + expect(newRoute.options.pre.length > 0); + }); + }); +}); diff --git a/x-pack/legacy/plugins/alerting/server/extend_route_with_license_check.ts b/x-pack/legacy/plugins/alerting/server/extend_route_with_license_check.ts new file mode 100644 index 00000000000000..f39dc125071b4d --- /dev/null +++ b/x-pack/legacy/plugins/alerting/server/extend_route_with_license_check.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; + * you may not use this file except in compliance with the Elastic License. + */ +import { LicenseState, verifyApiAccessFactory } from './lib/license_state'; + +export function extendRouteWithLicenseCheck(route: any, licenseState: LicenseState) { + const verifyApiAccessPreRouting = verifyApiAccessFactory(licenseState); + + const key = route.options ? 'options' : 'config'; + return { + ...route, + [key]: { + ...route[key], + pre: [verifyApiAccessPreRouting], + }, + }; +} diff --git a/x-pack/legacy/plugins/alerting/server/lib/create_execution_handler.ts b/x-pack/legacy/plugins/alerting/server/lib/create_execution_handler.ts index 5e7c91c560a6fc..3fb197ec97f4f8 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/create_execution_handler.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/create_execution_handler.ts @@ -14,7 +14,7 @@ interface CreateExecutionHandlerOptions { executeAction: ActionsPluginStartContract['execute']; actions: AlertAction[]; spaceId: string; - apiKey?: string; + apiKey: string | null; alertType: AlertType; logger: Logger; } diff --git a/x-pack/legacy/plugins/alerting/server/lib/license_state.test.ts b/x-pack/legacy/plugins/alerting/server/lib/license_state.test.ts new file mode 100644 index 00000000000000..484dc49532567d --- /dev/null +++ b/x-pack/legacy/plugins/alerting/server/lib/license_state.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { LicenseState } from './license_state'; +import { licensingMock } from '../../../../../plugins/licensing/server/mocks'; +import { LICENSE_CHECK_STATE } from '../../../../../plugins/licensing/server'; + +describe('license_state', () => { + let getRawLicense: any; + + beforeEach(() => { + getRawLicense = jest.fn(); + }); + + describe('status is LICENSE_STATUS_INVALID', () => { + beforeEach(() => { + const license = licensingMock.createLicense({ license: { status: 'invalid' } }); + license.check = jest.fn(() => ({ + state: LICENSE_CHECK_STATE.Invalid, + })); + getRawLicense.mockReturnValue(license); + }); + + it('check application link should be disabled', () => { + const licensing = licensingMock.createSetup(); + const licenseState = new LicenseState(licensing.license$); + const alertingLicenseInfo = licenseState.checkLicense(getRawLicense()); + expect(alertingLicenseInfo.enableAppLink).to.be(false); + }); + }); + + describe('status is LICENSE_STATUS_VALID', () => { + beforeEach(() => { + const license = licensingMock.createLicense({ license: { status: 'active' } }); + license.check = jest.fn(() => ({ + state: LICENSE_CHECK_STATE.Valid, + })); + getRawLicense.mockReturnValue(license); + }); + + it('check application link should be enabled', () => { + const licensing = licensingMock.createSetup(); + const licenseState = new LicenseState(licensing.license$); + const alertingLicenseInfo = licenseState.checkLicense(getRawLicense()); + expect(alertingLicenseInfo.enableAppLink).to.be(true); + }); + }); +}); diff --git a/x-pack/legacy/plugins/alerting/server/lib/license_state.ts b/x-pack/legacy/plugins/alerting/server/lib/license_state.ts new file mode 100644 index 00000000000000..344bc9c409edf3 --- /dev/null +++ b/x-pack/legacy/plugins/alerting/server/lib/license_state.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { i18n } from '@kbn/i18n'; +import { Observable, Subscription } from 'rxjs'; +import { ILicense, LICENSE_CHECK_STATE } from '../../../../../plugins/licensing/common/types'; +import { assertNever } from '../../../../../../src/core/utils'; +import { PLUGIN } from '../constants/plugin'; + +export interface AlertingLicenseInformation { + showAppLink: boolean; + enableAppLink: boolean; + message: string; +} + +export class LicenseState { + private licenseInformation: AlertingLicenseInformation = this.checkLicense(undefined); + private subscription: Subscription; + + constructor(license$: Observable) { + this.subscription = license$.subscribe(this.updateInformation.bind(this)); + } + + private updateInformation(license: ILicense | undefined) { + this.licenseInformation = this.checkLicense(license); + } + + public clean() { + this.subscription.unsubscribe(); + } + + public getLicenseInformation() { + return this.licenseInformation; + } + + public checkLicense(license: ILicense | undefined): AlertingLicenseInformation { + if (!license || !license.isAvailable) { + return { + showAppLink: true, + enableAppLink: false, + message: i18n.translate( + 'xpack.alerting.serverSideErrors.unavailableLicenseInformationErrorMessage', + { + defaultMessage: + 'Alerting is unavailable - license information is not available at this time.', + } + ), + }; + } + + const check = license.check(PLUGIN.ID, PLUGIN.MINIMUM_LICENSE_REQUIRED); + + switch (check.state) { + case LICENSE_CHECK_STATE.Expired: + return { + showAppLink: true, + enableAppLink: false, + message: check.message || '', + }; + case LICENSE_CHECK_STATE.Invalid: + case LICENSE_CHECK_STATE.Unavailable: + return { + showAppLink: false, + enableAppLink: false, + message: check.message || '', + }; + case LICENSE_CHECK_STATE.Valid: + return { + showAppLink: true, + enableAppLink: true, + message: '', + }; + default: + return assertNever(check.state); + } + } +} + +export function verifyApiAccessFactory(licenseState: LicenseState) { + function verifyApiAccess() { + const licenseCheckResults = licenseState.getLicenseInformation(); + + if (licenseCheckResults.showAppLink && licenseCheckResults.enableAppLink) { + return null; + } + + throw Boom.forbidden(licenseCheckResults.message); + } + return verifyApiAccess; +} diff --git a/x-pack/legacy/plugins/alerting/server/plugin.ts b/x-pack/legacy/plugins/alerting/server/plugin.ts index 32e57536873670..3b17fa066d55a1 100644 --- a/x-pack/legacy/plugins/alerting/server/plugin.ts +++ b/x-pack/legacy/plugins/alerting/server/plugin.ts @@ -10,6 +10,7 @@ import { Services } from './types'; import { AlertsClient } from './alerts_client'; import { AlertTypeRegistry } from './alert_type_registry'; import { AlertsClientFactory, TaskRunnerFactory } from './lib'; +import { LicenseState } from './lib/license_state'; import { IClusterClient, KibanaRequest, Logger } from '../../../../../src/core/server'; import { AlertingPluginInitializerContext, @@ -33,6 +34,7 @@ import { muteAlertInstanceRoute, unmuteAlertInstanceRoute, } from './routes'; +import { extendRouteWithLicenseCheck } from './extend_route_with_license_check'; export interface PluginSetupContract { registerType: AlertTypeRegistry['register']; @@ -48,6 +50,7 @@ export class Plugin { private readonly taskRunnerFactory: TaskRunnerFactory; private adminClient?: IClusterClient; private serverBasePath?: string; + private licenseState: LicenseState | null = null; constructor(initializerContext: AlertingPluginInitializerContext) { this.logger = initializerContext.logger.get('plugins', 'alerting'); @@ -60,6 +63,8 @@ export class Plugin { ): Promise { this.adminClient = await core.elasticsearch.adminClient$.pipe(first()).toPromise(); + this.licenseState = new LicenseState(plugins.licensing.license$); + // Encrypted attributes plugins.encryptedSavedObjects.registerType({ type: 'alert', @@ -80,19 +85,19 @@ export class Plugin { this.serverBasePath = core.http.basePath.serverBasePath; // Register routes - core.http.route(createAlertRoute); - core.http.route(deleteAlertRoute); - core.http.route(findAlertRoute); - core.http.route(getAlertRoute); - core.http.route(listAlertTypesRoute); - core.http.route(updateAlertRoute); - core.http.route(enableAlertRoute); - core.http.route(disableAlertRoute); - core.http.route(updateApiKeyRoute); - core.http.route(muteAllAlertRoute); - core.http.route(unmuteAllAlertRoute); - core.http.route(muteAlertInstanceRoute); - core.http.route(unmuteAlertInstanceRoute); + core.http.route(extendRouteWithLicenseCheck(createAlertRoute, this.licenseState)); + core.http.route(extendRouteWithLicenseCheck(deleteAlertRoute, this.licenseState)); + core.http.route(extendRouteWithLicenseCheck(findAlertRoute, this.licenseState)); + core.http.route(extendRouteWithLicenseCheck(getAlertRoute, this.licenseState)); + core.http.route(extendRouteWithLicenseCheck(listAlertTypesRoute, this.licenseState)); + core.http.route(extendRouteWithLicenseCheck(updateAlertRoute, this.licenseState)); + core.http.route(extendRouteWithLicenseCheck(enableAlertRoute, this.licenseState)); + core.http.route(extendRouteWithLicenseCheck(disableAlertRoute, this.licenseState)); + core.http.route(extendRouteWithLicenseCheck(updateApiKeyRoute, this.licenseState)); + core.http.route(extendRouteWithLicenseCheck(muteAllAlertRoute, this.licenseState)); + core.http.route(extendRouteWithLicenseCheck(unmuteAllAlertRoute, this.licenseState)); + core.http.route(extendRouteWithLicenseCheck(muteAlertInstanceRoute, this.licenseState)); + core.http.route(extendRouteWithLicenseCheck(unmuteAlertInstanceRoute, this.licenseState)); return { registerType: alertTypeRegistry.register.bind(alertTypeRegistry), @@ -140,4 +145,10 @@ export class Plugin { alertsClientFactory!.create(KibanaRequest.from(request), request), }; } + + public stop() { + if (this.licenseState) { + this.licenseState.clean(); + } + } } diff --git a/x-pack/legacy/plugins/alerting/server/shim.ts b/x-pack/legacy/plugins/alerting/server/shim.ts index ef1f1b41049e5e..a3d6b778b3a1fa 100644 --- a/x-pack/legacy/plugins/alerting/server/shim.ts +++ b/x-pack/legacy/plugins/alerting/server/shim.ts @@ -25,6 +25,7 @@ import { PluginSetupContract as ActionsPluginSetupContract, PluginStartContract as ActionsPluginStartContract, } from '../../actions'; +import { LicensingPluginSetup } from '../../../../plugins/licensing/server'; // Extend PluginProperties to indicate which plugins are guaranteed to exist // due to being marked as dependencies @@ -40,7 +41,10 @@ export interface Server extends Legacy.Server { /** * Shim what we're thinking setup and start contracts will look like */ -export type TaskManagerStartContract = Pick; +export type TaskManagerStartContract = Pick< + TaskManager, + 'schedule' | 'fetch' | 'remove' | 'runNow' +>; export type SecurityPluginSetupContract = Pick; export type SecurityPluginStartContract = Pick; export type XPackMainPluginSetupContract = Pick; @@ -73,6 +77,7 @@ export interface AlertingPluginsSetup { actions: ActionsPluginSetupContract; xpack_main: XPackMainPluginSetupContract; encryptedSavedObjects: EncryptedSavedObjectsSetupContract; + licensing: LicensingPluginSetup; } export interface AlertingPluginsStart { actions: ActionsPluginStartContract; @@ -121,6 +126,7 @@ export function shim( xpack_main: server.plugins.xpack_main, encryptedSavedObjects: newPlatform.setup.plugins .encryptedSavedObjects as EncryptedSavedObjectsSetupContract, + licensing: newPlatform.setup.plugins.licensing as LicensingPluginSetup, }; const pluginsStart: AlertingPluginsStart = { diff --git a/x-pack/legacy/plugins/alerting/server/test_utils/index.ts b/x-pack/legacy/plugins/alerting/server/test_utils/index.ts new file mode 100644 index 00000000000000..be9c5493ccf2bd --- /dev/null +++ b/x-pack/legacy/plugins/alerting/server/test_utils/index.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; + * you may not use this file except in compliance with the Elastic License. + */ + +interface Resolvable { + resolve: (arg?: T) => void; +} + +/** + * Creates a promise which can be resolved externally, useful for + * coordinating async tests. + */ +export function resolvable(): Promise & Resolvable { + let resolve: (arg?: T) => void; + const result = new Promise(r => { + resolve = r; + }) as any; + + result.resolve = (arg: T) => resolve(arg); + + return result; +} diff --git a/x-pack/legacy/plugins/alerting/server/types.ts b/x-pack/legacy/plugins/alerting/server/types.ts index f11c36adbeb644..7c2f3dcc918dc1 100644 --- a/x-pack/legacy/plugins/alerting/server/types.ts +++ b/x-pack/legacy/plugins/alerting/server/types.ts @@ -76,8 +76,8 @@ export interface Alert { scheduledTaskId?: string; createdBy: string | null; updatedBy: string | null; - apiKey?: string; - apiKeyOwner?: string; + apiKey: string | null; + apiKeyOwner: string | null; throttle: string | null; muteAll: boolean; mutedInstanceIds: string[]; @@ -95,8 +95,8 @@ export interface RawAlert extends SavedObjectAttributes { scheduledTaskId?: string; createdBy: string | null; updatedBy: string | null; - apiKey?: string; - apiKeyOwner?: string; + apiKey: string | null; + apiKeyOwner: string | null; throttle: string | null; muteAll: boolean; mutedInstanceIds: string[]; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts index de423e967b0b3c..f83daa4ea1a8a1 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts @@ -10,7 +10,7 @@ import uuid from 'uuid'; import * as rest from '../../../../../services/rest/watcher'; import { createErrorGroupWatch } from '../createErrorGroupWatch'; import { esResponse } from './esResponse'; -import { HttpServiceBase } from 'kibana/public'; +import { HttpSetup } from 'kibana/public'; // disable html escaping since this is also disabled in watcher\s mustache implementation mustache.escape = value => value; @@ -30,7 +30,7 @@ describe('createErrorGroupWatch', () => { jest.spyOn(uuid, 'v4').mockReturnValue(Buffer.from('mocked-uuid')); createWatchResponse = await createErrorGroupWatch({ - http: {} as HttpServiceBase, + http: {} as HttpSetup, emails: ['my@email.dk', 'mySecond@email.dk'], schedule: { daily: { diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts index 1d21e35f122d92..d45453e24f1c96 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import url from 'url'; import uuid from 'uuid'; -import { HttpServiceBase } from 'kibana/public'; +import { HttpSetup } from 'kibana/public'; import { ERROR_CULPRIT, ERROR_EXC_HANDLED, @@ -35,7 +35,7 @@ export interface Schedule { } interface Arguments { - http: HttpServiceBase; + http: HttpSetup; emails: string[]; schedule: Schedule; serviceName: string; diff --git a/x-pack/legacy/plugins/apm/public/services/__test__/callApi.test.ts b/x-pack/legacy/plugins/apm/public/services/__test__/callApi.test.ts index 31ba1e8d40aaa3..95ebed1fcb2a65 100644 --- a/x-pack/legacy/plugins/apm/public/services/__test__/callApi.test.ts +++ b/x-pack/legacy/plugins/apm/public/services/__test__/callApi.test.ts @@ -7,10 +7,10 @@ import { mockNow } from '../../utils/testHelpers'; import { clearCache, callApi } from '../rest/callApi'; import { SessionStorageMock } from './SessionStorageMock'; -import { HttpServiceBase } from 'kibana/public'; +import { HttpSetup } from 'kibana/public'; -type HttpMock = HttpServiceBase & { - get: jest.SpyInstance; +type HttpMock = HttpSetup & { + get: jest.SpyInstance; }; describe('callApi', () => { diff --git a/x-pack/legacy/plugins/apm/public/services/__test__/callApmApi.test.ts b/x-pack/legacy/plugins/apm/public/services/__test__/callApmApi.test.ts index e8a9fa74bd1da5..9cca9469bba0ed 100644 --- a/x-pack/legacy/plugins/apm/public/services/__test__/callApmApi.test.ts +++ b/x-pack/legacy/plugins/apm/public/services/__test__/callApmApi.test.ts @@ -6,7 +6,7 @@ import * as callApiExports from '../rest/callApi'; import { createCallApmApi, APMClient } from '../rest/createCallApmApi'; -import { HttpServiceBase } from 'kibana/public'; +import { HttpSetup } from 'kibana/public'; const callApi = jest .spyOn(callApiExports, 'callApi') @@ -15,7 +15,7 @@ const callApi = jest describe('callApmApi', () => { let callApmApi: APMClient; beforeEach(() => { - callApmApi = createCallApmApi({} as HttpServiceBase); + callApmApi = createCallApmApi({} as HttpSetup); }); afterEach(() => { diff --git a/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts b/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts index 853ba5023f6fd1..43ecb860a1f1af 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts @@ -7,7 +7,7 @@ import { isString, startsWith } from 'lodash'; import LRU from 'lru-cache'; import hash from 'object-hash'; -import { HttpServiceBase, HttpFetchOptions } from 'kibana/public'; +import { HttpSetup, HttpFetchOptions } from 'kibana/public'; export type FetchOptions = Omit & { pathname: string; @@ -42,7 +42,7 @@ export function clearCache() { export type CallApi = typeof callApi; export async function callApi( - http: HttpServiceBase, + http: HttpSetup, fetchOptions: FetchOptions ): Promise { const cacheKey = getCacheKey(fetchOptions); diff --git a/x-pack/legacy/plugins/apm/public/services/rest/createCallApmApi.ts b/x-pack/legacy/plugins/apm/public/services/rest/createCallApmApi.ts index 964cc12794075f..b4d060adec5a1b 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/createCallApmApi.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/createCallApmApi.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { HttpServiceBase } from 'kibana/public'; +import { HttpSetup } from 'kibana/public'; import { callApi, FetchOptions } from './callApi'; import { APMAPI } from '../../../server/routes/create_apm_api'; import { Client } from '../../../server/routes/typings'; @@ -17,7 +17,7 @@ export type APMClientOptions = Omit & { }; }; -export const createCallApmApi = (http: HttpServiceBase) => +export const createCallApmApi = (http: HttpSetup) => ((options: APMClientOptions) => { const { pathname, params = {}, ...opts } = options; diff --git a/x-pack/legacy/plugins/apm/public/services/rest/index_pattern.ts b/x-pack/legacy/plugins/apm/public/services/rest/index_pattern.ts index 477a9f96cc4fb9..8e1234dd55e69b 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/index_pattern.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/index_pattern.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HttpServiceBase } from 'kibana/public'; +import { HttpSetup } from 'kibana/public'; import { createCallApmApi } from './createCallApmApi'; -export const createStaticIndexPattern = async (http: HttpServiceBase) => { +export const createStaticIndexPattern = async (http: HttpSetup) => { const callApmApi = createCallApmApi(http); return await callApmApi({ method: 'POST', diff --git a/x-pack/legacy/plugins/apm/public/services/rest/ml.ts b/x-pack/legacy/plugins/apm/public/services/rest/ml.ts index e495a8968a7f37..e42b9536362e03 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/ml.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/ml.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HttpServiceBase } from 'kibana/public'; +import { HttpSetup } from 'kibana/public'; import { PROCESSOR_EVENT, SERVICE_NAME, @@ -32,7 +32,7 @@ interface StartedMLJobApiResponse { jobs: MlResponseItem[]; } -async function getTransactionIndices(http: HttpServiceBase) { +async function getTransactionIndices(http: HttpSetup) { const callApmApi: APMClient = createCallApmApi(http); const indices = await callApmApi({ method: 'GET', @@ -48,7 +48,7 @@ export async function startMLJob({ }: { serviceName: string; transactionType: string; - http: HttpServiceBase; + http: HttpSetup; }) { const transactionIndices = await getTransactionIndices(http); const groups = ['apm', serviceName.toLowerCase()]; @@ -90,7 +90,7 @@ export async function getHasMLJob({ }: { serviceName: string; transactionType: string; - http: HttpServiceBase; + http: HttpSetup; }) { try { await callApi(http, { diff --git a/x-pack/legacy/plugins/apm/public/services/rest/watcher.ts b/x-pack/legacy/plugins/apm/public/services/rest/watcher.ts index dfa64b5368ee96..259f2af33ba9a6 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/watcher.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/watcher.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HttpServiceBase } from 'kibana/public'; +import { HttpSetup } from 'kibana/public'; import { callApi } from './callApi'; export async function createWatch({ @@ -12,7 +12,7 @@ export async function createWatch({ watch, http }: { - http: HttpServiceBase; + http: HttpSetup; id: string; watch: any; }) { diff --git a/x-pack/legacy/plugins/canvas/public/lib/loading_indicator.ts b/x-pack/legacy/plugins/canvas/public/lib/loading_indicator.ts index a95f4278b6a699..4b9e548d5c7183 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/loading_indicator.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/loading_indicator.ts @@ -16,7 +16,7 @@ export interface LoadingIndicatorInterface { const loadingCount$ = new Rx.BehaviorSubject(0); -export const initLoadingIndicator = (addLoadingCount: CoreStart['http']['addLoadingCount']) => +export const initLoadingIndicator = (addLoadingCount: CoreStart['http']['addLoadingCountSource']) => addLoadingCount(loadingCount$); export const loadingIndicator = { diff --git a/x-pack/legacy/plugins/canvas/public/plugin.tsx b/x-pack/legacy/plugins/canvas/public/plugin.tsx index 5190e8521101ba..9828845d9ffa92 100644 --- a/x-pack/legacy/plugins/canvas/public/plugin.tsx +++ b/x-pack/legacy/plugins/canvas/public/plugin.tsx @@ -77,7 +77,7 @@ export class CanvasPlugin initLocationProvider(core, plugins); initStore(core, plugins); initClipboard(plugins.__LEGACY.storage); - initLoadingIndicator(core.http.addLoadingCount); + initLoadingIndicator(core.http.addLoadingCountSource); const CanvasRootController = CanvasRootControllerFactory(core, plugins); plugins.__LEGACY.setRootController('canvas', CanvasRootController); diff --git a/x-pack/legacy/plugins/index_management/public/app/components/template_form/steps/step_review.tsx b/x-pack/legacy/plugins/index_management/public/app/components/template_form/steps/step_review.tsx index 8f2d3978a67806..09172bf5cd0caa 100644 --- a/x-pack/legacy/plugins/index_management/public/app/components/template_form/steps/step_review.tsx +++ b/x-pack/legacy/plugins/index_management/public/app/components/template_form/steps/step_review.tsx @@ -163,6 +163,10 @@ export const StepReview: React.FunctionComponent = ({ template, updat const templateString = JSON.stringify(serializedTemplate, null, 2); const request = `${endpoint}\n${templateString}`; + // Beyond a certain point, highlighting the syntax will bog down performance to unacceptable + // levels. This way we prevent that happening for very large requests. + const language = request.length < 60000 ? 'json' : undefined; + return (
@@ -178,7 +182,7 @@ export const StepReview: React.FunctionComponent = ({ template, updat - + {request}
diff --git a/x-pack/legacy/plugins/index_management/public/app/services/http.ts b/x-pack/legacy/plugins/index_management/public/app/services/http.ts index d88bd52405495a..34f5abe5983cc2 100644 --- a/x-pack/legacy/plugins/index_management/public/app/services/http.ts +++ b/x-pack/legacy/plugins/index_management/public/app/services/http.ts @@ -4,16 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HttpServiceBase } from '../../../../../../../src/core/public'; +import { HttpSetup } from '../../../../../../../src/core/public'; class HttpService { private client: any; - public init(httpClient: HttpServiceBase): void { + public init(httpClient: HttpSetup): void { this.client = httpClient; } - public get httpClient(): HttpServiceBase { + public get httpClient(): HttpSetup { return this.client; } } diff --git a/x-pack/legacy/plugins/infra/index.ts b/x-pack/legacy/plugins/infra/index.ts index 38661f430c4020..e03b91c58f4a39 100644 --- a/x-pack/legacy/plugins/infra/index.ts +++ b/x-pack/legacy/plugins/infra/index.ts @@ -16,6 +16,7 @@ import { plugin, InfraServerPluginDeps } from './server/new_platform_index'; import { InfraSetup } from '../../../plugins/infra/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../../plugins/features/server'; import { SpacesPluginSetup } from '../../../plugins/spaces/server'; +import { VisTypeTimeseriesSetup } from '../../../../src/plugins/vis_type_timeseries/server'; import { APMPluginContract } from '../../../plugins/apm/server'; const APP_ID = 'infra'; @@ -92,19 +93,9 @@ export function infra(kibana: any) { indexPatterns: { indexPatternsServiceFactory: legacyServer.indexPatternsServiceFactory, }, - metrics: legacyServer.plugins.metrics, + metrics: plugins.metrics as VisTypeTimeseriesSetup, spaces: plugins.spaces as SpacesPluginSetup, features: plugins.features as FeaturesPluginSetup, - // NP_NOTE: [TSVB_GROUP] Huge hack to make TSVB (getVisData()) work with raw requests that - // originate from the New Platform router (and are very different to the old request object). - // Once TSVB has migrated over to NP, and can work with the new raw requests, or ideally just - // the requestContext, this can be removed. - ___legacy: { - tsvb: { - elasticsearch: legacyServer.plugins.elasticsearch, - __internals: legacyServer.newPlatform.__internals, - }, - }, apm: plugins.apm as APMPluginContract, }; diff --git a/x-pack/legacy/plugins/infra/public/app.ts b/x-pack/legacy/plugins/infra/public/app.ts index 255c51c9e48cea..4b14e168eb7683 100644 --- a/x-pack/legacy/plugins/infra/public/app.ts +++ b/x-pack/legacy/plugins/infra/public/app.ts @@ -4,4 +4,51 @@ * you may not use this file except in compliance with the Elastic License. */ -import './apps/kibana_app'; +// NP_TODO: This app.ts layer is needed until we migrate 100% to the NP. +// This is so other plugins can import from our public/index file without trying to +// actually mount and run our application. Once in the NP this won't be an issue +// as the NP will look for an export named "plugin" and run that from the index file. + +import { npStart } from 'ui/new_platform'; +import { PluginInitializerContext } from 'kibana/public'; +import chrome from 'ui/chrome'; +// @ts-ignore +import { uiModules } from 'ui/modules'; +import uiRoutes from 'ui/routes'; +// @ts-ignore +import { timezoneProvider } from 'ui/vis/lib/timezone'; +import { plugin } from './new_platform_index'; + +const ROOT_ELEMENT_ID = 'react-infra-root'; +export { ROOT_ELEMENT_ID }; + +const { core, plugins } = npStart; +const __LEGACY = { + uiModules, + uiRoutes, + timezoneProvider, +}; +// This will be moved to core.application.register when the new platform +// migration is complete. +// @ts-ignore +chrome.setRootTemplate(` +
+`); + +const checkForRoot = () => { + return new Promise(resolve => { + const ready = !!document.getElementById(ROOT_ELEMENT_ID); + if (ready) { + resolve(); + } else { + setTimeout(() => resolve(checkForRoot()), 10); + } + }); +}; + +checkForRoot().then(() => { + plugin({} as PluginInitializerContext).start(core, plugins, __LEGACY); +}); diff --git a/x-pack/legacy/plugins/infra/public/apps/start_app.tsx b/x-pack/legacy/plugins/infra/public/apps/start_app.tsx index 41479cf6351ecd..8ccb051724ede5 100644 --- a/x-pack/legacy/plugins/infra/public/apps/start_app.tsx +++ b/x-pack/legacy/plugins/infra/public/apps/start_app.tsx @@ -6,16 +6,15 @@ import { createHashHistory } from 'history'; import React from 'react'; +import ReactDOM from 'react-dom'; import { ApolloProvider } from 'react-apollo'; import { Provider as ReduxStoreProvider } from 'react-redux'; import { BehaviorSubject } from 'rxjs'; import { pluck } from 'rxjs/operators'; +import { CoreStart } from 'kibana/public'; // TODO use theme provided from parentApp when kibana supports it import { EuiErrorBoundary } from '@elastic/eui'; -import { UICapabilitiesProvider } from 'ui/capabilities/react'; -import { I18nContext } from 'ui/i18n'; -import { npStart } from 'ui/new_platform'; import { EuiThemeProvider } from '../../../../common/eui_styled_components'; import { InfraFrontendLibs } from '../lib/lib'; import { PageRouter } from '../routes'; @@ -27,12 +26,10 @@ import { useUiSetting$, KibanaContextProvider, } from '../../../../../../src/plugins/kibana_react/public'; - -const { uiSettings } = npStart.core; - -export async function startApp(libs: InfraFrontendLibs) { +import { ROOT_ELEMENT_ID } from '../app'; +// NP_TODO: Type plugins +export async function startApp(libs: InfraFrontendLibs, core: CoreStart, plugins: any) { const history = createHashHistory(); - const libs$ = new BehaviorSubject(libs); const store = createStore({ apolloClient: libs$.pipe(pluck('apolloClient')), @@ -43,31 +40,35 @@ export async function startApp(libs: InfraFrontendLibs) { const [darkMode] = useUiSetting$('theme:darkMode'); return ( - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + ); }; - libs.framework.render( - + const node = await document.getElementById(ROOT_ELEMENT_ID); + + const App = ( + ); + + if (node) { + ReactDOM.render(App, node); + } } diff --git a/x-pack/legacy/plugins/infra/public/components/header/external_header.tsx b/x-pack/legacy/plugins/infra/public/components/header/external_header.tsx deleted file mode 100644 index 4649241d2a0c8f..00000000000000 --- a/x-pack/legacy/plugins/infra/public/components/header/external_header.tsx +++ /dev/null @@ -1,47 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import isEqual from 'lodash/fp/isEqual'; -import React from 'react'; - -import { Badge } from 'ui/chrome/api/badge'; -import { Breadcrumb } from 'ui/chrome/api/breadcrumbs'; - -interface ExternalHeaderProps { - breadcrumbs?: Breadcrumb[]; - setBreadcrumbs: (breadcrumbs: Breadcrumb[]) => void; - badge: Badge | undefined; - setBadge: (badge: Badge | undefined) => void; -} - -export class ExternalHeader extends React.Component { - public componentDidMount() { - this.setBreadcrumbs(); - this.setBadge(); - } - - public componentDidUpdate(prevProps: ExternalHeaderProps) { - if (!isEqual(this.props.breadcrumbs, prevProps.breadcrumbs)) { - this.setBreadcrumbs(); - } - - if (!isEqual(this.props.badge, prevProps.badge)) { - this.setBadge(); - } - } - - public render() { - return null; - } - - private setBadge = () => { - this.props.setBadge(this.props.badge); - }; - - private setBreadcrumbs = () => { - this.props.setBreadcrumbs(this.props.breadcrumbs || []); - }; -} diff --git a/x-pack/legacy/plugins/infra/public/components/header/header.tsx b/x-pack/legacy/plugins/infra/public/components/header/header.tsx index 5ff8472e94ddcb..731e62b927ae4e 100644 --- a/x-pack/legacy/plugins/infra/public/components/header/header.tsx +++ b/x-pack/legacy/plugins/infra/public/components/header/header.tsx @@ -4,40 +4,51 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; - +import { useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; - -import { Breadcrumb } from 'ui/chrome/api/breadcrumbs'; -import { WithKibanaChrome } from '../../containers/with_kibana_chrome'; -import { ExternalHeader } from './external_header'; +import { ChromeBreadcrumb } from 'src/core/public'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; interface HeaderProps { - breadcrumbs?: Breadcrumb[]; + breadcrumbs?: ChromeBreadcrumb[]; readOnlyBadge?: boolean; } -export const Header = ({ breadcrumbs = [], readOnlyBadge = false }: HeaderProps) => ( - - {({ setBreadcrumbs, setBadge }) => ( - - )} - -); +export const Header = ({ breadcrumbs = [], readOnlyBadge = false }: HeaderProps) => { + const chrome = useKibana().services.chrome; + + const badge = readOnlyBadge + ? { + text: i18n.translate('xpack.infra.header.badge.readOnly.text', { + defaultMessage: 'Read only', + }), + tooltip: i18n.translate('xpack.infra.header.badge.readOnly.tooltip', { + defaultMessage: 'Unable to change source configuration', + }), + iconType: 'glasses', + } + : undefined; + + const setBreadcrumbs = useCallback(() => { + return chrome?.setBreadcrumbs(breadcrumbs || []); + }, [breadcrumbs, chrome]); + + const setBadge = useCallback(() => { + return chrome?.setBadge(badge); + }, [badge, chrome]); + + useEffect(() => { + setBreadcrumbs(); + setBadge(); + }, [setBreadcrumbs, setBadge]); + + useEffect(() => { + setBreadcrumbs(); + }, [breadcrumbs, setBreadcrumbs]); + + useEffect(() => { + setBadge(); + }, [badge, setBadge]); + + return null; +}; diff --git a/x-pack/legacy/plugins/infra/public/components/help_center_content.tsx b/x-pack/legacy/plugins/infra/public/components/help_center_content.tsx index 3095230ab8311c..4b27c6cfce53f7 100644 --- a/x-pack/legacy/plugins/infra/public/components/help_center_content.tsx +++ b/x-pack/legacy/plugins/infra/public/components/help_center_content.tsx @@ -5,7 +5,7 @@ */ import React, { useEffect } from 'react'; -import chrome from 'ui/chrome'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; interface HelpCenterContentProps { feedbackLink: string; @@ -13,8 +13,10 @@ interface HelpCenterContentProps { } export const HelpCenterContent: React.FC = ({ feedbackLink, appName }) => { + const chrome = useKibana().services.chrome; + useEffect(() => { - chrome.helpExtension.set({ + return chrome?.setHelpExtension({ appName, links: [ { @@ -23,7 +25,7 @@ export const HelpCenterContent: React.FC = ({ feedbackLi }, ], }); - }, [feedbackLink, appName]); + }, [feedbackLink, appName, chrome]); return null; }; diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_results/analyze_in_ml_button.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_results/analyze_in_ml_button.tsx index c5d83e1c205ccc..536dd24faa7c16 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_results/analyze_in_ml_button.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_results/analyze_in_ml_button.tsx @@ -8,10 +8,9 @@ import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { encode } from 'rison-node'; -import chrome from 'ui/chrome'; import { QueryString } from 'ui/utils/query_string'; import url from 'url'; - +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { TimeRange } from '../../../../common/http_api/shared/time_range'; export const AnalyzeInMlButton: React.FunctionComponent<{ @@ -19,7 +18,11 @@ export const AnalyzeInMlButton: React.FunctionComponent<{ partition?: string; timeRange: TimeRange; }> = ({ jobId, partition, timeRange }) => { - const pathname = chrome.addBasePath('/app/ml'); + const prependBasePath = useKibana().services.http?.basePath?.prepend; + if (!prependBasePath) { + return null; + } + const pathname = prependBasePath('/app/ml'); const buttonLabel = ( = ({ logItem }) => { + const prependBasePath = useKibana().services.http?.basePath?.prepend; const { hide, isVisible, show } = useVisibilityState(false); - const uptimeLink = useMemo(() => getUptimeLink(logItem), [logItem]); + const uptimeLink = useMemo(() => { + const link = getUptimeLink(logItem); + return prependBasePath && link ? prependBasePath(link) : link; + }, [logItem, prependBasePath]); - const apmLink = useMemo(() => getAPMLink(logItem), [logItem]); + const apmLink = useMemo(() => { + const link = getAPMLink(logItem); + return prependBasePath && link ? prependBasePath(link) : link; + }, [logItem, prependBasePath]); const menuItems = useMemo( () => [ @@ -56,7 +62,6 @@ export const LogEntryActionsMenu: React.FunctionComponent<{ ); const hasMenuItems = useMemo(() => menuItems.length > 0, [menuItems]); - return ( { } return url.format({ - pathname: chrome.addBasePath('/app/uptime'), + pathname: '/app/uptime', hash: `/?search=(${searchExpressions.join(' OR ')})`, }); }; @@ -123,7 +128,7 @@ const getAPMLink = (logItem: InfraLogItem) => { : { rangeFrom: 'now-1y', rangeTo: 'now' }; return url.format({ - pathname: chrome.addBasePath('/app/apm'), + pathname: '/app/apm', hash: getTraceUrl({ traceId: traceIdEntry.value, rangeFrom, rangeTo }), }); }; diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart.tsx index 2759ea8124c913..a700a455cd2bb1 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart.tsx @@ -18,8 +18,6 @@ import { } from '@elastic/charts'; import { first, last } from 'lodash'; import moment from 'moment'; -import { UICapabilities } from 'ui/capabilities'; -import { injectUICapabilities } from 'ui/capabilities/react'; import { MetricsExplorerSeries } from '../../../server/routes/metrics_explorer/types'; import { MetricsExplorerOptions, @@ -37,6 +35,7 @@ import { MetricsExplorerNoMetrics } from './no_metrics'; import { getChartTheme } from './helpers/get_chart_theme'; import { useKibanaUiSetting } from '../../utils/use_kibana_ui_setting'; import { calculateDomain } from './helpers/calculate_domain'; +import { useKibana, useUiSetting } from '../../../../../../../src/plugins/kibana_react/public'; interface Props { title?: string | null; @@ -49,132 +48,130 @@ interface Props { source: SourceQuery.Query['source']['configuration'] | undefined; timeRange: MetricsExplorerTimeOptions; onTimeChange: (start: string, end: string) => void; - uiCapabilities: UICapabilities; } -export const MetricsExplorerChart = injectUICapabilities( - ({ - source, - options, - chartOptions, - series, - title, - onFilter, - height = 200, - width = '100%', - timeRange, - onTimeChange, - uiCapabilities, - }: Props) => { - const { metrics } = options; - const [dateFormat] = useKibanaUiSetting('dateFormat'); - const handleTimeChange = (from: number, to: number) => { - onTimeChange(moment(from).toISOString(), moment(to).toISOString()); - }; - const dateFormatter = useMemo( - () => - series.rows.length > 0 - ? niceTimeFormatter([first(series.rows).timestamp, last(series.rows).timestamp]) - : (value: number) => `${value}`, - [series.rows] - ); - const tooltipProps = { - headerFormatter: useCallback( - (data: TooltipValue) => moment(data.value).format(dateFormat || 'Y-MM-DD HH:mm:ss.SSS'), - [dateFormat] - ), - }; - const yAxisFormater = useCallback(createFormatterForMetric(first(metrics)), [options]); - const dataDomain = calculateDomain(series, metrics, chartOptions.stack); - const domain = - chartOptions.yAxisMode === MetricsExplorerYAxisMode.fromZero - ? { ...dataDomain, min: 0 } - : dataDomain; - return ( -
- {options.groupBy ? ( - - - - - {title} - - - - - - - - ) : ( - +export const MetricsExplorerChart = ({ + source, + options, + chartOptions, + series, + title, + onFilter, + height = 200, + width = '100%', + timeRange, + onTimeChange, +}: Props) => { + const uiCapabilities = useKibana().services.application?.capabilities; + const isDarkMode = useUiSetting('theme:darkMode'); + const { metrics } = options; + const [dateFormat] = useKibanaUiSetting('dateFormat'); + const handleTimeChange = (from: number, to: number) => { + onTimeChange(moment(from).toISOString(), moment(to).toISOString()); + }; + const dateFormatter = useMemo( + () => + series.rows.length > 0 + ? niceTimeFormatter([first(series.rows).timestamp, last(series.rows).timestamp]) + : (value: number) => `${value}`, + [series.rows] + ); + const tooltipProps = { + headerFormatter: useCallback( + (data: TooltipValue) => moment(data.value).format(dateFormat || 'Y-MM-DD HH:mm:ss.SSS'), + [dateFormat] + ), + }; + const yAxisFormater = useCallback(createFormatterForMetric(first(metrics)), [options]); + const dataDomain = calculateDomain(series, metrics, chartOptions.stack); + const domain = + chartOptions.yAxisMode === MetricsExplorerYAxisMode.fromZero + ? { ...dataDomain, min: 0 } + : dataDomain; + return ( +
+ {options.groupBy ? ( + + + + + {title} + + - )} -
- {series.rows.length > 0 ? ( - - {metrics.map((metric, id) => ( - - ))} - - - + ) : ( + + + + + + )} +
+ {series.rows.length > 0 ? ( + + {metrics.map((metric, id) => ( + - - ) : options.metrics.length > 0 ? ( - - ) : ( - - )} -
+ ))} + + + +
+ ) : options.metrics.length > 0 ? ( + + ) : ( + + )}
- ); - } -); +
+ ); +}; const ChartTitle = euiStyled.div` - width: 100% - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - text-align: left; - flex: 1 1 auto; - margin: 12px; - `; + width: 100% + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: left; + flex: 1 1 auto; + margin: 12px; +`; diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart_context_menu.test.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart_context_menu.test.tsx index 2c700d40534891..6c044baa56d7ce 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart_context_menu.test.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart_context_menu.test.tsx @@ -8,13 +8,13 @@ import React from 'react'; import { MetricsExplorerChartContextMenu, createNodeDetailLink } from './chart_context_menu'; import { mount } from 'enzyme'; import { options, source, timeRange, chartOptions } from '../../utils/fixtures/metrics_explorer'; -import { UICapabilities } from 'ui/capabilities'; import { InfraNodeType } from '../../graphql/types'; import DateMath from '@elastic/datemath'; import { ReactWrapper } from 'enzyme'; +import { Capabilities } from 'src/core/public'; const series = { id: 'exmaple-01', rows: [], columns: [] }; -const uiCapabilities: UICapabilities = { +const uiCapabilities: Capabilities = { navLinks: { show: false }, management: { fake: { show: false } }, catalogue: { show: false }, diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart_context_menu.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart_context_menu.tsx index aa3616c5ecfbee..597386fc24ee84 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart_context_menu.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/chart_context_menu.tsx @@ -12,8 +12,8 @@ import { EuiContextMenuPanelDescriptor, EuiPopover, } from '@elastic/eui'; -import { UICapabilities } from 'ui/capabilities'; import DateMath from '@elastic/datemath'; +import { Capabilities } from 'src/core/public'; import { MetricsExplorerSeries } from '../../../server/routes/metrics_explorer/types'; import { MetricsExplorerOptions, @@ -31,7 +31,7 @@ interface Props { series: MetricsExplorerSeries; source?: SourceConfiguration; timeRange: MetricsExplorerTimeOptions; - uiCapabilities: UICapabilities; + uiCapabilities?: Capabilities; chartOptions: MetricsExplorerChartOptions; } @@ -118,7 +118,7 @@ export const MetricsExplorerChartContextMenu = ({ ] : []; - const openInVisualize = uiCapabilities.visualize.show + const openInVisualize = uiCapabilities?.visualize?.show ? [ { name: i18n.translate('xpack.infra.metricsExplorer.openInTSVB', { diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/group_by.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/group_by.tsx index 568d7688002590..505966e62e45f8 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/group_by.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/group_by.tsx @@ -8,14 +8,14 @@ import { EuiComboBox } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useCallback } from 'react'; -import { FieldType } from 'ui/index_patterns'; +import { IFieldType } from 'src/plugins/data/public'; import { MetricsExplorerOptions } from '../../containers/metrics_explorer/use_metrics_explorer_options'; import { isDisplayable } from '../../utils/is_displayable'; interface Props { options: MetricsExplorerOptions; onChange: (groupBy: string | null) => void; - fields: FieldType[]; + fields: IFieldType[]; } export const MetricsExplorerGroupBy = ({ options, onChange, fields }: Props) => { diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/helpers/get_chart_theme.ts b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/helpers/get_chart_theme.ts index 7be2e54d47d75d..42469ffb5ee9a1 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/helpers/get_chart_theme.ts +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/helpers/get_chart_theme.ts @@ -4,10 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; import { Theme, LIGHT_THEME, DARK_THEME } from '@elastic/charts'; -export function getChartTheme(): Theme { - const isDarkMode = chrome.getUiSettingsClient().get('theme:darkMode'); +export function getChartTheme(isDarkMode: boolean): Theme { return isDarkMode ? DARK_THEME : LIGHT_THEME; } diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/metrics.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/metrics.tsx index 31d301e2164f87..7a8b22467ccd80 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/metrics.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/metrics.tsx @@ -8,7 +8,7 @@ import { EuiComboBox } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useCallback, useState } from 'react'; -import { FieldType } from 'ui/index_patterns'; +import { IFieldType } from 'src/plugins/data/public'; import { colorTransformer, MetricsExplorerColor } from '../../../common/color_palette'; import { MetricsExplorerMetric } from '../../../server/routes/metrics_explorer/types'; import { MetricsExplorerOptions } from '../../containers/metrics_explorer/use_metrics_explorer_options'; @@ -18,7 +18,7 @@ interface Props { autoFocus?: boolean; options: MetricsExplorerOptions; onChange: (metrics: MetricsExplorerMetric[]) => void; - fields: FieldType[]; + fields: IFieldType[]; } interface SelectedOption { diff --git a/x-pack/legacy/plugins/infra/public/components/saved_views/toolbar_control.tsx b/x-pack/legacy/plugins/infra/public/components/saved_views/toolbar_control.tsx index ae04ede9fbe0c7..e03b7fcc8ffa42 100644 --- a/x-pack/legacy/plugins/infra/public/components/saved_views/toolbar_control.tsx +++ b/x-pack/legacy/plugins/infra/public/components/saved_views/toolbar_control.tsx @@ -7,11 +7,12 @@ import { EuiButtonEmpty, EuiFlexGroup } from '@elastic/eui'; import React, { useCallback, useState, useEffect } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; import { useSavedView } from '../../hooks/use_saved_view'; import { SavedViewCreateModal } from './create_modal'; import { SavedViewListFlyout } from './view_list_flyout'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; + interface Props { viewType: string; viewState: ViewState; @@ -20,6 +21,7 @@ interface Props { } export function SavedViewsToolbarControls(props: Props) { + const kibana = useKibana(); const { views, saveView, @@ -77,11 +79,11 @@ export function SavedViewsToolbarControls(props: Props) { useEffect(() => { if (errorOnCreate) { - toastNotifications.addWarning(getErrorToast('create', errorOnCreate)!); + kibana.notifications.toasts.warning(getErrorToast('create', errorOnCreate)!); } else if (errorOnFind) { - toastNotifications.addWarning(getErrorToast('find', errorOnFind)!); + kibana.notifications.toasts.warning(getErrorToast('find', errorOnFind)!); } - }, [errorOnCreate, errorOnFind]); + }, [errorOnCreate, errorOnFind, kibana]); return ( <> @@ -119,6 +121,7 @@ export function SavedViewsToolbarControls(props: Props) { const getErrorToast = (type: 'create' | 'find', msg?: string) => { if (type === 'create') { return { + toastLifeTimeMs: 3000, title: msg || i18n.translate('xpack.infra.savedView.errorOnCreate.title', { @@ -127,6 +130,7 @@ const getErrorToast = (type: 'create' | 'find', msg?: string) => { }; } else if (type === 'find') { return { + toastLifeTimeMs: 3000, title: msg || i18n.translate('xpack.infra.savedView.findError.title', { diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/custom_field_panel.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/custom_field_panel.tsx index 1bfbf69b434bdf..01bff0b4f96e1e 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/custom_field_panel.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/custom_field_panel.tsx @@ -6,12 +6,12 @@ import { EuiButton, EuiComboBox, EuiForm, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; - import React from 'react'; -import { FieldType } from 'ui/index_patterns'; +import { IFieldType } from 'src/plugins/data/public'; + interface Props { onSubmit: (field: string) => void; - fields: FieldType[]; + fields: IFieldType[]; } interface SelectedOption { diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/node_context_menu.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/node_context_menu.tsx index 43f0afb125d2b8..cb894f37c1fce4 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/node_context_menu.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/node_context_menu.tsx @@ -13,13 +13,12 @@ import { import { i18n } from '@kbn/i18n'; import React from 'react'; -import { UICapabilities } from 'ui/capabilities'; -import { injectUICapabilities } from 'ui/capabilities/react'; import { InfraNodeType } from '../../graphql/types'; import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../lib/lib'; import { getNodeDetailUrl, getNodeLogsUrl } from '../../pages/link_to'; import { createUptimeLink } from './lib/create_uptime_link'; import { findInventoryModel } from '../../../common/inventory_models'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; interface Props { options: InfraWaffleMapOptions; @@ -29,102 +28,99 @@ interface Props { nodeType: InfraNodeType; isPopoverOpen: boolean; closePopover: () => void; - uiCapabilities: UICapabilities; popoverPosition: EuiPopoverProps['anchorPosition']; } -export const NodeContextMenu = injectUICapabilities( - ({ - options, - currentTime, - children, - node, - isPopoverOpen, - closePopover, - nodeType, - uiCapabilities, - popoverPosition, - }: Props) => { - const inventoryModel = findInventoryModel(nodeType); - // Due to the changing nature of the fields between APM and this UI, - // We need to have some exceptions until 7.0 & ECS is finalized. Reference - // #26620 for the details for these fields. - // TODO: This is tech debt, remove it after 7.0 & ECS migration. - const apmField = nodeType === InfraNodeType.host ? 'host.hostname' : inventoryModel.fields.id; +export const NodeContextMenu = ({ + options, + currentTime, + children, + node, + isPopoverOpen, + closePopover, + nodeType, + popoverPosition, +}: Props) => { + const uiCapabilities = useKibana().services.application?.capabilities; + const inventoryModel = findInventoryModel(nodeType); + // Due to the changing nature of the fields between APM and this UI, + // We need to have some exceptions until 7.0 & ECS is finalized. Reference + // #26620 for the details for these fields. + // TODO: This is tech debt, remove it after 7.0 & ECS migration. + const apmField = nodeType === InfraNodeType.host ? 'host.hostname' : inventoryModel.fields.id; - const nodeLogsMenuItem = { - name: i18n.translate('xpack.infra.nodeContextMenu.viewLogsName', { - defaultMessage: 'View logs', - }), - href: getNodeLogsUrl({ - nodeType, - nodeId: node.id, - time: currentTime, - }), - 'data-test-subj': 'viewLogsContextMenuItem', - }; + const nodeLogsMenuItem = { + name: i18n.translate('xpack.infra.nodeContextMenu.viewLogsName', { + defaultMessage: 'View logs', + }), + href: getNodeLogsUrl({ + nodeType, + nodeId: node.id, + time: currentTime, + }), + 'data-test-subj': 'viewLogsContextMenuItem', + }; - const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000; - const nodeDetailMenuItem = { - name: i18n.translate('xpack.infra.nodeContextMenu.viewMetricsName', { - defaultMessage: 'View metrics', - }), - href: getNodeDetailUrl({ - nodeType, - nodeId: node.id, - from: nodeDetailFrom, - to: currentTime, - }), - }; + const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000; + const nodeDetailMenuItem = { + name: i18n.translate('xpack.infra.nodeContextMenu.viewMetricsName', { + defaultMessage: 'View metrics', + }), + href: getNodeDetailUrl({ + nodeType, + nodeId: node.id, + from: nodeDetailFrom, + to: currentTime, + }), + }; - const apmTracesMenuItem = { - name: i18n.translate('xpack.infra.nodeContextMenu.viewAPMTraces', { - defaultMessage: 'View APM traces', - }), - href: `../app/apm#/traces?_g=()&kuery=${apmField}:"${node.id}"`, - 'data-test-subj': 'viewApmTracesContextMenuItem', - }; + const apmTracesMenuItem = { + name: i18n.translate('xpack.infra.nodeContextMenu.viewAPMTraces', { + defaultMessage: 'View APM traces', + }), + href: `../app/apm#/traces?_g=()&kuery=${apmField}:"${node.id}"`, + 'data-test-subj': 'viewApmTracesContextMenuItem', + }; - const uptimeMenuItem = { - name: i18n.translate('xpack.infra.nodeContextMenu.viewUptimeLink', { - defaultMessage: 'View in Uptime', - }), - href: createUptimeLink(options, nodeType, node), - }; + const uptimeMenuItem = { + name: i18n.translate('xpack.infra.nodeContextMenu.viewUptimeLink', { + defaultMessage: 'View in Uptime', + }), + href: createUptimeLink(options, nodeType, node), + }; - const showDetail = inventoryModel.crosslinkSupport.details; - const showLogsLink = - inventoryModel.crosslinkSupport.logs && node.id && uiCapabilities.logs.show; - const showAPMTraceLink = - inventoryModel.crosslinkSupport.apm && uiCapabilities.apm && uiCapabilities.apm.show; - const showUptimeLink = - inventoryModel.crosslinkSupport.uptime && - ([InfraNodeType.pod, InfraNodeType.container].includes(nodeType) || node.ip); + const showDetail = inventoryModel.crosslinkSupport.details; + const showLogsLink = + inventoryModel.crosslinkSupport.logs && node.id && uiCapabilities?.logs?.show; + const showAPMTraceLink = + inventoryModel.crosslinkSupport.apm && uiCapabilities?.apm && uiCapabilities?.apm.show; + const showUptimeLink = + inventoryModel.crosslinkSupport.uptime && + ([InfraNodeType.pod, InfraNodeType.container].includes(nodeType) || node.ip); - const items = [ - ...(showLogsLink ? [nodeLogsMenuItem] : []), - ...(showDetail ? [nodeDetailMenuItem] : []), - ...(showAPMTraceLink ? [apmTracesMenuItem] : []), - ...(showUptimeLink ? [uptimeMenuItem] : []), - ]; - const panels: EuiContextMenuPanelDescriptor[] = [{ id: 0, title: '', items }]; + const items = [ + ...(showLogsLink ? [nodeLogsMenuItem] : []), + ...(showDetail ? [nodeDetailMenuItem] : []), + ...(showAPMTraceLink ? [apmTracesMenuItem] : []), + ...(showUptimeLink ? [uptimeMenuItem] : []), + ]; + const panels: EuiContextMenuPanelDescriptor[] = [{ id: 0, title: '', items }]; - // If there is nothing to show then we need to return the child as is - if (items.length === 0) { - return <>{children}; - } - - return ( - - - - ); + // If there is nothing to show then we need to return the child as is + if (items.length === 0) { + return <>{children}; } -); + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx index 949896fbb60027..0a9df1f666f3d5 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx @@ -16,7 +16,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { FieldType } from 'ui/index_patterns'; +import { IFieldType } from 'src/plugins/data/public'; import { InfraNodeType, InfraSnapshotGroupbyInput } from '../../graphql/types'; import { InfraGroupByOptions } from '../../lib/lib'; import { CustomFieldPanel } from './custom_field_panel'; @@ -28,7 +28,7 @@ interface Props { groupBy: InfraSnapshotGroupbyInput[]; onChange: (groupBy: InfraSnapshotGroupbyInput[]) => void; onChangeCustomOptions: (options: InfraGroupByOptions[]) => void; - fields: FieldType[]; + fields: IFieldType[]; customOptions: InfraGroupByOptions[]; } diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_cleanup.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_cleanup.ts index 5054f607fa5dc2..76e5f210ca2380 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_cleanup.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_cleanup.ts @@ -8,7 +8,7 @@ import * as rt from 'io-ts'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { kfetch } from 'ui/kfetch'; +import { npStart } from 'ui/new_platform'; import { getDatafeedId, getJobId } from '../../../../../common/log_analysis'; import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; @@ -19,9 +19,8 @@ export const callDeleteJobs = async ( jobTypes: JobType[] ) => { // NOTE: Deleting the jobs via this API will delete the datafeeds at the same time - const deleteJobsResponse = await kfetch({ + const deleteJobsResponse = await npStart.core.http.fetch('/api/ml/jobs/delete_jobs', { method: 'POST', - pathname: '/api/ml/jobs/delete_jobs', body: JSON.stringify( deleteJobsRequestPayloadRT.encode({ jobIds: jobTypes.map(jobType => getJobId(spaceId, sourceId, jobType)), @@ -36,10 +35,9 @@ export const callDeleteJobs = async ( }; export const callGetJobDeletionTasks = async () => { - const jobDeletionTasksResponse = await kfetch({ - method: 'GET', - pathname: '/api/ml/jobs/deleting_jobs_tasks', - }); + const jobDeletionTasksResponse = await npStart.core.http.fetch( + '/api/ml/jobs/deleting_jobs_tasks' + ); return pipe( getJobDeletionTasksResponsePayloadRT.decode(jobDeletionTasksResponse), @@ -53,9 +51,8 @@ export const callStopDatafeeds = async ( jobTypes: JobType[] ) => { // Stop datafeed due to https://github.com/elastic/kibana/issues/44652 - const stopDatafeedResponse = await kfetch({ + const stopDatafeedResponse = await npStart.core.http.fetch('/api/ml/jobs/stop_datafeeds', { method: 'POST', - pathname: '/api/ml/jobs/stop_datafeeds', body: JSON.stringify( stopDatafeedsRequestPayloadRT.encode({ datafeedIds: jobTypes.map(jobType => getDatafeedId(spaceId, sourceId, jobType)), diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts index 2e1ba52353bef6..41c155e185c3a5 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts @@ -8,8 +8,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import * as rt from 'io-ts'; -import { kfetch } from 'ui/kfetch'; - +import { npStart } from 'ui/new_platform'; import { jobCustomSettingsRT } from './ml_api_types'; import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; import { getJobId } from '../../../../../common/log_analysis'; @@ -19,9 +18,8 @@ export const callJobsSummaryAPI = async ( sourceId: string, jobTypes: JobType[] ) => { - const response = await kfetch({ + const response = await npStart.core.http.fetch('/api/ml/jobs/jobs_summary', { method: 'POST', - pathname: '/api/ml/jobs/jobs_summary', body: JSON.stringify( fetchJobStatusRequestPayloadRT.encode({ jobIds: jobTypes.map(jobType => getJobId(spaceId, sourceId, jobType)), diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_module.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_module.ts index b58677ffa844e3..ae90c7bb841303 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_module.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_module.ts @@ -8,15 +8,13 @@ import { fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import { identity } from 'fp-ts/lib/function'; import * as rt from 'io-ts'; -import { kfetch } from 'ui/kfetch'; - +import { npStart } from 'ui/new_platform'; import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; import { jobCustomSettingsRT } from './ml_api_types'; export const callGetMlModuleAPI = async (moduleId: string) => { - const response = await kfetch({ + const response = await npStart.core.http.fetch(`/api/ml/modules/get_module/${moduleId}`, { method: 'GET', - pathname: `/api/ml/modules/get_module/${moduleId}`, }); return pipe( diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts index 80a4f975cdd576..774a521ff1d34b 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts @@ -8,8 +8,7 @@ import { fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import { identity } from 'fp-ts/lib/function'; import * as rt from 'io-ts'; -import { kfetch } from 'ui/kfetch'; - +import { npStart } from 'ui/new_platform'; import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; import { getJobIdPrefix } from '../../../../../common/log_analysis'; @@ -23,9 +22,8 @@ export const callSetupMlModuleAPI = async ( jobOverrides: SetupMlModuleJobOverrides[] = [], datafeedOverrides: SetupMlModuleDatafeedOverrides[] = [] ) => { - const response = await kfetch({ + const response = await npStart.core.http.fetch(`/api/ml/modules/setup/${moduleId}`, { method: 'POST', - pathname: `/api/ml/modules/setup/${moduleId}`, body: JSON.stringify( setupMlModuleRequestPayloadRT.encode({ start, diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/validate_indices.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/validate_indices.ts index 0d2e9b673488e7..480e84ff53e438 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/validate_indices.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/validate_indices.ts @@ -7,8 +7,7 @@ import { fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import { identity } from 'fp-ts/lib/function'; -import { kfetch } from 'ui/kfetch'; - +import { npStart } from 'ui/new_platform'; import { LOG_ANALYSIS_VALIDATE_INDICES_PATH, ValidationIndicesFieldSpecification, @@ -22,9 +21,8 @@ export const callValidateIndicesAPI = async ( indices: string[], fields: ValidationIndicesFieldSpecification[] ) => { - const response = await kfetch({ + const response = await npStart.core.http.fetch(LOG_ANALYSIS_VALIDATE_INDICES_PATH, { method: 'POST', - pathname: LOG_ANALYSIS_VALIDATE_INDICES_PATH, body: JSON.stringify(validationIndicesRequestPayloadRT.encode({ data: { indices, fields } })), }); diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_capabilities.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_capabilities.tsx index bb01043b0db6e9..bd8be6df8ea692 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_capabilities.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_capabilities.tsx @@ -6,8 +6,7 @@ import createContainer from 'constate'; import { useMemo, useState, useEffect } from 'react'; -import { kfetch } from 'ui/kfetch'; - +import { npStart } from 'ui/new_platform'; import { fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import { identity } from 'fp-ts/lib/function'; @@ -27,10 +26,7 @@ export const useLogAnalysisCapabilities = () => { { cancelPreviousOn: 'resolution', createPromise: async () => { - const rawResponse = await kfetch({ - method: 'GET', - pathname: '/api/ml/ml_capabilities', - }); + const rawResponse = await npStart.core.http.fetch('/api/ml/ml_capabilities'); return pipe( getMlCapabilitiesResponsePayloadRT.decode(rawResponse), diff --git a/x-pack/legacy/plugins/infra/public/containers/with_kibana_chrome.tsx b/x-pack/legacy/plugins/infra/public/containers/with_kibana_chrome.tsx deleted file mode 100644 index 5fc83315d3dfd8..00000000000000 --- a/x-pack/legacy/plugins/infra/public/containers/with_kibana_chrome.tsx +++ /dev/null @@ -1,42 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; - -import chrome from 'ui/chrome'; -import { Badge } from 'ui/chrome/api/badge'; -import { Breadcrumb } from 'ui/chrome/api/breadcrumbs'; -import { RendererFunction } from '../utils/typed_react'; - -interface WithKibanaChromeProps { - children: RendererFunction< - { - setBreadcrumbs: (newBreadcrumbs: Breadcrumb[]) => void; - setBadge: (badge: Badge | undefined) => void; - } & WithKibanaChromeState - >; -} - -interface WithKibanaChromeState { - basePath: string; -} - -export class WithKibanaChrome extends React.Component< - WithKibanaChromeProps, - WithKibanaChromeState -> { - public state: WithKibanaChromeState = { - basePath: chrome.getBasePath(), - }; - - public render() { - return this.props.children({ - ...this.state, - setBreadcrumbs: chrome.breadcrumbs.set, - setBadge: chrome.badge.set, - }); - } -} diff --git a/x-pack/legacy/plugins/infra/public/hooks/use_http_request.tsx b/x-pack/legacy/plugins/infra/public/hooks/use_http_request.tsx index a7dff2f11f2322..2078c4bdc151dc 100644 --- a/x-pack/legacy/plugins/infra/public/hooks/use_http_request.tsx +++ b/x-pack/legacy/plugins/infra/public/hooks/use_http_request.tsx @@ -5,57 +5,69 @@ */ import React, { useMemo, useState } from 'react'; -import { kfetch } from 'ui/kfetch'; -import { toastNotifications } from 'ui/notify'; +import { IHttpFetchError } from 'src/core/public'; import { i18n } from '@kbn/i18n'; -import { KFetchError } from 'ui/kfetch/kfetch_error'; -import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; import { useTrackedPromise } from '../utils/use_tracked_promise'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; + export function useHTTPRequest( pathname: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD', body?: string, decode: (response: any) => Response = response => response ) { + const kibana = useKibana(); + const fetch = kibana.services.http?.fetch; + const toasts = kibana.notifications.toasts; const [response, setResponse] = useState(null); - const [error, setError] = useState(null); + const [error, setError] = useState(null); const [request, makeRequest] = useTrackedPromise( { cancelPreviousOn: 'resolution', - createPromise: () => - kfetch({ + createPromise: () => { + if (!fetch) { + throw new Error('HTTP service is unavailable'); + } + return fetch(pathname, { method, - pathname, body, - }), + }); + }, onResolve: resp => setResponse(decode(resp)), onReject: (e: unknown) => { - const err = e as KFetchError; + const err = e as IHttpFetchError; setError(err); - toastNotifications.addWarning({ + toasts.warning({ + toastLifeTimeMs: 3000, title: i18n.translate('xpack.infra.useHTTPRequest.error.title', { defaultMessage: `Error while fetching resource`, }), - text: toMountPoint( + body: (
-
- {i18n.translate('xpack.infra.useHTTPRequest.error.status', { - defaultMessage: `Error`, - })} -
- {err.res?.statusText} ({err.res?.status}) -
- {i18n.translate('xpack.infra.useHTTPRequest.error.url', { - defaultMessage: `URL`, - })} -
- {err.res?.url} + {err.response ? ( + <> +
+ {i18n.translate('xpack.infra.useHTTPRequest.error.status', { + defaultMessage: `Error`, + })} +
+ {err.response?.statusText} ({err.response?.status}) +
+ {i18n.translate('xpack.infra.useHTTPRequest.error.url', { + defaultMessage: `URL`, + })} +
+ {err.response?.url} + + ) : ( +
{err.message}
+ )}
), }); }, }, - [pathname, body, method] + [pathname, body, method, fetch, toasts] ); const loading = useMemo(() => { diff --git a/x-pack/legacy/plugins/infra/public/index.ts b/x-pack/legacy/plugins/infra/public/index.ts index 7967e640ee9d35..5e68ca85681fe5 100644 --- a/x-pack/legacy/plugins/infra/public/index.ts +++ b/x-pack/legacy/plugins/infra/public/index.ts @@ -4,4 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +// NP_NOTE: Whilst we are in the transition period of the NP migration, this index file +// is exclusively for our static code exports that other plugins (e.g. APM) use. +// When we switch over to the real NP, and an export of "plugin" is expected and called, +// we can do away with the middle "app.ts" layer. The "app.ts" layer is needed for now, +// and needs to be situated differently to this index file, so that our code for setting the root template +// and attempting to start the app doesn't try to run just because another plugin is importing from this file. + export { useTrackPageview } from './hooks/use_track_metric'; diff --git a/x-pack/legacy/plugins/infra/public/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/infra/public/lib/adapters/framework/kibana_framework_adapter.ts deleted file mode 100644 index f91b40815a3ae0..00000000000000 --- a/x-pack/legacy/plugins/infra/public/lib/adapters/framework/kibana_framework_adapter.ts +++ /dev/null @@ -1,187 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -/* eslint-disable max-classes-per-file */ - -import { IModule, IScope } from 'angular'; -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; - -import { UIRoutes as KibanaUIRoutes } from 'ui/routes'; - -import { - InfraBufferedKibanaServiceCall, - InfraFrameworkAdapter, - InfraKibanaAdapterServiceRefs, - InfraKibanaUIConfig, - InfraTimezoneProvider, - InfraUiKibanaAdapterScope, -} from '../../lib'; - -const ROOT_ELEMENT_ID = 'react-infra-root'; -const BREADCRUMBS_ELEMENT_ID = 'react-infra-breadcrumbs'; - -export class KibanaFramework implements InfraFrameworkAdapter { - public appState: object; - public kbnVersion?: string; - public timezone?: string; - - private adapterService: KibanaAdapterServiceProvider; - private timezoneProvider: InfraTimezoneProvider; - private rootComponent: React.ReactElement | null = null; - private breadcrumbsComponent: React.ReactElement | null = null; - - constructor( - uiModule: IModule, - uiRoutes: KibanaUIRoutes, - timezoneProvider: InfraTimezoneProvider - ) { - this.adapterService = new KibanaAdapterServiceProvider(); - this.timezoneProvider = timezoneProvider; - this.appState = {}; - this.register(uiModule, uiRoutes); - } - - public setUISettings = (key: string, value: any) => { - this.adapterService.callOrBuffer(({ config }) => { - config.set(key, value); - }); - }; - - public render = (component: React.ReactElement) => { - this.adapterService.callOrBuffer(() => (this.rootComponent = component)); - }; - - public renderBreadcrumbs = (component: React.ReactElement) => { - this.adapterService.callOrBuffer(() => (this.breadcrumbsComponent = component)); - }; - - private register = (adapterModule: IModule, uiRoutes: KibanaUIRoutes) => { - adapterModule.provider('kibanaAdapter', this.adapterService); - - adapterModule.directive('infraUiKibanaAdapter', () => ({ - controller: ($scope: InfraUiKibanaAdapterScope, $element: JQLite) => ({ - $onDestroy: () => { - const targetRootElement = $element[0].querySelector(`#${ROOT_ELEMENT_ID}`); - const targetBreadcrumbsElement = $element[0].querySelector(`#${ROOT_ELEMENT_ID}`); - - if (targetRootElement) { - ReactDOM.unmountComponentAtNode(targetRootElement); - } - - if (targetBreadcrumbsElement) { - ReactDOM.unmountComponentAtNode(targetBreadcrumbsElement); - } - }, - $onInit: () => { - $scope.topNavMenu = []; - }, - $postLink: () => { - $scope.$watchGroup( - [ - () => this.breadcrumbsComponent, - () => $element[0].querySelector(`#${BREADCRUMBS_ELEMENT_ID}`), - ], - ([breadcrumbsComponent, targetElement]) => { - if (!targetElement) { - return; - } - - if (breadcrumbsComponent) { - ReactDOM.render(breadcrumbsComponent, targetElement); - } else { - ReactDOM.unmountComponentAtNode(targetElement); - } - } - ); - $scope.$watchGroup( - [() => this.rootComponent, () => $element[0].querySelector(`#${ROOT_ELEMENT_ID}`)], - ([rootComponent, targetElement]) => { - if (!targetElement) { - return; - } - - if (rootComponent) { - ReactDOM.render(rootComponent, targetElement); - } else { - ReactDOM.unmountComponentAtNode(targetElement); - } - } - ); - }, - }), - scope: true, - template: ` -
- `, - })); - - adapterModule.run( - ( - config: InfraKibanaUIConfig, - kbnVersion: string, - Private: (provider: Provider) => Provider, - // @ts-ignore: inject kibanaAdapter to force eager instatiation - kibanaAdapter: any - ) => { - this.timezone = Private(this.timezoneProvider)(); - this.kbnVersion = kbnVersion; - } - ); - - uiRoutes.enable(); - - uiRoutes.otherwise({ - reloadOnSearch: false, - template: - '', - }); - }; -} - -class KibanaAdapterServiceProvider { - public serviceRefs: InfraKibanaAdapterServiceRefs | null = null; - public bufferedCalls: Array> = []; - - public $get($rootScope: IScope, config: InfraKibanaUIConfig) { - this.serviceRefs = { - config, - rootScope: $rootScope, - }; - - this.applyBufferedCalls(this.bufferedCalls); - - return this; - } - - public callOrBuffer(serviceCall: (serviceRefs: InfraKibanaAdapterServiceRefs) => void) { - if (this.serviceRefs !== null) { - this.applyBufferedCalls([serviceCall]); - } else { - this.bufferedCalls.push(serviceCall); - } - } - - public applyBufferedCalls( - bufferedCalls: Array> - ) { - if (!this.serviceRefs) { - return; - } - - this.serviceRefs.rootScope.$apply(() => { - bufferedCalls.forEach(serviceCall => { - if (!this.serviceRefs) { - return; - } - return serviceCall(this.serviceRefs); - }); - }); - } -} diff --git a/x-pack/legacy/plugins/infra/public/lib/adapters/framework/testing_framework_adapter.ts b/x-pack/legacy/plugins/infra/public/lib/adapters/framework/testing_framework_adapter.ts deleted file mode 100644 index f1f08f6ffbf348..00000000000000 --- a/x-pack/legacy/plugins/infra/public/lib/adapters/framework/testing_framework_adapter.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { InfraFrameworkAdapter } from '../../lib'; - -export class InfraTestingFrameworkAdapter implements InfraFrameworkAdapter { - public appState?: object; - public kbnVersion?: string; - public timezone?: string; - - constructor() { - this.appState = {}; - } - - public render() { - return; - } - public renderBreadcrumbs() { - return; - } - public setUISettings() { - return; - } -} diff --git a/x-pack/legacy/plugins/infra/public/lib/adapters/observable_api/kibana_observable_api.ts b/x-pack/legacy/plugins/infra/public/lib/adapters/observable_api/kibana_observable_api.ts index 157a7ebe9fedf7..9ae21d96886f34 100644 --- a/x-pack/legacy/plugins/infra/public/lib/adapters/observable_api/kibana_observable_api.ts +++ b/x-pack/legacy/plugins/infra/public/lib/adapters/observable_api/kibana_observable_api.ts @@ -16,13 +16,13 @@ import { export class InfraKibanaObservableApiAdapter implements InfraObservableApi { private basePath: string; private defaultHeaders: { - [headerName: string]: string; + [headerName: string]: boolean | string; }; - constructor({ basePath, xsrfToken }: { basePath: string; xsrfToken: string }) { + constructor({ basePath }: { basePath: string }) { this.basePath = basePath; this.defaultHeaders = { - 'kbn-version': xsrfToken, + 'kbn-xsrf': true, }; } diff --git a/x-pack/legacy/plugins/infra/public/lib/compose/kibana_compose.ts b/x-pack/legacy/plugins/infra/public/lib/compose/kibana_compose.ts deleted file mode 100644 index 9b0beb3ad519c7..00000000000000 --- a/x-pack/legacy/plugins/infra/public/lib/compose/kibana_compose.ts +++ /dev/null @@ -1,68 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import 'ui/autoload/all'; -// @ts-ignore: path dynamic for kibana -import chrome from 'ui/chrome'; -// @ts-ignore: path dynamic for kibana -import { uiModules } from 'ui/modules'; -import uiRoutes from 'ui/routes'; -// @ts-ignore: path dynamic for kibana -import { timezoneProvider } from 'ui/vis/lib/timezone'; - -import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'; -import ApolloClient from 'apollo-client'; -import { ApolloLink } from 'apollo-link'; -import { HttpLink } from 'apollo-link-http'; -import { withClientState } from 'apollo-link-state'; -import { InfraFrontendLibs } from '../lib'; -import introspectionQueryResultData from '../../graphql/introspection.json'; -import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; -import { InfraKibanaObservableApiAdapter } from '../adapters/observable_api/kibana_observable_api'; - -export function compose(): InfraFrontendLibs { - const cache = new InMemoryCache({ - addTypename: false, - fragmentMatcher: new IntrospectionFragmentMatcher({ - introspectionQueryResultData, - }), - }); - - const observableApi = new InfraKibanaObservableApiAdapter({ - basePath: chrome.getBasePath(), - xsrfToken: chrome.getXsrfToken(), - }); - - const graphQLOptions = { - cache, - link: ApolloLink.from([ - withClientState({ - cache, - resolvers: {}, - }), - new HttpLink({ - credentials: 'same-origin', - headers: { - 'kbn-xsrf': chrome.getXsrfToken(), - }, - uri: `${chrome.getBasePath()}/api/infra/graphql`, - }), - ]), - }; - - const apolloClient = new ApolloClient(graphQLOptions); - - const infraModule = uiModules.get('app/infa'); - - const framework = new KibanaFramework(infraModule, uiRoutes, timezoneProvider); - - const libs: InfraFrontendLibs = { - apolloClient, - framework, - observableApi, - }; - return libs; -} diff --git a/x-pack/legacy/plugins/infra/public/lib/compose/testing_compose.ts b/x-pack/legacy/plugins/infra/public/lib/compose/testing_compose.ts deleted file mode 100644 index 1e0b2f079497db..00000000000000 --- a/x-pack/legacy/plugins/infra/public/lib/compose/testing_compose.ts +++ /dev/null @@ -1,59 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import 'ui/autoload/all'; -// @ts-ignore: path dynamic for kibana -import chrome from 'ui/chrome'; -// @ts-ignore: path dynamic for kibana -import { uiModules } from 'ui/modules'; -import uiRoutes from 'ui/routes'; -// @ts-ignore: path dynamic for kibana -import { timezoneProvider } from 'ui/vis/lib/timezone'; - -import { InMemoryCache } from 'apollo-cache-inmemory'; -import ApolloClient from 'apollo-client'; -import { SchemaLink } from 'apollo-link-schema'; -import { addMockFunctionsToSchema, makeExecutableSchema } from 'graphql-tools'; -import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; -import { InfraKibanaObservableApiAdapter } from '../adapters/observable_api/kibana_observable_api'; -import { InfraFrontendLibs } from '../lib'; - -export function compose(): InfraFrontendLibs { - const infraModule = uiModules.get('app/infa'); - const observableApi = new InfraKibanaObservableApiAdapter({ - basePath: chrome.getBasePath(), - xsrfToken: chrome.getXsrfToken(), - }); - const framework = new KibanaFramework(infraModule, uiRoutes, timezoneProvider); - const typeDefs = ` - Query {} -`; - - const mocks = { - Mutation: () => undefined, - Query: () => undefined, - }; - - const schema = makeExecutableSchema({ typeDefs }); - addMockFunctionsToSchema({ - mocks, - schema, - }); - - const cache = new InMemoryCache((window as any).__APOLLO_CLIENT__); - - const apolloClient = new ApolloClient({ - cache, - link: new SchemaLink({ schema }), - }); - - const libs: InfraFrontendLibs = { - apolloClient, - framework, - observableApi, - }; - return libs; -} diff --git a/x-pack/legacy/plugins/infra/public/lib/lib.ts b/x-pack/legacy/plugins/infra/public/lib/lib.ts index 3a9c12f9680e34..4b402ce6beafe3 100644 --- a/x-pack/legacy/plugins/infra/public/lib/lib.ts +++ b/x-pack/legacy/plugins/infra/public/lib/lib.ts @@ -20,7 +20,6 @@ import { } from '../graphql/types'; export interface InfraFrontendLibs { - framework: InfraFrameworkAdapter; apolloClient: InfraApolloClient; observableApi: InfraObservableApi; } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/types.ts b/x-pack/legacy/plugins/infra/public/new_platform_index.ts similarity index 55% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/types.ts rename to x-pack/legacy/plugins/infra/public/new_platform_index.ts index f6878c9edc9b82..33b40da2361452 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/types.ts +++ b/x-pack/legacy/plugins/infra/public/new_platform_index.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestFacade } from '../../../../types'; +import { PluginInitializerContext } from 'kibana/public'; +import { Plugin } from './new_platform_plugin'; -export type QueryRequest = Omit & { - query: { id: string | undefined; rule_id: string | undefined }; -}; +export function plugin(context: PluginInitializerContext) { + return new Plugin(context); +} diff --git a/x-pack/legacy/plugins/infra/public/new_platform_plugin.ts b/x-pack/legacy/plugins/infra/public/new_platform_plugin.ts new file mode 100644 index 00000000000000..78594afcc8ada7 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/new_platform_plugin.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; + * you may not use this file except in compliance with the Elastic License. + */ +import { CoreStart, PluginInitializerContext } from 'kibana/public'; +import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'; +import ApolloClient from 'apollo-client'; +import { ApolloLink } from 'apollo-link'; +import { HttpLink } from 'apollo-link-http'; +import { withClientState } from 'apollo-link-state'; +import { startApp } from './apps/start_app'; +import { InfraFrontendLibs } from './lib/lib'; +import introspectionQueryResultData from './graphql/introspection.json'; +import { InfraKibanaObservableApiAdapter } from './lib/adapters/observable_api/kibana_observable_api'; + +type ClientPlugins = any; +type LegacyDeps = any; + +export class Plugin { + constructor(context: PluginInitializerContext) {} + start(core: CoreStart, plugins: ClientPlugins, __LEGACY: LegacyDeps) { + startApp(this.composeLibs(core, plugins, __LEGACY), core, plugins); + } + + composeLibs(core: CoreStart, plugins: ClientPlugins, legacy: LegacyDeps) { + const cache = new InMemoryCache({ + addTypename: false, + fragmentMatcher: new IntrospectionFragmentMatcher({ + introspectionQueryResultData, + }), + }); + + const observableApi = new InfraKibanaObservableApiAdapter({ + basePath: core.http.basePath.get(), + }); + + const graphQLOptions = { + cache, + link: ApolloLink.from([ + withClientState({ + cache, + resolvers: {}, + }), + new HttpLink({ + credentials: 'same-origin', + headers: { + 'kbn-xsrf': true, + }, + uri: `${core.http.basePath.get()}/api/infra/graphql`, + }), + ]), + }; + + const apolloClient = new ApolloClient(graphQLOptions); + + const libs: InfraFrontendLibs = { + apolloClient, + observableApi, + }; + return libs; + } +} diff --git a/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx b/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx index 9efbbe790abc14..dfe4fb05d669a8 100644 --- a/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx @@ -8,8 +8,6 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { Route, RouteComponentProps, Switch } from 'react-router-dom'; -import { UICapabilities } from 'ui/capabilities'; -import { injectUICapabilities } from 'ui/capabilities/react'; import { DocumentTitle } from '../../components/document_title'; import { HelpCenterContent } from '../../components/help_center_content'; @@ -25,13 +23,11 @@ import { SnapshotPage } from './snapshot'; import { SettingsPage } from '../shared/settings'; import { AppNavigation } from '../../components/navigation/app_navigation'; import { SourceLoadingPage } from '../../components/source_loading_page'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -interface InfrastructurePageProps extends RouteComponentProps { - uiCapabilities: UICapabilities; -} - -export const InfrastructurePage = injectUICapabilities( - ({ match, uiCapabilities }: InfrastructurePageProps) => ( +export const InfrastructurePage = ({ match }: RouteComponentProps) => { + const uiCapabilities = useKibana().services.application?.capabilities; + return ( - ) -); + ); +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/index.tsx b/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/index.tsx index 612b4a6d91e7f6..b9651763e43c06 100644 --- a/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/index.tsx @@ -8,8 +8,6 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useContext } from 'react'; -import { UICapabilities } from 'ui/capabilities'; -import { injectUICapabilities } from 'ui/capabilities/react'; import { SnapshotPageContent } from './page_content'; import { SnapshotToolbar } from './toolbar'; @@ -27,15 +25,11 @@ import { Source } from '../../../containers/source'; import { WithWaffleFilterUrlState } from '../../../containers/waffle/with_waffle_filters'; import { WithWaffleOptionsUrlState } from '../../../containers/waffle/with_waffle_options'; import { WithWaffleTimeUrlState } from '../../../containers/waffle/with_waffle_time'; -import { WithKibanaChrome } from '../../../containers/with_kibana_chrome'; import { useTrackPageview } from '../../../hooks/use_track_metric'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; -interface SnapshotPageProps { - uiCapabilities: UICapabilities; -} - -export const SnapshotPage = injectUICapabilities((props: SnapshotPageProps) => { - const { uiCapabilities } = props; +export const SnapshotPage = () => { + const uiCapabilities = useKibana().services.application?.capabilities; const { createDerivedIndexPattern, hasFailedLoadingSource, @@ -44,7 +38,7 @@ export const SnapshotPage = injectUICapabilities((props: SnapshotPageProps) => { loadSource, metricIndicesExist, } = useContext(Source.Context); - + const basePath = useKibana().services.http?.basePath || ''; useTrackPageview({ app: 'infra_metrics', path: 'inventory' }); useTrackPageview({ app: 'infra_metrics', path: 'inventory', delay: 15000 }); @@ -73,49 +67,44 @@ export const SnapshotPage = injectUICapabilities((props: SnapshotPageProps) => { ) : hasFailedLoadingSource ? ( ) : ( - - {({ basePath }) => ( - - - - {i18n.translate( - 'xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel', - { defaultMessage: 'View setup instructions' } - )} - - - {uiCapabilities.infrastructure.configureSource ? ( - - - {i18n.translate('xpack.infra.configureSourceActionLabel', { - defaultMessage: 'Change source configuration', - })} - - - ) : null} -
- } - data-test-subj="noMetricsIndicesPrompt" - /> - )} - + + + + {i18n.translate('xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel', { + defaultMessage: 'View setup instructions', + })} + + + {uiCapabilities?.infrastructure?.configureSource ? ( + + + {i18n.translate('xpack.infra.configureSourceActionLabel', { + defaultMessage: 'Change source configuration', + })} + + + ) : null} + + } + data-test-subj="noMetricsIndicesPrompt" + /> )} ); -}); +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx index b0619fdcc2acf9..a8a75f99253c2b 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx @@ -7,8 +7,6 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { Route, RouteComponentProps, Switch } from 'react-router-dom'; -import { UICapabilities } from 'ui/capabilities'; -import { injectUICapabilities } from 'ui/capabilities/react'; import { DocumentTitle } from '../../components/document_title'; import { HelpCenterContent } from '../../components/help_center_content'; @@ -27,14 +25,12 @@ import { } from '../../containers/logs/log_analysis'; import { useSourceId } from '../../containers/source_id'; import { RedirectWithQueryParams } from '../../utils/redirect_with_query_params'; +import { useKibana } from '../../../../../../..//src/plugins/kibana_react/public'; import { LogEntryCategoriesPage } from './log_entry_categories'; import { LogEntryRatePage } from './log_entry_rate'; -interface LogsPageProps extends RouteComponentProps { - uiCapabilities: UICapabilities; -} - -export const LogsPage = injectUICapabilities(({ match, uiCapabilities }: LogsPageProps) => { +export const LogsPage = ({ match }: RouteComponentProps) => { + const uiCapabilities = useKibana().services.application?.capabilities; const [sourceId] = useSourceId(); const source = useSource({ sourceId }); const logAnalysisCapabilities = useLogAnalysisCapabilities(); @@ -83,7 +79,7 @@ export const LogsPage = injectUICapabilities(({ match, uiCapabilities }: LogsPag text: pageTitle, }, ]} - readOnlyBadge={!uiCapabilities.logs.save} + readOnlyBadge={!uiCapabilities?.logs?.save} /> {source.isLoadingSource || (!source.isLoadingSource && @@ -124,7 +120,7 @@ export const LogsPage = injectUICapabilities(({ match, uiCapabilities }: LogsPag ); -}); +}; const pageTitle = i18n.translate('xpack.infra.header.logsTitle', { defaultMessage: 'Logs', diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_rate.ts b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_rate.ts index 471a00d40984cf..1df7ef06b9d465 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_rate.ts +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_rate.ts @@ -7,8 +7,7 @@ import { fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import { identity } from 'fp-ts/lib/function'; -import { kfetch } from 'ui/kfetch'; - +import { npStart } from 'ui/new_platform'; import { getLogEntryRateRequestPayloadRT, getLogEntryRateSuccessReponsePayloadRT, @@ -22,9 +21,8 @@ export const callGetLogEntryRateAPI = async ( endTime: number, bucketDuration: number ) => { - const response = await kfetch({ + const response = await npStart.core.http.fetch(LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, { method: 'POST', - pathname: LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, body: JSON.stringify( getLogEntryRateRequestPayloadRT.encode({ data: { diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx index f1bce3da6c876f..0fe382bae03b96 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx @@ -9,66 +9,53 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; -import { UICapabilities } from 'ui/capabilities'; -import { injectUICapabilities } from 'ui/capabilities/react'; import { NoIndices } from '../../../components/empty_states/no_indices'; -import { WithKibanaChrome } from '../../../containers/with_kibana_chrome'; import { ViewSourceConfigurationButton, ViewSourceConfigurationButtonHrefBase, } from '../../../components/source_configuration'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; -interface LogsPageNoIndicesContentProps { - uiCapabilities: UICapabilities; -} - -export const LogsPageNoIndicesContent = injectUICapabilities( - (props: LogsPageNoIndicesContentProps) => { - const { uiCapabilities } = props; - - return ( - - {({ basePath }) => ( - - - - {i18n.translate( - 'xpack.infra.logsPage.noLoggingIndicesInstructionsActionLabel', - { defaultMessage: 'View setup instructions' } - )} - - - {uiCapabilities.logs.configureSource ? ( - - - {i18n.translate('xpack.infra.configureSourceActionLabel', { - defaultMessage: 'Change source configuration', - })} - - - ) : null} - - } - /> - )} - - ); - } -); +export const LogsPageNoIndicesContent = () => { + const basePath = useKibana().services.http?.basePath || ''; + const uiCapabilities = useKibana().services.application?.capabilities; + return ( + + + + {i18n.translate('xpack.infra.logsPage.noLoggingIndicesInstructionsActionLabel', { + defaultMessage: 'View setup instructions', + })} + + + {uiCapabilities?.logs?.configureSource ? ( + + + {i18n.translate('xpack.infra.configureSourceActionLabel', { + defaultMessage: 'Change source configuration', + })} + + + ) : null} + + } + /> + ); +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/components/chart_section_vis.tsx b/x-pack/legacy/plugins/infra/public/pages/metrics/components/chart_section_vis.tsx index 309961cc390259..3d4d6568233322 100644 --- a/x-pack/legacy/plugins/infra/public/pages/metrics/components/chart_section_vis.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/metrics/components/chart_section_vis.tsx @@ -28,6 +28,7 @@ import { } from './helpers'; import { ErrorMessage } from './error_message'; import { useKibanaUiSetting } from '../../../utils/use_kibana_ui_setting'; +import { useUiSetting } from '../../../../../../../../src/plugins/kibana_react/public'; import { VisSectionProps } from '../types'; export const ChartSectionVis = ({ @@ -42,6 +43,7 @@ export const ChartSectionVis = ({ seriesOverrides, type, }: VisSectionProps) => { + const isDarkMode = useUiSetting('theme:darkMode'); const [dateFormat] = useKibanaUiSetting('dateFormat'); const valueFormatter = useCallback(getFormatter(formatter, formatterTemplate), [ formatter, @@ -125,7 +127,7 @@ export const ChartSectionVis = ({ diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/components/invalid_node.tsx b/x-pack/legacy/plugins/infra/public/pages/metrics/components/invalid_node.tsx index f9e56791746f5c..6bda7b7a7cd085 100644 --- a/x-pack/legacy/plugins/infra/public/pages/metrics/components/invalid_node.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/metrics/components/invalid_node.tsx @@ -9,70 +9,67 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import euiStyled from '../../../../../../common/eui_styled_components'; -import { WithKibanaChrome } from '../../../containers/with_kibana_chrome'; import { ViewSourceConfigurationButton, ViewSourceConfigurationButtonHrefBase, } from '../../../components/source_configuration'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; interface InvalidNodeErrorProps { nodeName: string; } export const InvalidNodeError: React.FunctionComponent = ({ nodeName }) => { + const basePath = useKibana().services.http?.basePath || ''; return ( - - {({ basePath }) => ( - + + + + } + body={ +

+ +

+ } + actions={ + + + - - } - body={ -

+ + + + -

- } - actions={ - - - - - - - - - - - - - } - /> - )} -
+ + + + } + /> ); }; diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/components/page_error.tsx b/x-pack/legacy/plugins/infra/public/pages/metrics/components/page_error.tsx index c893d3079364d5..e54cdcd151f6fc 100644 --- a/x-pack/legacy/plugins/infra/public/pages/metrics/components/page_error.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/metrics/components/page_error.tsx @@ -7,7 +7,7 @@ // import { GraphQLFormattedError } from 'graphql'; import React from 'react'; import { i18n } from '@kbn/i18n'; -import { KFetchError } from 'ui/kfetch/kfetch_error'; +import { IHttpFetchError } from 'src/core/public'; import { InvalidNodeError } from './invalid_node'; // import { InfraMetricsErrorCodes } from '../../../../common/errors'; import { DocumentTitle } from '../../../components/document_title'; @@ -15,7 +15,7 @@ import { ErrorPageBody } from '../../error'; interface Props { name: string; - error: KFetchError; + error: IHttpFetchError; } export const PageError = ({ error, name }: Props) => { diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx b/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx index b330ad02f10222..b7e8274802555a 100644 --- a/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx @@ -5,8 +5,6 @@ */ import { i18n } from '@kbn/i18n'; import React, { useContext, useState } from 'react'; -import { UICapabilities } from 'ui/capabilities'; -import { injectUICapabilities } from 'ui/capabilities/react'; import euiStyled, { EuiTheme, withTheme } from '../../../../../common/eui_styled_components'; import { DocumentTitle } from '../../components/document_title'; import { Header } from '../../components/header'; @@ -20,6 +18,7 @@ import { InfraLoadingPanel } from '../../components/loading'; import { findInventoryModel } from '../../../common/inventory_models'; import { NavItem } from './lib/side_nav_context'; import { NodeDetailsPage } from './components/node_details_page'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; const DetailPageContent = euiStyled(PageContent)` overflow: auto; @@ -34,111 +33,109 @@ interface Props { node: string; }; }; - uiCapabilities: UICapabilities; } export const MetricDetail = withMetricPageProviders( - injectUICapabilities( - withTheme(({ uiCapabilities, match, theme }: Props) => { - const nodeId = match.params.node; - const nodeType = match.params.type as InfraNodeType; - const inventoryModel = findInventoryModel(nodeType); - const { sourceId } = useContext(Source.Context); - const { - name, - filteredRequiredMetrics, - loading: metadataLoading, - cloudId, - metadata, - } = useMetadata(nodeId, nodeType, inventoryModel.requiredMetrics, sourceId); + withTheme(({ match, theme }: Props) => { + const uiCapabilities = useKibana().services.application?.capabilities; + const nodeId = match.params.node; + const nodeType = match.params.type as InfraNodeType; + const inventoryModel = findInventoryModel(nodeType); + const { sourceId } = useContext(Source.Context); + const { + name, + filteredRequiredMetrics, + loading: metadataLoading, + cloudId, + metadata, + } = useMetadata(nodeId, nodeType, inventoryModel.requiredMetrics, sourceId); - const [sideNav, setSideNav] = useState([]); + const [sideNav, setSideNav] = useState([]); - const addNavItem = React.useCallback( - (item: NavItem) => { - if (!sideNav.some(n => n.id === item.id)) { - setSideNav([item, ...sideNav]); - } - }, - [sideNav] - ); - - const breadcrumbs = [ - { - href: '#/', - text: i18n.translate('xpack.infra.header.infrastructureTitle', { - defaultMessage: 'Metrics', - }), - }, - { text: name }, - ]; + const addNavItem = React.useCallback( + (item: NavItem) => { + if (!sideNav.some(n => n.id === item.id)) { + setSideNav([item, ...sideNav]); + } + }, + [sideNav] + ); - if (metadataLoading && !filteredRequiredMetrics.length) { - return ( - - ); - } + const breadcrumbs = [ + { + href: '#/', + text: i18n.translate('xpack.infra.header.infrastructureTitle', { + defaultMessage: 'Metrics', + }), + }, + { text: name }, + ]; + if (metadataLoading && !filteredRequiredMetrics.length) { return ( - - {({ - timeRange, - parsedTimeRange, - setTimeRange, - refreshInterval, - setRefreshInterval, - isAutoReloading, - setAutoReload, - triggerRefresh, - }) => ( - -
- - - - {metadata ? ( - - ) : null} - - - )} - + ); - }) - ) + } + + return ( + + {({ + timeRange, + parsedTimeRange, + setTimeRange, + refreshInterval, + setRefreshInterval, + isAutoReloading, + setAutoReload, + triggerRefresh, + }) => ( + +
+ + + + {metadata ? ( + + ) : null} + + + )} + + ); + }) ); diff --git a/x-pack/legacy/plugins/infra/public/pages/shared/settings/index.tsx b/x-pack/legacy/plugins/infra/public/pages/shared/settings/index.tsx index daea6cfabdc2ae..23ed1b273aa0e5 100644 --- a/x-pack/legacy/plugins/infra/public/pages/shared/settings/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/shared/settings/index.tsx @@ -5,14 +5,14 @@ */ import React from 'react'; -import { UICapabilities } from 'ui/capabilities'; -import { injectUICapabilities } from 'ui/capabilities/react'; import { SourceConfigurationSettings } from '../../../components/source_configuration/source_configuration_settings'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; -interface SettingsPageProps { - uiCapabilities: UICapabilities; -} - -export const SettingsPage = injectUICapabilities(({ uiCapabilities }: SettingsPageProps) => ( - -)); +export const SettingsPage = () => { + const uiCapabilities = useKibana().services.application?.capabilities; + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/infra/public/routes.tsx b/x-pack/legacy/plugins/infra/public/routes.tsx index 9dedc612bbe540..fd69c6e8424485 100644 --- a/x-pack/legacy/plugins/infra/public/routes.tsx +++ b/x-pack/legacy/plugins/infra/public/routes.tsx @@ -8,55 +8,54 @@ import { History } from 'history'; import React from 'react'; import { Route, Router, Switch } from 'react-router-dom'; -import { UICapabilities } from 'ui/capabilities'; -import { injectUICapabilities } from 'ui/capabilities/react'; import { NotFoundPage } from './pages/404'; import { InfrastructurePage } from './pages/infrastructure'; import { LinkToPage } from './pages/link_to'; import { LogsPage } from './pages/logs'; import { MetricDetail } from './pages/metrics'; import { RedirectWithQueryParams } from './utils/redirect_with_query_params'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; interface RouterProps { history: History; - uiCapabilities: UICapabilities; } -const PageRouterComponent: React.FC = ({ history, uiCapabilities }) => { +export const PageRouter: React.FC = ({ history }) => { + const uiCapabilities = useKibana().services.application?.capabilities; return ( - {uiCapabilities.infrastructure.show && ( + {uiCapabilities?.infrastructure?.show && ( )} - {uiCapabilities.infrastructure.show && ( + {uiCapabilities?.infrastructure?.show && ( )} - {uiCapabilities.infrastructure.show && ( + {uiCapabilities?.infrastructure?.show && ( )} - {uiCapabilities.infrastructure.show && ( + {uiCapabilities?.infrastructure?.show && ( )} - {uiCapabilities.infrastructure.show && ( + {uiCapabilities?.infrastructure?.show && ( )} - {uiCapabilities.infrastructure.show && ( + {uiCapabilities?.infrastructure?.show && ( )} - {uiCapabilities.logs.show && ( + {uiCapabilities?.logs?.show && ( )} - {uiCapabilities.logs.show && } - {uiCapabilities.infrastructure.show && ( + {uiCapabilities?.logs?.show && } + {uiCapabilities?.infrastructure?.show && ( )} @@ -65,5 +64,3 @@ const PageRouterComponent: React.FC = ({ history, uiCapabilities }) ); }; - -export const PageRouter = injectUICapabilities(PageRouterComponent); diff --git a/x-pack/legacy/plugins/infra/public/utils/is_displayable.ts b/x-pack/legacy/plugins/infra/public/utils/is_displayable.ts index 6af6bce3155993..534282e807036d 100644 --- a/x-pack/legacy/plugins/infra/public/utils/is_displayable.ts +++ b/x-pack/legacy/plugins/infra/public/utils/is_displayable.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FieldType } from 'ui/index_patterns'; +import { IFieldType } from 'src/plugins/data/public'; import { startsWith, uniq } from 'lodash'; import { getAllowedListForPrefix } from '../../common/ecs_allowed_list'; -interface DisplayableFieldType extends FieldType { +interface DisplayableFieldType extends IFieldType { displayable?: boolean; } diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts index 28d5f75c3fdaf8..5b4ada31a6b0cf 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -11,21 +11,19 @@ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { RouteMethod, RouteConfig } from '../../../../../../../../src/core/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../../plugins/features/server'; import { SpacesPluginSetup } from '../../../../../../../plugins/spaces/server'; +import { VisTypeTimeseriesSetup } from '../../../../../../../../src/plugins/vis_type_timeseries/server'; import { APMPluginContract } from '../../../../../../../plugins/apm/server'; // NP_TODO: Compose real types from plugins we depend on, no "any" export interface InfraServerPluginDeps { spaces: SpacesPluginSetup; usageCollection: UsageCollectionSetup; - metrics: { - getVisData: any; - }; + metrics: VisTypeTimeseriesSetup; indexPatterns: { indexPatternsServiceFactory: any; }; features: FeaturesPluginSetup; apm: APMPluginContract; - ___legacy: any; } export interface CallWithRequestParams extends GenericParams { diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index b0cdb8389cb290..d7dd4ed1c82a42 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -36,12 +36,10 @@ import { InfraConfig } from '../../../../../../../plugins/infra/server'; export class KibanaFramework { public router: IRouter; - private core: CoreSetup; public plugins: InfraServerPluginDeps; constructor(core: CoreSetup, config: InfraConfig, plugins: InfraServerPluginDeps) { this.router = core.http.createRouter(); - this.core = core; this.plugins = plugins; } @@ -246,45 +244,21 @@ export class KibanaFramework { } } - // NP_TODO: [TSVB_GROUP] This method needs fixing when the metrics plugin has migrated to the New Platform public async makeTSVBRequest( - request: KibanaRequest, + requestContext: RequestHandlerContext, model: TSVBMetricModel, timerange: { min: number; max: number }, - filters: any[], - requestContext: RequestHandlerContext + filters: any[] ): Promise { const { getVisData } = this.plugins.metrics; if (typeof getVisData !== 'function') { throw new Error('TSVB is not available'); } - const url = this.core.http.basePath.prepend('/api/metrics/vis/data'); - // For the following request we need a copy of the instnace of the internal request - // but modified for our TSVB request. This will ensure all the instance methods - // are available along with our overriden values - const requestCopy = Object.assign({}, request, { - url, - method: 'POST', - payload: { - timerange, - panels: [model], - filters, - }, - // NP_NOTE: [TSVB_GROUP] Huge hack to make TSVB (getVisData()) work with raw requests that - // originate from the New Platform router (and are very different to the old request object). - // Once TSVB has migrated over to NP, and can work with the new raw requests, or ideally just - // the requestContext, this can be removed. - server: { - plugins: { - elasticsearch: this.plugins.___legacy.tsvb.elasticsearch, - }, - newPlatform: { - __internals: this.plugins.___legacy.tsvb.__internals, - }, - }, - getUiSettingsService: () => requestContext.core.uiSettings.client, - getSavedObjectsClient: () => requestContext.core.savedObjects.client, - }); - return getVisData(requestCopy); + const options = { + timerange, + panels: [model], + filters, + }; + return getVisData(requestContext, options); } } diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts index c1b567576c2149..6acb8afbfb2491 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts @@ -47,7 +47,7 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { } const requests = options.metrics.map(metricId => - this.makeTSVBRequest(metricId, options, rawRequest, nodeField, requestContext) + this.makeTSVBRequest(metricId, options, nodeField, requestContext) ); return Promise.all(requests) @@ -89,7 +89,6 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { async makeTSVBRequest( metricId: InfraMetric, options: InfraMetricsRequestOptions, - req: KibanaRequest, nodeField: string, requestContext: RequestHandlerContext ) { @@ -150,6 +149,6 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { ? [{ match: { [model.map_field_to]: id } }] : [{ match: { [nodeField]: id } }]; - return this.framework.makeTSVBRequest(req, model, timerange, filters, requestContext); + return this.framework.makeTSVBRequest(requestContext, model, timerange, filters); } } diff --git a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts index 3dfbfbb5c39797..17fc46b41278af 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts @@ -76,13 +76,7 @@ export const populateSeriesWithTSVBData = ( } // Get TSVB results using the model, timerange and filters - const tsvbResults = await framework.makeTSVBRequest( - request, - model, - timerange, - filters, - requestContext - ); + const tsvbResults = await framework.makeTSVBRequest(requestContext, model, timerange, filters); // If there is no data `custom` will not exist. if (!tsvbResults.custom) { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx index f3e86c5b592145..82e172b6bd7e27 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx @@ -17,7 +17,7 @@ import { import { DropHandler, DragContextState } from '../../drag_drop'; import { createMockedDragDropContext } from '../mocks'; import { mountWithIntl as mount, shallowWithIntl as shallow } from 'test_utils/enzyme_helpers'; -import { IUiSettingsClient, SavedObjectsClientContract, HttpServiceBase } from 'src/core/public'; +import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { IndexPatternPrivateState } from '../types'; import { documentField } from '../document_field'; @@ -138,7 +138,7 @@ describe('IndexPatternDimensionPanel', () => { storage: {} as IStorageWrapper, uiSettings: {} as IUiSettingsClient, savedObjectsClient: {} as SavedObjectsClientContract, - http: {} as HttpServiceBase, + http: {} as HttpSetup, }; jest.clearAllMocks(); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx index fded53cd35f591..dfeb8632d096e0 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx @@ -8,7 +8,7 @@ import _ from 'lodash'; import React, { memo, useMemo } from 'react'; import { EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { IUiSettingsClient, SavedObjectsClientContract, HttpServiceBase } from 'src/core/public'; +import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { DatasourceDimensionPanelProps, StateSetter } from '../../types'; import { IndexPatternColumn, OperationType } from '../indexpattern'; @@ -29,7 +29,7 @@ export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & { storage: IStorageWrapper; savedObjectsClient: SavedObjectsClientContract; layerId: string; - http: HttpServiceBase; + http: HttpSetup; uniqueLabel: string; dateRange: DateRange; }; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts index 89d4224a7df14a..661c627f3454f6 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts @@ -6,11 +6,7 @@ import _ from 'lodash'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { - SavedObjectsClientContract, - SavedObjectAttributes, - HttpServiceBase, -} from 'src/core/public'; +import { SavedObjectsClientContract, SavedObjectAttributes, HttpSetup } from 'src/core/public'; import { SimpleSavedObject } from 'src/core/public'; import { StateSetter } from '../types'; import { @@ -233,7 +229,7 @@ export async function syncExistingFields({ }: { dateRange: DateRange; indexPatterns: Array<{ title: string; timeFieldName?: string | null }>; - fetchJson: HttpServiceBase['get']; + fetchJson: HttpSetup['get']; setState: SetState; }) { const emptinessInfo = await Promise.all( diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.test.tsx index f7125a1adae528..ea9fa516d4d916 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.test.tsx @@ -9,7 +9,7 @@ import { DateHistogramIndexPatternColumn } from './date_histogram'; import { dateHistogramOperation } from '.'; import { shallow } from 'enzyme'; import { EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; -import { IUiSettingsClient, SavedObjectsClientContract, HttpServiceBase } from 'src/core/public'; +import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { createMockedIndexPattern } from '../../mocks'; import { IndexPatternPrivateState } from '../../types'; @@ -36,7 +36,7 @@ const defaultOptions = { fromDate: 'now-1y', toDate: 'now', }, - http: {} as HttpServiceBase, + http: {} as HttpSetup, }; describe('date_histogram', () => { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/index.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/index.ts index 252a3d788fd301..63e4564878e317 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/index.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IUiSettingsClient, SavedObjectsClientContract, HttpServiceBase } from 'src/core/public'; +import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { termsOperation } from './terms'; import { cardinalityOperation } from './cardinality'; @@ -47,7 +47,7 @@ export interface ParamEditorProps { uiSettings: IUiSettingsClient; storage: IStorageWrapper; savedObjectsClient: SavedObjectsClientContract; - http: HttpServiceBase; + http: HttpSetup; dateRange: DateRange; } diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx index c0d995d4207605..a891814bb04965 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { EuiRange, EuiSelect } from '@elastic/eui'; -import { IUiSettingsClient, SavedObjectsClientContract, HttpServiceBase } from 'src/core/public'; +import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { createMockedIndexPattern } from '../../mocks'; import { TermsIndexPatternColumn } from './terms'; @@ -21,7 +21,7 @@ const defaultProps = { uiSettings: {} as IUiSettingsClient, savedObjectsClient: {} as SavedObjectsClientContract, dateRange: { fromDate: 'now-1d', toDate: 'now' }, - http: {} as HttpServiceBase, + http: {} as HttpSetup, }; describe('terms', () => { diff --git a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.test.ts b/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.test.ts index 15d36a7c311690..8bb1e086a37c2b 100644 --- a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.test.ts +++ b/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.test.ts @@ -12,7 +12,7 @@ import { trackSuggestionEvent, } from './factory'; import { coreMock } from 'src/core/public/mocks'; -import { HttpServiceBase } from 'kibana/public'; +import { HttpSetup } from 'kibana/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; jest.useFakeTimers(); @@ -31,7 +31,7 @@ const createMockStorage = () => { describe('Lens UI telemetry', () => { let storage: jest.Mocked; - let http: jest.Mocked; + let http: jest.Mocked; let dateSpy: jest.SpyInstance; beforeEach(() => { diff --git a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.ts b/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.ts index 1a8ec604eda54d..babc65969afb50 100644 --- a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.ts +++ b/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.ts @@ -5,7 +5,7 @@ */ import moment from 'moment'; -import { HttpServiceBase } from 'src/core/public'; +import { HttpSetup } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { BASE_API_URL } from '../../common'; @@ -44,10 +44,10 @@ export class LensReportManager { private suggestionEvents: Record> = {}; private storage: IStorageWrapper; - private http: HttpServiceBase; + private http: HttpSetup; private timer: ReturnType; - constructor({ storage, http }: { storage: IStorageWrapper; http: HttpServiceBase }) { + constructor({ storage, http }: { storage: IStorageWrapper; http: HttpSetup }) { this.storage = storage; this.http = http; diff --git a/x-pack/legacy/plugins/maps/public/layers/layer.js b/x-pack/legacy/plugins/maps/public/layers/layer.js index 2890b75172fb02..21c5f15fb61225 100644 --- a/x-pack/legacy/plugins/maps/public/layers/layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/layer.js @@ -344,6 +344,10 @@ export class AbstractLayer { return []; } + async getFields() { + return []; + } + syncVisibilityWithMb(mbMap, mbLayerId) { mbMap.setLayoutProperty(mbLayerId, 'visibility', this.isVisible() ? 'visible' : 'none'); } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js index d8960ae1f6a044..d852332ac2f84d 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js @@ -124,6 +124,24 @@ export class ESSearchSource extends AbstractESSource { } } + async getFields() { + try { + const indexPattern = await this.getIndexPattern(); + return indexPattern.fields + .filter(field => { + // Ensure fielddata is enabled for field. + // Search does not request _source + return field.aggregatable; + }) + .map(field => { + return this.createField({ fieldName: field.name }); + }); + } catch (error) { + // failed index-pattern retrieval will show up as error-message in the layer-toc-entry + return []; + } + } + getFieldNames() { return [this._descriptor.geoField]; } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js index 814cbffc7bfe3a..bf7267e9c5858e 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js @@ -103,6 +103,10 @@ export class AbstractVectorSource extends AbstractSource { return []; } + async getFields() { + return [...(await this.getDateFields()), ...(await this.getNumberFields())]; + } + async getLeftJoinFields() { return []; } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_selection.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_selection.js index 2db92ba7f93371..84327635f2b65a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_selection.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_selection.js @@ -12,7 +12,7 @@ import { FieldSelect, fieldShape } from '../field_select'; import { ColorRampSelect } from './color_ramp_select'; import { EuiSpacer } from '@elastic/eui'; -export function DynamicColorSelection({ ordinalFields, onChange, styleOptions }) { +export function DynamicColorSelection({ fields, onChange, styleOptions }) { const onFieldChange = ({ field }) => { onChange({ ...styleOptions, field }); }; @@ -32,7 +32,7 @@ export function DynamicColorSelection({ ordinalFields, onChange, styleOptions }) /> { + onChange({ ...styleOptions, field }); + }; + + return ( + + ); +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/static_label_selector.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/static_label_selector.js new file mode 100644 index 00000000000000..ea296a3312799e --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/static_label_selector.js @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFieldText } from '@elastic/eui'; + +export function StaticLabelSelector({ onChange, styleOptions }) { + const onValueChange = event => { + onChange({ value: event.target.value }); + }; + + return ( + + ); +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/vector_style_label_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/vector_style_label_editor.js new file mode 100644 index 00000000000000..6bca56425d38d5 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/vector_style_label_editor.js @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { StaticDynamicStyleRow } from '../static_dynamic_style_row'; +import { DynamicLabelSelector } from './dynamic_label_selector'; +import { StaticLabelSelector } from './static_label_selector'; + +export function VectorStyleLabelEditor(props) { + return ( + + ); +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/dynamic_orientation_selection.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/dynamic_orientation_selection.js index 6df3283737c734..8ad3916ac65093 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/dynamic_orientation_selection.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/dynamic_orientation_selection.js @@ -10,14 +10,14 @@ import PropTypes from 'prop-types'; import { dynamicOrientationShape } from '../style_option_shapes'; import { FieldSelect, fieldShape } from '../field_select'; -export function DynamicOrientationSelection({ ordinalFields, styleOptions, onChange }) { +export function DynamicOrientationSelection({ fields, styleOptions, onChange }) { const onFieldChange = ({ field }) => { onChange({ ...styleOptions, field }); }; return ( { onChange({ ...styleOptions, field }); }; @@ -32,7 +32,7 @@ export function DynamicSizeSelection({ ordinalFields, styleOptions, onChange }) /> 0; + return this.props.fields.length > 0; } _isDynamic() { @@ -78,7 +78,7 @@ export class StaticDynamicStyleRow extends Component { return ( diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js index 43dd7b1b2d0322..44f630db9d890e 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js @@ -11,6 +11,7 @@ import chrome from 'ui/chrome'; import { VectorStyleColorEditor } from './color/vector_style_color_editor'; import { VectorStyleSizeEditor } from './size/vector_style_size_editor'; import { VectorStyleSymbolEditor } from './vector_style_symbol_editor'; +import { VectorStyleLabelEditor } from './label/vector_style_label_editor'; import { OrientationEditor } from './orientation/orientation_editor'; import { getDefaultDynamicProperties, getDefaultStaticProperties } from '../vector_style_defaults'; import { DEFAULT_FILL_COLORS, DEFAULT_LINE_COLORS } from '../../color_utils'; @@ -25,6 +26,7 @@ export class VectorStyleEditor extends Component { state = { dateFields: [], numberFields: [], + fields: [], defaultDynamicProperties: getDefaultDynamicProperties(), defaultStaticProperties: getDefaultStaticProperties(), supportedFeatures: undefined, @@ -37,16 +39,16 @@ export class VectorStyleEditor extends Component { componentDidMount() { this._isMounted = true; - this._loadOrdinalFields(); + this._loadFields(); this._loadSupportedFeatures(); } componentDidUpdate() { - this._loadOrdinalFields(); + this._loadFields(); this._loadSupportedFeatures(); } - async _loadOrdinalFields() { + async _loadFields() { const getFieldMeta = async field => { return { label: await field.getLabel(), @@ -54,21 +56,27 @@ export class VectorStyleEditor extends Component { origin: field.getOrigin(), }; }; + const dateFields = await this.props.layer.getDateFields(); const dateFieldPromises = dateFields.map(getFieldMeta); const dateFieldsArray = await Promise.all(dateFieldPromises); - if (this._isMounted && !_.isEqual(dateFieldsArray, this.state.dateFields)) { this.setState({ dateFields: dateFieldsArray }); } const numberFields = await this.props.layer.getNumberFields(); const numberFieldPromises = numberFields.map(getFieldMeta); - const numberFieldsArray = await Promise.all(numberFieldPromises); if (this._isMounted && !_.isEqual(numberFieldsArray, this.state.numberFields)) { this.setState({ numberFields: numberFieldsArray }); } + + const fields = await this.props.layer.getFields(); + const fieldPromises = fields.map(getFieldMeta); + const fieldsArray = await Promise.all(fieldPromises); + if (this._isMounted && !_.isEqual(fieldsArray, this.state.fields)) { + this.setState({ fields: fieldsArray }); + } } async _loadSupportedFeatures() { @@ -126,7 +134,7 @@ export class VectorStyleEditor extends Component { swatches={DEFAULT_FILL_COLORS} handlePropertyChange={this.props.handlePropertyChange} styleProperty={this.props.styleProperties.fillColor} - ordinalFields={this._getOrdinalFields()} + fields={this._getOrdinalFields()} defaultStaticStyleOptions={this.state.defaultStaticProperties.fillColor.options} defaultDynamicStyleOptions={this.state.defaultDynamicProperties.fillColor.options} /> @@ -139,7 +147,7 @@ export class VectorStyleEditor extends Component { swatches={DEFAULT_LINE_COLORS} handlePropertyChange={this.props.handlePropertyChange} styleProperty={this.props.styleProperties.lineColor} - ordinalFields={this._getOrdinalFields()} + fields={this._getOrdinalFields()} defaultStaticStyleOptions={this.state.defaultStaticProperties.lineColor.options} defaultDynamicStyleOptions={this.state.defaultDynamicProperties.lineColor.options} /> @@ -151,7 +159,7 @@ export class VectorStyleEditor extends Component { @@ -163,27 +171,58 @@ export class VectorStyleEditor extends Component { ); } + _renderLabelProperties() { + return ( + + + + + + + + + + + ); + } + _renderPointProperties() { let iconOrientation; if (this.props.symbolDescriptor.options.symbolizeAs === SYMBOLIZE_AS_ICON) { iconOrientation = ( - - - - + ); } @@ -207,8 +246,12 @@ export class VectorStyleEditor extends Component { {iconOrientation} + {this._renderSymbolSize()} + + + {this._renderLabelProperties()} ); } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js index 167a4be0476c91..3961325c3bd59f 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js @@ -46,6 +46,12 @@ export class DynamicColorProperty extends DynamicStyleProperty { mbMap.setPaintProperty(mbLayerId, 'line-opacity', alpha); } + syncLabelColorWithMb(mbLayerId, mbMap, alpha) { + const color = this._getMbColor(); + mbMap.setPaintProperty(mbLayerId, 'text-color', color); + mbMap.setPaintProperty(mbLayerId, 'text-opacity', alpha); + } + isCustomColorRamp() { return this._options.useCustomColorRamp; } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js index df8d11fb455a89..78d75dc818d50b 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js @@ -43,6 +43,10 @@ function getSymbolSizeIcons() { } export class DynamicSizeProperty extends DynamicStyleProperty { + supportsFeatureState() { + return this.getStyleName() !== VECTOR_STYLES.LABEL_SIZE; + } + syncHaloWidthWithMb(mbLayerId, mbMap) { const haloWidth = this._getMbSize(); mbMap.setPaintProperty(mbLayerId, 'icon-halo-width', haloWidth); @@ -89,6 +93,11 @@ export class DynamicSizeProperty extends DynamicStyleProperty { mbMap.setPaintProperty(mbLayerId, 'line-width', lineWidth); } + syncLabelSizeWithMb(mbLayerId, mbMap) { + const lineWidth = this._getMbSize(); + mbMap.setLayoutProperty(mbLayerId, 'text-size', lineWidth); + } + _getMbSize() { if (this._isSizeDynamicConfigComplete(this._options)) { return this._getMbDataDrivenSize({ @@ -101,10 +110,11 @@ export class DynamicSizeProperty extends DynamicStyleProperty { } _getMbDataDrivenSize({ targetName, minSize, maxSize }) { + const lookup = this.supportsFeatureState() ? 'feature-state' : 'get'; return [ 'interpolate', ['linear'], - ['coalesce', ['feature-state', targetName], 0], + ['coalesce', [lookup, targetName], 0], 0, minSize, 1, diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js index 4b7cac51d4fa7c..8d734a702c8ca6 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js @@ -33,6 +33,10 @@ export class DynamicStyleProperty extends AbstractStyleProperty { return true; } + isOrdinal() { + return true; + } + isComplete() { return !!this._field; } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_text_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_text_property.js new file mode 100644 index 00000000000000..b716030d2f2639 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_text_property.js @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DynamicStyleProperty } from './dynamic_style_property'; +import { getComputedFieldName } from '../style_util'; + +export class DynamicTextProperty extends DynamicStyleProperty { + syncTextFieldWithMb(mbLayerId, mbMap) { + if (this._field && this._field.isValid()) { + const targetName = getComputedFieldName(this._styleName, this._options.field.name); + mbMap.setLayoutProperty(mbLayerId, 'text-field', ['coalesce', ['get', targetName], '']); + } else { + mbMap.setLayoutProperty(mbLayerId, 'text-field', null); + } + } + + isOrdinal() { + return false; + } + + supportsFieldMeta() { + return false; + } + + supportsFeatureState() { + return false; + } + + isScaled() { + return false; + } + + renderHeader() { + return null; + } +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/static_color_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/static_color_property.js index a32b17af0be9e2..658eb6a164556f 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/static_color_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/static_color_property.js @@ -30,8 +30,13 @@ export class StaticColorProperty extends StaticStyleProperty { mbMap.setPaintProperty(mbLayerId, 'line-opacity', alpha); } - syncCircleStrokeWithMb(pointLayerId, mbMap, alpha) { - mbMap.setPaintProperty(pointLayerId, 'circle-stroke-color', this._options.color); - mbMap.setPaintProperty(pointLayerId, 'circle-stroke-opacity', alpha); + syncCircleStrokeWithMb(mbLayerId, mbMap, alpha) { + mbMap.setPaintProperty(mbLayerId, 'circle-stroke-color', this._options.color); + mbMap.setPaintProperty(mbLayerId, 'circle-stroke-opacity', alpha); + } + + syncLabelColorWithMb(mbLayerId, mbMap, alpha) { + mbMap.setPaintProperty(mbLayerId, 'text-color', this._options.color); + mbMap.setPaintProperty(mbLayerId, 'text-opacity', alpha); } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/static_size_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/static_size_property.js index d2682a07def623..1584dec9989866 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/static_size_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/static_size_property.js @@ -43,4 +43,8 @@ export class StaticSizeProperty extends StaticStyleProperty { syncLineWidthWithMb(mbLayerId, mbMap) { mbMap.setPaintProperty(mbLayerId, 'line-width', this._options.size); } + + syncLabelSizeWithMb(mbLayerId, mbMap) { + mbMap.setLayoutProperty(mbLayerId, 'text-size', this._options.size); + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/static_text_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/static_text_property.js new file mode 100644 index 00000000000000..7a4a4672152c0a --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/static_text_property.js @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { StaticStyleProperty } from './static_style_property'; + +export class StaticTextProperty extends StaticStyleProperty { + isComplete() { + return this.getOptions().value.length > 0; + } + + syncTextFieldWithMb(mbLayerId, mbMap) { + if (this.getOptions().value.length) { + mbMap.setLayoutProperty(mbLayerId, 'text-field', this.getOptions().value); + } else { + mbMap.setLayoutProperty(mbLayerId, 'text-field', null); + } + } +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js index 65698c67a229ed..9d9764b5fb1fb0 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js @@ -36,6 +36,8 @@ import { StaticColorProperty } from './properties/static_color_property'; import { DynamicColorProperty } from './properties/dynamic_color_property'; import { StaticOrientationProperty } from './properties/static_orientation_property'; import { DynamicOrientationProperty } from './properties/dynamic_orientation_property'; +import { StaticTextProperty } from './properties/static_text_property'; +import { DynamicTextProperty } from './properties/dynamic_text_property'; const POINTS = [GEO_JSON_TYPE.POINT, GEO_JSON_TYPE.MULTI_POINT]; const LINES = [GEO_JSON_TYPE.LINE_STRING, GEO_JSON_TYPE.MULTI_LINE_STRING]; @@ -85,6 +87,17 @@ export class VectorStyle extends AbstractStyle { this._descriptor.properties[VECTOR_STYLES.ICON_ORIENTATION], VECTOR_STYLES.ICON_ORIENTATION ); + this._labelStyleProperty = this._makeLabelProperty( + this._descriptor.properties[VECTOR_STYLES.LABEL_TEXT] + ); + this._labelSizeStyleProperty = this._makeSizeProperty( + this._descriptor.properties[VECTOR_STYLES.LABEL_SIZE], + VECTOR_STYLES.LABEL_SIZE + ); + this._labelColorStyleProperty = this._makeColorProperty( + this._descriptor.properties[VECTOR_STYLES.LABEL_COLOR], + VECTOR_STYLES.LABEL_COLOR + ); } _getAllStyleProperties() { @@ -94,6 +107,9 @@ export class VectorStyle extends AbstractStyle { this._lineWidthStyleProperty, this._iconSizeStyleProperty, this._iconOrientationProperty, + this._labelStyleProperty, + this._labelSizeStyleProperty, + this._labelColorStyleProperty, ]; } @@ -114,16 +130,15 @@ export class VectorStyle extends AbstractStyle { return dynamicStyleProp.isFieldMetaEnabled(); }); + const styleProperties = {}; + this._getAllStyleProperties().forEach(styleProperty => { + styleProperties[styleProperty.getStyleName()] = styleProperty; + }); + return ( { + const styleName = styleProperty.getStyleName(); + if ([VECTOR_STYLES.ICON_ORIENTATION, VECTOR_STYLES.LABEL_TEXT].includes(styleName)) { + return false; + } + if (isLinesOnly) { - return LINE_STYLES.includes(styleProperty.getStyleName()); + return LINE_STYLES.includes(styleName); } if (isPolygonsOnly) { - return POLYGON_STYLES.includes(styleProperty.getStyleName()); + return POLYGON_STYLES.includes(styleName); } return true; @@ -420,6 +440,7 @@ export class VectorStyle extends AbstractStyle { // To work around this limitation, some styling values must fall back to geojson property values. let supportsFeatureState; let isScaled; + // TODO move first check into DynamicSizeProperty.supportsFeatureState if ( styleProperty.getStyleName() === VECTOR_STYLES.ICON_SIZE && this._descriptor.properties.symbol.options.symbolizeAs === SYMBOLIZE_AS_ICON @@ -435,8 +456,10 @@ export class VectorStyle extends AbstractStyle { return { supportsFeatureState, isScaled, + isOrdinal: styleProperty.isOrdinal(), name: field.getName(), meta: this._getFieldMeta(field.getName()), + formatter: this._getFieldFormatter(field.getName()), computedName: getComputedFieldName(styleProperty.getStyleName(), field.getName()), }; }); @@ -455,6 +478,20 @@ export class VectorStyle extends AbstractStyle { } } + _getOrdinalValue(value, isScaled, range) { + const valueAsFloat = parseFloat(value); + + if (isScaled) { + return scaleValue(valueAsFloat, range); + } + + if (isNaN(valueAsFloat)) { + return 0; + } + + return valueAsFloat; + } + setFeatureStateAndStyleProps(featureCollection, mbMap, mbSourceId) { if (!featureCollection) { return; @@ -479,20 +516,20 @@ export class VectorStyle extends AbstractStyle { const { supportsFeatureState, isScaled, + isOrdinal, name, meta: range, + formatter, computedName, } = featureStateParams[j]; - const value = parseFloat(feature.properties[name]); + let styleValue; - if (isScaled) { - styleValue = scaleValue(value, range); + if (isOrdinal) { + styleValue = this._getOrdinalValue(feature.properties[name], isScaled, range); + } else if (formatter) { + styleValue = formatter(feature.properties[name]); } else { - if (isNaN(value)) { - styleValue = 0; - } else { - styleValue = value; - } + styleValue = feature.properties[name]; } if (supportsFeatureState) { @@ -530,6 +567,14 @@ export class VectorStyle extends AbstractStyle { this._iconSizeStyleProperty.syncCircleRadiusWithMb(pointLayerId, mbMap); } + setMBPropertiesForLabelText({ alpha, mbMap, textLayerId }) { + mbMap.setLayoutProperty(textLayerId, 'icon-allow-overlap', true); + mbMap.setLayoutProperty(textLayerId, 'text-allow-overlap', true); + this._labelStyleProperty.syncTextFieldWithMb(textLayerId, mbMap); + this._labelColorStyleProperty.syncLabelColorWithMb(textLayerId, mbMap, alpha); + this._labelSizeStyleProperty.syncLabelSizeWithMb(textLayerId, mbMap); + } + setMBSymbolPropertiesForPoints({ mbMap, symbolLayerId, alpha }) { const symbolId = this._descriptor.properties.symbol.options.symbolId; mbMap.setLayoutProperty(symbolLayerId, 'icon-ignore-placement', true); @@ -619,4 +664,17 @@ export class VectorStyle extends AbstractStyle { throw new Error(`${descriptor} not implemented`); } } + + _makeLabelProperty(descriptor) { + if (!descriptor || !descriptor.options) { + return new StaticTextProperty({ value: '' }, VECTOR_STYLES.LABEL_TEXT); + } else if (descriptor.type === StaticStyleProperty.type) { + return new StaticTextProperty(descriptor.options, VECTOR_STYLES.LABEL_TEXT); + } else if (descriptor.type === DynamicStyleProperty.type) { + const field = this._makeField(descriptor.options.field); + return new DynamicTextProperty(descriptor.options, VECTOR_STYLES.LABEL_TEXT, field); + } else { + throw new Error(`${descriptor} not implemented`); + } + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.test.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.test.js index 1242d7307dc481..aa0badd5583d5b 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.test.js @@ -96,6 +96,24 @@ describe('getDescriptorWithMissingStylePropsRemoved', () => { }, type: 'DYNAMIC', }, + labelText: { + options: { + value: '', + }, + type: 'STATIC', + }, + labelColor: { + options: { + color: '#000000', + }, + type: 'STATIC', + }, + labelSize: { + options: { + size: 14, + }, + type: 'STATIC', + }, lineColor: { options: {}, type: 'DYNAMIC', diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js index 3f2851fa092cd3..4bae90c3165f20 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js @@ -7,11 +7,14 @@ import { VectorStyle } from './vector_style'; import { SYMBOLIZE_AS_CIRCLE, DEFAULT_ICON_SIZE } from './vector_constants'; import { COLOR_GRADIENTS, DEFAULT_FILL_COLORS, DEFAULT_LINE_COLORS } from '../color_utils'; +import chrome from 'ui/chrome'; const DEFAULT_ICON = 'airfield'; export const MIN_SIZE = 1; export const MAX_SIZE = 64; +export const DEFAULT_MIN_SIZE = 4; +export const DEFAULT_MAX_SIZE = 32; export const DEFAULT_SIGMA = 3; export const VECTOR_STYLES = { @@ -21,6 +24,9 @@ export const VECTOR_STYLES = { LINE_WIDTH: 'lineWidth', ICON_SIZE: 'iconSize', ICON_ORIENTATION: 'iconOrientation', + LABEL_TEXT: 'labelText', + LABEL_COLOR: 'labelColor', + LABEL_SIZE: 'labelSize', }; export const LINE_STYLES = [VECTOR_STYLES.LINE_COLOR, VECTOR_STYLES.LINE_WIDTH]; @@ -49,6 +55,8 @@ export function getDefaultStaticProperties(mapColors = []) { const nextFillColor = DEFAULT_FILL_COLORS[nextColorIndex]; const nextLineColor = DEFAULT_LINE_COLORS[nextColorIndex]; + const isDarkMode = chrome.getUiSettingsClient().get('theme:darkMode', false); + return { [VECTOR_STYLES.FILL_COLOR]: { type: VectorStyle.STYLE_TYPE.STATIC, @@ -80,6 +88,24 @@ export function getDefaultStaticProperties(mapColors = []) { orientation: 0, }, }, + [VECTOR_STYLES.LABEL_TEXT]: { + type: VectorStyle.STYLE_TYPE.STATIC, + options: { + value: '', + }, + }, + [VECTOR_STYLES.LABEL_COLOR]: { + type: VectorStyle.STYLE_TYPE.STATIC, + options: { + color: isDarkMode ? '#FFFFFF' : '#000000', + }, + }, + [VECTOR_STYLES.LABEL_SIZE]: { + type: VectorStyle.STYLE_TYPE.STATIC, + options: { + size: 14, + }, + }, }; } @@ -122,8 +148,8 @@ export function getDefaultDynamicProperties() { [VECTOR_STYLES.ICON_SIZE]: { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { - minSize: 4, - maxSize: 32, + minSize: DEFAULT_MIN_SIZE, + maxSize: DEFAULT_MAX_SIZE, field: undefined, fieldMetaOptions: { isEnabled: true, @@ -141,5 +167,34 @@ export function getDefaultDynamicProperties() { }, }, }, + [VECTOR_STYLES.LABEL_TEXT]: { + type: VectorStyle.STYLE_TYPE.STATIC, + options: { + field: undefined, + }, + }, + [VECTOR_STYLES.LABEL_COLOR]: { + type: VectorStyle.STYLE_TYPE.STATIC, + options: { + color: COLOR_GRADIENTS[0].value, + field: undefined, + fieldMetaOptions: { + isEnabled: true, + sigma: DEFAULT_SIGMA, + }, + }, + }, + [VECTOR_STYLES.LABEL_SIZE]: { + type: VectorStyle.STYLE_TYPE.STATIC, + options: { + minSize: DEFAULT_MIN_SIZE, + maxSize: DEFAULT_MAX_SIZE, + field: undefined, + fieldMetaOptions: { + isEnabled: true, + sigma: DEFAULT_SIGMA, + }, + }, + }, }; } diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js index 30c47658bb3279..6ebc1b3d952500 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js @@ -191,24 +191,33 @@ export class VectorLayer extends AbstractLayer { return this._source.getDisplayName(); } + _getJoinFields() { + const joinFields = []; + this.getValidJoins().forEach(join => { + const fields = join.getJoinFields(); + joinFields.push(...fields); + }); + return joinFields; + } + async getDateFields() { return await this._source.getDateFields(); } async getNumberFields() { const numberFieldOptions = await this._source.getNumberFields(); - const joinFields = []; - this.getValidJoins().forEach(join => { - const fields = join.getJoinFields(); - joinFields.push(...fields); - }); - return [...numberFieldOptions, ...joinFields]; + return [...numberFieldOptions, ...this._getJoinFields()]; } async getOrdinalFields() { return [...(await this.getDateFields()), ...(await this.getNumberFields())]; } + async getFields() { + const sourceFields = await this._source.getFields(); + return [...sourceFields, ...this._getJoinFields()]; + } + getIndexPatternIds() { const indexPatternIds = this._source.getIndexPatternIds(); this.getValidJoins().forEach(join => { @@ -621,30 +630,40 @@ export class VectorLayer extends AbstractLayer { const pointLayer = mbMap.getLayer(pointLayerId); const symbolLayer = mbMap.getLayer(symbolLayerId); - let mbLayerId; + // Point layers symbolized as circles require 2 mapbox layers because + // "circle" layers do not support "text" style properties + // Point layers symbolized as icons only contain a single mapbox layer. + let markerLayerId; + let textLayerId; if (this._style.arePointsSymbolizedAsCircles()) { - mbLayerId = pointLayerId; + markerLayerId = pointLayerId; + textLayerId = this._getMbTextLayerId(); if (symbolLayer) { mbMap.setLayoutProperty(symbolLayerId, 'visibility', 'none'); } this._setMbCircleProperties(mbMap); } else { - mbLayerId = symbolLayerId; + markerLayerId = symbolLayerId; + textLayerId = symbolLayerId; if (pointLayer) { mbMap.setLayoutProperty(pointLayerId, 'visibility', 'none'); + mbMap.setLayoutProperty(this._getMbTextLayerId(), 'visibility', 'none'); } this._setMbSymbolProperties(mbMap); } - this.syncVisibilityWithMb(mbMap, mbLayerId); - mbMap.setLayerZoomRange(mbLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); + this.syncVisibilityWithMb(mbMap, markerLayerId); + mbMap.setLayerZoomRange(markerLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); + if (markerLayerId !== textLayerId) { + this.syncVisibilityWithMb(mbMap, textLayerId); + mbMap.setLayerZoomRange(textLayerId, this._descriptor.minZoom, this._descriptor.maxZoom); + } } _setMbCircleProperties(mbMap) { const sourceId = this.getId(); const pointLayerId = this._getMbPointLayerId(); const pointLayer = mbMap.getLayer(pointLayerId); - if (!pointLayer) { mbMap.addLayer({ id: pointLayerId, @@ -654,15 +673,32 @@ export class VectorLayer extends AbstractLayer { }); } + const textLayerId = this._getMbTextLayerId(); + const textLayer = mbMap.getLayer(textLayerId); + if (!textLayer) { + mbMap.addLayer({ + id: textLayerId, + type: 'symbol', + source: sourceId, + }); + } + const filterExpr = getPointFilterExpression(this._hasJoins()); if (filterExpr !== mbMap.getFilter(pointLayerId)) { mbMap.setFilter(pointLayerId, filterExpr); + mbMap.setFilter(textLayerId, filterExpr); } this._style.setMBPaintPropertiesForPoints({ alpha: this.getAlpha(), mbMap, - pointLayerId: pointLayerId, + pointLayerId, + }); + + this._style.setMBPropertiesForLabelText({ + alpha: this.getAlpha(), + mbMap, + textLayerId, }); } @@ -687,7 +723,13 @@ export class VectorLayer extends AbstractLayer { this._style.setMBSymbolPropertiesForPoints({ alpha: this.getAlpha(), mbMap, - symbolLayerId: symbolLayerId, + symbolLayerId, + }); + + this._style.setMBPropertiesForLabelText({ + alpha: this.getAlpha(), + mbMap, + textLayerId: symbolLayerId, }); } @@ -759,6 +801,10 @@ export class VectorLayer extends AbstractLayer { return this.makeMbLayerId('circle'); } + _getMbTextLayerId() { + return this.makeMbLayerId('text'); + } + _getMbSymbolLayerId() { return this.makeMbLayerId('symbol'); } @@ -774,6 +820,7 @@ export class VectorLayer extends AbstractLayer { getMbLayerIds() { return [ this._getMbPointLayerId(), + this._getMbTextLayerId(), this._getMbSymbolLayerId(), this._getMbLineLayerId(), this._getMbPolygonLayerId(), @@ -781,12 +828,7 @@ export class VectorLayer extends AbstractLayer { } ownsMbLayerId(mbLayerId) { - return ( - this._getMbPointLayerId() === mbLayerId || - this._getMbLineLayerId() === mbLayerId || - this._getMbPolygonLayerId() === mbLayerId || - this._getMbSymbolLayerId() === mbLayerId - ); + return this.getMbLayerIds().includes(mbLayerId); } ownsMbSourceId(mbSourceId) { diff --git a/x-pack/legacy/plugins/ml/public/application/components/validate_job/__snapshots__/validate_job_view.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/validate_job/__snapshots__/validate_job_view.test.js.snap index 9ba92a3aa1677c..29d10eb99ff4ea 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/validate_job/__snapshots__/validate_job_view.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/components/validate_job/__snapshots__/validate_job_view.test.js.snap @@ -60,7 +60,7 @@ exports[`ValidateJob renders button and modal with a message 1`] = ` values={ Object { "mlJobTipsLink": { return messages; } + const createJobsDocsUrl = `https://www.elastic.co/guide/en/elastic-stack-overview/{{version}}/create-jobs.html`; + return (messages = { field_not_aggregatable: { status: 'ERROR', @@ -43,7 +45,7 @@ export const getMessages = () => { fieldName: 'by_field "{{fieldName}}"', }, }), - url: 'https://www.elastic.co/guide/en/kibana/{{version}}/job-tips.html#cardinality', + url: `${createJobsDocsUrl}#cardinality`, }, cardinality_over_field_low: { status: 'WARNING', @@ -57,7 +59,7 @@ export const getMessages = () => { }, } ), - url: 'https://www.elastic.co/guide/en/kibana/{{version}}/job-tips.html#cardinality', + url: `${createJobsDocsUrl}#cardinality`, }, cardinality_over_field_high: { status: 'WARNING', @@ -71,7 +73,7 @@ export const getMessages = () => { }, } ), - url: 'https://www.elastic.co/guide/en/kibana/{{version}}/job-tips.html#cardinality', + url: `${createJobsDocsUrl}#cardinality`, }, cardinality_partition_field: { status: 'WARNING', @@ -85,7 +87,7 @@ export const getMessages = () => { }, } ), - url: 'https://www.elastic.co/guide/en/kibana/{{version}}/job-tips.html#cardinality', + url: `${createJobsDocsUrl}#cardinality`, }, cardinality_model_plot_high: { status: 'WARNING', @@ -155,7 +157,7 @@ export const getMessages = () => { }, } ), - url: 'https://www.elastic.co/guide/en/kibana/{{version}}/job-tips.html#bucket-span', + url: `${createJobsDocsUrl}#bucket-span`, }, bucket_span_high: { status: 'INFO', @@ -166,7 +168,7 @@ export const getMessages = () => { defaultMessage: '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/kibana/{{version}}/job-tips.html#bucket-span', + url: `${createJobsDocsUrl}#bucket-span`, }, bucket_span_valid: { status: 'SUCCESS', @@ -209,21 +211,21 @@ export const getMessages = () => { partitionFieldNameParam: `'partition_field_name'`, }, }), - url: 'https://www.elastic.co/guide/en/kibana/{{version}}/job-tips.html#detectors', + url: `${createJobsDocsUrl}#detectors`, }, detectors_empty: { status: 'ERROR', text: i18n.translate('xpack.ml.models.jobValidation.messages.detectorsEmptyMessage', { defaultMessage: 'No detectors were found. At least one detector must be specified.', }), - url: 'https://www.elastic.co/guide/en/kibana/{{version}}/job-tips.html#detectors', + url: `${createJobsDocsUrl}#detectors`, }, detectors_function_empty: { status: 'ERROR', text: i18n.translate('xpack.ml.models.jobValidation.messages.detectorsFunctionEmptyMessage', { defaultMessage: 'One of the detector functions is empty.', }), - url: 'https://www.elastic.co/guide/en/kibana/{{version}}/job-tips.html#detectors', + url: `${createJobsDocsUrl}#detectors`, }, detectors_function_not_empty: { status: 'SUCCESS', @@ -239,7 +241,7 @@ export const getMessages = () => { defaultMessage: 'Presence of detector functions validated in all detectors.', } ), - url: 'https://www.elastic.co/guide/en/kibana/{{version}}/job-tips.html#detectors', + url: `${createJobsDocsUrl}#detectors`, }, index_fields_invalid: { status: 'ERROR', @@ -260,7 +262,7 @@ export const getMessages = () => { 'The job configuration includes more than 3 influencers. ' + 'Consider using fewer influencers or creating multiple jobs.', }), - url: 'https://www.elastic.co/guide/en/kibana/{{version}}/job-tips.html#influencers', + url: 'https://www.elastic.co/guide/en/machine-learning/{{version}}/ml-influencers.html', }, influencer_low: { status: 'WARNING', @@ -268,7 +270,7 @@ export const getMessages = () => { defaultMessage: 'No influencers have been configured. Picking an influencer is strongly recommended.', }), - url: 'https://www.elastic.co/guide/en/kibana/{{version}}/job-tips.html#influencers', + url: 'https://www.elastic.co/guide/en/machine-learning/{{version}}/ml-influencers.html', }, influencer_low_suggestion: { status: 'WARNING', @@ -280,7 +282,7 @@ export const getMessages = () => { values: { influencerSuggestion: '{{influencerSuggestion}}' }, } ), - url: 'https://www.elastic.co/guide/en/kibana/{{version}}/job-tips.html#influencers', + url: 'https://www.elastic.co/guide/en/machine-learning/{{version}}/ml-influencers.html', }, influencer_low_suggestions: { status: 'WARNING', @@ -292,7 +294,7 @@ export const getMessages = () => { values: { influencerSuggestion: '{{influencerSuggestion}}' }, } ), - url: 'https://www.elastic.co/guide/en/kibana/{{version}}/job-tips.html#influencers', + url: 'https://www.elastic.co/guide/en/machine-learning/{{version}}/ml-influencers.html', }, job_id_empty: { status: 'ERROR', @@ -401,7 +403,7 @@ export const getMessages = () => { text: i18n.translate('xpack.ml.models.jobValidation.messages.successCardinalityMessage', { defaultMessage: 'Cardinality of detector fields is within recommended bounds.', }), - url: 'https://www.elastic.co/guide/en/kibana/{{version}}/job-tips.html#cardinality', + url: `${createJobsDocsUrl}#cardinality`, }, success_bucket_span: { status: 'SUCCESS', @@ -412,14 +414,14 @@ export const getMessages = () => { defaultMessage: 'Format of {bucketSpan} is valid and passed validation checks.', values: { bucketSpan: '"{{bucketSpan}}"' }, }), - url: 'https://www.elastic.co/guide/en/kibana/{{version}}/job-tips.html#bucket-span', + url: `${createJobsDocsUrl}#bucket-span`, }, success_influencers: { status: 'SUCCESS', text: i18n.translate('xpack.ml.models.jobValidation.messages.successInfluencersMessage', { defaultMessage: 'Influencer configuration passed the validation checks.', }), - url: 'https://www.elastic.co/guide/en/kibana/{{version}}/job-tips.html#influencers', + url: 'https://www.elastic.co/guide/en/machine-learning/{{version}}/ml-influencers.html', }, estimated_mml_greater_than_max_mml: { status: 'WARNING', @@ -446,7 +448,7 @@ export const getMessages = () => { '1MB and should be specified in bytes e.g. 10MB.', values: { mml: '{{mml}}' }, }), - url: 'https://www.elastic.co/guide/en/kibana/{{version}}/job-tips.html#model-memory-limits', + url: `${createJobsDocsUrl}#model-memory-limits`, }, half_estimated_mml_greater_than_mml: { status: 'WARNING', @@ -458,7 +460,7 @@ export const getMessages = () => { 'memory limit and will likely hit the hard limit.', } ), - url: 'https://www.elastic.co/guide/en/kibana/{{version}}/job-tips.html#model-memory-limits', + url: `${createJobsDocsUrl}#model-memory-limits`, }, estimated_mml_greater_than_mml: { status: 'INFO', @@ -469,7 +471,7 @@ export const getMessages = () => { 'The estimated model memory limit is greater than the model memory limit you have configured.', } ), - url: 'https://www.elastic.co/guide/en/kibana/{{version}}/job-tips.html#model-memory-limits', + url: `${createJobsDocsUrl}#model-memory-limits`, }, success_mml: { status: 'SUCCESS', @@ -479,7 +481,7 @@ export const getMessages = () => { text: i18n.translate('xpack.ml.models.jobValidation.messages.successMmlMessage', { defaultMessage: 'Valid and within the estimated model memory limit.', }), - url: 'https://www.elastic.co/guide/en/kibana/{{version}}/job-tips.html#model-memory-limits', + url: `${createJobsDocsUrl}#model-memory-limits`, }, success_time_range: { status: 'SUCCESS', diff --git a/x-pack/legacy/plugins/monitoring/index.js b/x-pack/legacy/plugins/monitoring/index.js index f78f8d45d6440b..3d98b11c2045b1 100644 --- a/x-pack/legacy/plugins/monitoring/index.js +++ b/x-pack/legacy/plugins/monitoring/index.js @@ -68,12 +68,13 @@ export const monitoring = kibana => _hapi: server, _kbnServer: this.kbnServer, }; - const { usageCollection } = server.newPlatform.setup.plugins; + const { usageCollection, licensing } = server.newPlatform.setup.plugins; const plugins = { xpack_main: server.plugins.xpack_main, elasticsearch: server.plugins.elasticsearch, infra: server.plugins.infra, usageCollection, + licensing, }; new Plugin().setup(serverFacade, plugins); diff --git a/x-pack/legacy/plugins/monitoring/server/__tests__/check_license.js b/x-pack/legacy/plugins/monitoring/server/__tests__/check_license.js new file mode 100644 index 00000000000000..60b27cae308782 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/__tests__/check_license.js @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { BehaviorSubject } from 'rxjs'; +import expect from '@kbn/expect'; +import sinon from 'sinon'; + +import { XPackInfo } from '../../../xpack_main/server/lib/xpack_info'; +import { licensingMock } from '../../../../../plugins/licensing/server/mocks'; + +const createLicense = (type = 'basic') => { + return licensingMock.createLicense({ + license: { + uid: 'custom-uid', + type, + mode: 'basic', + status: 'active', + expiryDateInMillis: 1286575200000, + }, + features: { + monitoring: { + description: '...', + isAvailable: true, + isEnabled: true, + }, + }, + }); +}; + +describe('XPackInfo', () => { + let mockServer; + let mockElasticsearchPlugin; + + beforeEach(() => { + mockServer = sinon.stub({ + plugins: { elasticsearch: mockElasticsearchPlugin }, + events: { on() {} }, + newPlatform: { + setup: { + plugins: { + licensing: {}, + }, + }, + }, + }); + }); + + describe('refreshNow()', () => { + it('check new platform licensing plugin', async () => { + const refresh = sinon.spy(); + const license$ = new BehaviorSubject(createLicense()); + const xPackInfo = new XPackInfo(mockServer, { + licensing: { + license$, + refresh, + }, + }); + + let changed = false; + license$.subscribe(() => (changed = true)); + await xPackInfo.refreshNow(); + expect(changed).to.be(true); + sinon.assert.calledOnce(refresh); + }); + }); + + describe('Change type', () => { + it('trigger event when license type changes', async () => { + const license$ = new BehaviorSubject(createLicense()); + const refresh = () => void 0; + const xPackInfo = new XPackInfo(mockServer, { + licensing: { + license$, + refresh, + }, + }); + let changed = false; + license$.subscribe(() => (changed = true)); + await license$.next(createLicense('gold')); + expect(xPackInfo.license.getType()).to.be('gold'); + expect(changed).to.be(true); + }); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/plugin.js b/x-pack/legacy/plugins/monitoring/server/plugin.js index 013f8ae681bad5..9ce4b355d9c0db 100644 --- a/x-pack/legacy/plugins/monitoring/server/plugin.js +++ b/x-pack/legacy/plugins/monitoring/server/plugin.js @@ -17,6 +17,7 @@ export class Plugin { const kbnServer = core._kbnServer; const config = core.config(); const usageCollection = plugins.usageCollection; + const licensing = plugins.licensing; registerMonitoringCollection(); /* * Register collector objects for stats to show up in the APIs @@ -91,17 +92,16 @@ export class Plugin { kbnServerVersion: kbnServer.version, }); const kibanaCollectionEnabled = config.get('xpack.monitoring.kibana.collection.enabled'); - const { info: xpackMainInfo } = xpackMainPlugin; if (kibanaCollectionEnabled) { /* * Bulk uploading of Kibana stats */ - xpackMainInfo.onLicenseInfoChange(() => { + licensing.license$.subscribe(license => { // use updated xpack license info to start/stop bulk upload - const mainMonitoring = xpackMainInfo.feature('monitoring'); + const mainMonitoring = license.getFeature('monitoring'); const monitoringBulkEnabled = - mainMonitoring && mainMonitoring.isAvailable() && mainMonitoring.isEnabled(); + mainMonitoring && mainMonitoring.isAvailable && mainMonitoring.isEnabled; if (monitoringBulkEnabled) { bulkUploader.start(usageCollection); } else { diff --git a/x-pack/legacy/plugins/siem/common/constants.ts b/x-pack/legacy/plugins/siem/common/constants.ts index 91e6d9d5e3be55..4383329fea0723 100644 --- a/x-pack/legacy/plugins/siem/common/constants.ts +++ b/x-pack/legacy/plugins/siem/common/constants.ts @@ -66,3 +66,8 @@ export const SIGNALS_INDEX_KEY = 'signalsIndex'; export const DETECTION_ENGINE_SIGNALS_URL = `${DETECTION_ENGINE_URL}/signals`; export const DETECTION_ENGINE_SIGNALS_STATUS_URL = `${DETECTION_ENGINE_SIGNALS_URL}/status`; export const DETECTION_ENGINE_QUERY_SIGNALS_URL = `${DETECTION_ENGINE_SIGNALS_URL}/search`; + +/** + * Common naming convention for an unauthenticated user + */ +export const UNAUTHENTICATED_USER = 'Unauthenticated'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.tsx index 0dcf81fd26310d..79bdf5c4b83157 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.tsx @@ -140,7 +140,7 @@ const HostsTableComponent = React.memo( if (criteria.sort != null) { const sort: HostsSortField = { field: getSortField(criteria.sort.field), - direction: criteria.sort.direction, + direction: criteria.sort.direction as Direction, }; if (sort.direction !== direction || sort.field !== sortField) { updateHostsSort({ diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.tsx index 34fad309f23468..e316f951a03631 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.tsx @@ -10,7 +10,12 @@ import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; import { networkActions } from '../../../../store/actions'; -import { NetworkDnsEdges, NetworkDnsFields, NetworkDnsSortField } from '../../../../graphql/types'; +import { + Direction, + NetworkDnsEdges, + NetworkDnsFields, + NetworkDnsSortField, +} from '../../../../graphql/types'; import { networkModel, networkSelectors, State } from '../../../../store'; import { Criteria, ItemsPerRow, PaginatedTable } from '../../../paginated_table'; @@ -102,7 +107,7 @@ export const NetworkDnsTableComponent = React.memo( if (criteria.sort != null) { const newDnsSortField: NetworkDnsSortField = { field: criteria.sort.field.split('.')[1] as NetworkDnsFields, - direction: criteria.sort.direction, + direction: criteria.sort.direction as Direction, }; if (!isEqual(newDnsSortField, sort)) { updateNetworkTable({ diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.tsx index 8fd5cc9b3f3c08..6d14b52d3586d5 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.tsx @@ -141,7 +141,7 @@ const NetworkTopCountriesTableComponent = React.memo( const newSortDirection = field !== sort.field ? Direction.desc : criteria.sort.direction; // sort by desc on init click const newTopNFlowSort: NetworkTopTablesSortField = { field: field as NetworkTopTablesFields, - direction: newSortDirection, + direction: newSortDirection as Direction, }; if (!isEqual(newTopNFlowSort, sort)) { updateNetworkTable({ diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.tsx index 026ab9537d3d74..95c0fff054440d 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.tsx @@ -11,7 +11,7 @@ import { compose } from 'redux'; import { ActionCreator } from 'typescript-fsa'; import { networkActions } from '../../../../store/network'; -import { TlsEdges, TlsSortField, TlsFields } from '../../../../graphql/types'; +import { TlsEdges, TlsSortField, TlsFields, Direction } from '../../../../graphql/types'; import { networkModel, networkSelectors, State } from '../../../../store'; import { Criteria, ItemsPerRow, PaginatedTable, SortingBasicTable } from '../../../paginated_table'; import { getTlsColumns } from './columns'; @@ -105,7 +105,7 @@ const TlsTableComponent = React.memo( const splitField = criteria.sort.field.split('.'); const newTlsSort: TlsSortField = { field: getSortFromString(splitField[splitField.length - 1]), - direction: criteria.sort.direction, + direction: criteria.sort.direction as Direction, }; if (!isEqual(newTlsSort, sort)) { updateNetworkTable({ diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.tsx index 14fbf4860f7c03..f4f14c7c009dc0 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.tsx @@ -10,7 +10,13 @@ import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; import { networkActions } from '../../../../store/network'; -import { FlowTarget, UsersEdges, UsersFields, UsersSortField } from '../../../../graphql/types'; +import { + Direction, + FlowTarget, + UsersEdges, + UsersFields, + UsersSortField, +} from '../../../../graphql/types'; import { networkModel, networkSelectors, State } from '../../../../store'; import { Criteria, ItemsPerRow, PaginatedTable, SortingBasicTable } from '../../../paginated_table'; @@ -104,7 +110,7 @@ const UsersTableComponent = React.memo( const splitField = criteria.sort.field.split('.'); const newUsersSort: UsersSortField = { field: getSortFromString(splitField[splitField.length - 1]), - direction: criteria.sort.direction, + direction: criteria.sort.direction as Direction, }; if (!isEqual(newUsersSort, sort)) { updateNetworkTable({ diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts index a26f376f962d71..a3ee878e305b48 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts @@ -14,6 +14,7 @@ import { FetchRulesResponse, NewRule, Rule, + FetchRuleProps, } from './types'; import { throwIfNotOk } from '../../../hooks/api/api'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; @@ -27,7 +28,7 @@ import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; */ export const addRule = async ({ rule, kbnVersion, signal }: AddRulesProps): Promise => { const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}`, { - method: 'POST', + method: rule.id != null ? 'PUT' : 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json', @@ -96,6 +97,28 @@ export const fetchRules = async ({ : response.json(); }; +/** + * Fetch a Rule by providing a Rule ID + * + * @param id Rule ID's (not rule_id) + * @param kbnVersion current Kibana Version to use for headers + */ +export const fetchRuleById = async ({ id, kbnVersion, signal }: FetchRuleProps): Promise => { + const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}?id=${id}`, { + method: 'GET', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-version': kbnVersion, + 'kbn-xsrf': kbnVersion, + }, + signal, + }); + await throwIfNotOk(response); + const rule: Rule = await response.json(); + return rule; +}; + /** * Enables/Disables provided Rule ID's * @@ -177,11 +200,14 @@ export const duplicateRules = async ({ body: JSON.stringify({ ...rule, name: `${rule.name} [Duplicate]`, + created_at: undefined, created_by: undefined, id: undefined, rule_id: undefined, + updated_at: undefined, updated_by: undefined, enabled: rule.enabled, + immutable: false, }), }) ); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx index dd744f4d7ecd58..88a1333c82a45f 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEmpty, get } from 'lodash/fp'; +import { isEmpty, isEqual, get } from 'lodash/fp'; import { useEffect, useState, Dispatch, SetStateAction } from 'react'; -import { IIndexPattern } from 'src/plugins/data/public'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/public'; import { BrowserFields, getBrowserFields, @@ -40,6 +40,12 @@ export const useFetchIndexPatterns = (defaultIndices: string[] = []): Return => const [isLoading, setIsLoading] = useState(false); const [, dispatchToaster] = useStateToaster(); + useEffect(() => { + if (!isEqual(defaultIndices, indices)) { + setIndices(defaultIndices); + } + }, [defaultIndices, indices]); + useEffect(() => { let isSubscribed = true; const abortCtrl = new AbortController(); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/index.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/index.ts new file mode 100644 index 00000000000000..a61cbabd80626e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/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; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './api'; +export * from './fetch_index_patterns'; +export * from './persist_rule'; +export * from './types'; +export * from './use_rule'; +export * from './use_rules'; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts index fe6fb04800adc3..0885e541cead5d 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -10,11 +10,13 @@ export const NewRuleSchema = t.intersection([ t.type({ description: t.string, enabled: t.boolean, + filters: t.array(t.unknown), index: t.array(t.string), interval: t.string, language: t.string, name: t.string, query: t.string, + risk_score: t.number, severity: t.string, type: t.union([t.literal('query'), t.literal('saved_query')]), }), @@ -26,7 +28,9 @@ export const NewRuleSchema = t.intersection([ max_signals: t.number, references: t.array(t.string), rule_id: t.string, + saved_id: t.string, tags: t.array(t.string), + threats: t.array(t.unknown), to: t.string, updated_by: t.string, }), @@ -41,29 +45,41 @@ export interface AddRulesProps { signal: AbortSignal; } +const MetaRule = t.type({ + from: t.string, +}); + export const RuleSchema = t.intersection([ t.type({ + created_at: t.string, created_by: t.string, description: t.string, enabled: t.boolean, + false_positives: t.array(t.string), + filters: t.array(t.unknown), + from: t.string, id: t.string, index: t.array(t.string), interval: t.string, + immutable: t.boolean, language: t.string, name: t.string, + max_signals: t.number, + meta: MetaRule, query: t.string, + references: t.array(t.string), + risk_score: t.number, rule_id: t.string, severity: t.string, type: t.string, + tags: t.array(t.string), + to: t.string, + threats: t.array(t.unknown), + updated_at: t.string, updated_by: t.string, }), t.partial({ - false_positives: t.array(t.string), - from: t.string, - max_signals: t.number, - references: t.array(t.string), - tags: t.array(t.string), - to: t.string, + saved_id: t.string, }), ]); @@ -99,6 +115,12 @@ export interface FetchRulesResponse { data: Rule[]; } +export interface FetchRuleProps { + id: string; + kbnVersion: string; + signal: AbortSignal; +} + export interface EnableRulesProps { ids: string[]; enabled: boolean; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx new file mode 100644 index 00000000000000..8ba59a86a2b85a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState } from 'react'; + +import { useKibanaUiSetting } from '../../../lib/settings/use_kibana_ui_setting'; +import { DEFAULT_KBN_VERSION } from '../../../../common/constants'; +import { useStateToaster } from '../../../components/toasters'; +import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; +import { fetchRuleById } from './api'; +import * as i18n from './translations'; +import { Rule } from './types'; + +type Return = [boolean, Rule | null]; + +/** + * Hook for using to get a Rule from the Detection Engine API + * + * @param id desired Rule ID's (not rule_id) + * + */ +export const useRule = (id: string | undefined): Return => { + const [rule, setRule] = useState(null); + const [loading, setLoading] = useState(true); + const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + + async function fetchData(idToFetch: string) { + try { + setLoading(true); + const ruleResponse = await fetchRuleById({ + id: idToFetch, + kbnVersion, + signal: abortCtrl.signal, + }); + + if (isSubscribed) { + setRule(ruleResponse); + } + } catch (error) { + if (isSubscribed) { + setRule(null); + errorToToaster({ title: i18n.RULE_FETCH_FAILURE, error, dispatchToaster }); + } + } + if (isSubscribed) { + setLoading(false); + } + } + if (id != null) { + fetchData(id); + } + return () => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, [id]); + + return [loading, rule]; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts index 49dada410c5cc6..f6274db4a9dae8 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts @@ -5,15 +5,46 @@ */ import chrome from 'ui/chrome'; -import { UpdateSignalStatusProps } from './types'; + import { throwIfNotOk } from '../../../hooks/api/api'; -import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../common/constants'; +import { + DETECTION_ENGINE_QUERY_SIGNALS_URL, + DETECTION_ENGINE_SIGNALS_STATUS_URL, +} from '../../../../common/constants'; +import { QuerySignals, SignalSearchResponse, UpdateSignalStatusProps } from './types'; + +/** + * Fetch Signals by providing a query + * + * @param query String to match a dsl + * @param kbnVersion current Kibana Version to use for headers + */ +export const fetchQuerySignals = async ({ + query, + kbnVersion, + signal, +}: QuerySignals): Promise> => { + const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_QUERY_SIGNALS_URL}`, { + method: 'POST', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-version': kbnVersion, + 'kbn-xsrf': kbnVersion, + }, + body: query, + signal, + }); + await throwIfNotOk(response); + const signals = await response.json(); + return signals; +}; /** * Update signal status by query * * @param query of signals to update - * @param status to update to ('open' / 'closed') + * @param status to update to('open' / 'closed') * @param kbnVersion current Kibana Version to use for headers * @param signal to cancel request */ diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/edit_rule/translations.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/translations.ts similarity index 59% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/edit_rule/translations.ts rename to x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/translations.ts index cc2e2565eb8d0f..5b5dc9e9699fe9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/edit_rule/translations.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/translations.ts @@ -6,6 +6,9 @@ import { i18n } from '@kbn/i18n'; -export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.editRule.pageTitle', { - defaultMessage: 'Edit rule settings', -}); +export const SIGNAL_FETCH_FAILURE = i18n.translate( + 'xpack.siem.containers.detectionEngine.signals.errorFetchingSignalsDescription', + { + defaultMessage: 'Failed to query signals', + } +); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts index 4f316a7caaac5f..5846f3275c0fd9 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts @@ -4,6 +4,34 @@ * you may not use this file except in compliance with the Elastic License. */ +export interface QuerySignals { + query: string; + kbnVersion: string; + signal: AbortSignal; +} + +export interface SignalsResponse { + took: number; + timeout: boolean; +} + +export interface SignalSearchResponse extends SignalsResponse { + _shards: { + total: number; + successful: number; + skipped: number; + failed: number; + }; + aggregations?: Aggregations; + hits: { + total: { + value: number; + relation: string; + }; + hits: Hit[]; + }; +} + export interface UpdateSignalStatusProps { query: object; status: 'open' | 'closed'; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.tsx new file mode 100644 index 00000000000000..a1b3907e2b31b0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState } from 'react'; + +import { useKibanaUiSetting } from '../../../lib/settings/use_kibana_ui_setting'; +import { DEFAULT_KBN_VERSION } from '../../../../common/constants'; +import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; +import { useStateToaster } from '../../../components/toasters'; + +import { fetchQuerySignals } from './api'; +import * as i18n from './translations'; +import { SignalSearchResponse } from './types'; + +type Return = [boolean, SignalSearchResponse | null]; + +/** + * Hook for using to get a Signals from the Detection Engine API + * + * @param query convert a dsl into string + * + */ +export const useQuerySignals = (query: string): Return => { + const [signals, setSignals] = useState | null>(null); + const [loading, setLoading] = useState(true); + const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + setLoading(true); + + async function fetchData() { + try { + const signalResponse = await fetchQuerySignals({ + query, + kbnVersion, + signal: abortCtrl.signal, + }); + + if (isSubscribed) { + setSignals(signalResponse); + } + } catch (error) { + if (isSubscribed) { + setSignals(null); + errorToToaster({ title: i18n.SIGNAL_FETCH_FAILURE, error, dispatchToaster }); + } + } + if (isSubscribed) { + setLoading(false); + } + } + + fetchData(); + return () => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, [query]); + + return [loading, signals]; +}; diff --git a/x-pack/legacy/plugins/siem/public/graphql/types.ts b/x-pack/legacy/plugins/siem/public/graphql/types.ts index a1a608c6b5ffa4..6dfde08058f7cb 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/public/graphql/types.ts @@ -6,8 +6,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Direction as EuiDirection } from '@elastic/eui'; - export type Maybe = T | null; export interface PageInfoNote { @@ -54,7 +52,7 @@ export interface PaginationInput { export interface SortField { sortFieldId: string; - direction: Direction | EuiDirection; + direction: Direction; } export interface LastTimeDetails { @@ -66,25 +64,25 @@ export interface LastTimeDetails { export interface HostsSortField { field: HostsFields; - direction: Direction | EuiDirection; + direction: Direction; } export interface UsersSortField { field: UsersFields; - direction: Direction | EuiDirection; + direction: Direction; } export interface NetworkTopTablesSortField { field: NetworkTopTablesFields; - direction: Direction | EuiDirection; + direction: Direction; } export interface NetworkDnsSortField { field: NetworkDnsFields; - direction: Direction | EuiDirection; + direction: Direction; } export interface NetworkHttpSortField { @@ -94,7 +92,7 @@ export interface NetworkHttpSortField { export interface TlsSortField { field: TlsFields; - direction: Direction | EuiDirection; + direction: Direction; } export interface PageInfoTimeline { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/activity_monitor/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/activity_monitor/columns.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/activity_monitor/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/activity_monitor/index.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx similarity index 93% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/actions.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx index 0823024c078da6..c0ed58daeca7fc 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx @@ -6,9 +6,9 @@ import moment from 'moment'; -import { updateSignalStatus } from '../../../containers/detection_engine/signals/api'; +import { updateSignalStatus } from '../../../../containers/detection_engine/signals/api'; import { SendSignalsToTimelineActionProps, UpdateSignalStatusActionProps } from './types'; -import { TimelineNonEcsData } from '../../../graphql/types'; +import { TimelineNonEcsData } from '../../../../graphql/types'; export const getUpdateSignalsQuery = (eventIds: Readonly) => { return { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_config.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx similarity index 84% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_config.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx index d430262a1944a8..ea9a3ccef05b45 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_config.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx @@ -6,20 +6,20 @@ import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import React from 'react'; -import { ColumnHeader } from '../../../components/timeline/body/column_headers/column_header'; -import { defaultColumnHeaderType } from '../../../components/timeline/body/column_headers/default_headers'; + +import { esFilters } from '../../../../../../../../../src/plugins/data/common/es_query'; +import { ColumnHeader } from '../../../../components/timeline/body/column_headers/column_header'; +import { TimelineAction, TimelineActionProps } from '../../../../components/timeline/body/actions'; +import { defaultColumnHeaderType } from '../../../../components/timeline/body/column_headers/default_headers'; import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH, -} from '../../../components/timeline/body/helpers'; +} from '../../../../components/timeline/body/helpers'; +import { SubsetTimelineModel, timelineDefaults } from '../../../../store/timeline/model'; -import * as i18n from './translations'; -import { SubsetTimelineModel, timelineDefaults } from '../../../store/timeline/model'; -import { esFilters } from '../../../../../../../../src/plugins/data/common/es_query'; +import { FILTER_OPEN } from './signals_filter_group'; import { sendSignalsToTimelineAction, updateSignalStatusAction } from './actions'; -import { FILTER_OPEN } from './components/signals_filter_group/signals_filter_group'; - -import { TimelineAction, TimelineActionProps } from '../../../components/timeline/body/actions'; +import * as i18n from './translations'; import { CreateTimeline, SetEventsDeletedProps, SetEventsLoadingProps } from './types'; export const signalsOpenFilters: esFilters.Filter[] = [ @@ -62,6 +62,26 @@ export const signalsClosedFilters: esFilters.Filter[] = [ }, ]; +export const buildSignalsRuleIdFilter = (ruleId: string): esFilters.Filter[] => [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'signal.rule.id', + params: { + query: ruleId, + }, + }, + query: { + match_phrase: { + 'signal.rule.id': ruleId, + }, + }, + }, +]; + export const signalsHeaders: ColumnHeader[] = [ { columnHeaderType: defaultColumnHeaderType, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx similarity index 89% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/index.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx index a5d23227eda517..599870117890d6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx @@ -7,8 +7,8 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; -import { SignalsUtilityBar } from './components/signals_utility_bar'; -import { StatefulEventsViewer } from '../../../components/events_viewer'; +import { SignalsUtilityBar } from './signals_utility_bar'; +import { StatefulEventsViewer } from '../../../../components/events_viewer'; import * as i18n from './translations'; import { getSignalsActions, @@ -17,21 +17,21 @@ import { signalsDefaultModel, signalsOpenFilters, } from './default_config'; -import { timelineActions, timelineSelectors } from '../../../store/timeline'; -import { timelineDefaults, TimelineModel } from '../../../store/timeline/model'; +import { timelineActions, timelineSelectors } from '../../../../store/timeline'; +import { timelineDefaults, TimelineModel } from '../../../../store/timeline/model'; import { FILTER_CLOSED, FILTER_OPEN, SignalFilterOption, SignalsTableFilterGroup, -} from './components/signals_filter_group/signals_filter_group'; -import { useKibanaUiSetting } from '../../../lib/settings/use_kibana_ui_setting'; -import { DEFAULT_KBN_VERSION, DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; -import { defaultHeaders } from '../../../components/timeline/body/column_headers/default_headers'; -import { ColumnHeader } from '../../../components/timeline/body/column_headers/column_header'; -import { esFilters, esQuery } from '../../../../../../../../src/plugins/data/common/es_query'; -import { TimelineNonEcsData } from '../../../graphql/types'; -import { inputsSelectors, SerializedFilterQuery, State } from '../../../store'; +} from './signals_filter_group'; +import { useKibanaUiSetting } from '../../../../lib/settings/use_kibana_ui_setting'; +import { DEFAULT_KBN_VERSION, DEFAULT_SIGNALS_INDEX } from '../../../../../common/constants'; +import { defaultHeaders } from '../../../../components/timeline/body/column_headers/default_headers'; +import { ColumnHeader } from '../../../../components/timeline/body/column_headers/column_header'; +import { esFilters, esQuery } from '../../../../../../../../../src/plugins/data/common/es_query'; +import { TimelineNonEcsData } from '../../../../graphql/types'; +import { inputsSelectors, SerializedFilterQuery, State } from '../../../../store'; import { sendSignalsToTimelineAction, updateSignalStatusAction } from './actions'; import { CreateTimelineProps, @@ -41,12 +41,12 @@ import { UpdateSignalsStatus, UpdateSignalsStatusProps, } from './types'; -import { inputsActions } from '../../../store/inputs'; -import { combineQueries } from '../../../components/timeline/helpers'; -import { useKibanaCore } from '../../../lib/compose/kibana_core'; -import { useFetchIndexPatterns } from '../../../containers/detection_engine/rules/fetch_index_patterns'; -import { InputsRange } from '../../../store/inputs/model'; -import { Query } from '../../../../../../../../src/plugins/data/common/query'; +import { inputsActions } from '../../../../store/inputs'; +import { combineQueries } from '../../../../components/timeline/helpers'; +import { useKibanaCore } from '../../../../lib/compose/kibana_core'; +import { useFetchIndexPatterns } from '../../../../containers/detection_engine/rules/fetch_index_patterns'; +import { InputsRange } from '../../../../store/inputs/model'; +import { Query } from '../../../../../../../../../src/plugins/data/common/query'; const SIGNALS_PAGE_TIMELINE_ID = 'signals-page'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/components/signals_filter_group/signals_filter_group.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/components/signals_filter_group/signals_filter_group.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.tsx index 445a7a9aea2679..2a47cb5f1b0554 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/components/signals_filter_group/signals_filter_group.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.tsx @@ -5,7 +5,7 @@ */ import { EuiFilterButton, EuiFilterGroup } from '@elastic/eui'; import React, { useCallback, useState } from 'react'; -import * as i18n from '../../translations'; +import * as i18n from '../translations'; export const FILTER_OPEN = 'open'; export const FILTER_CLOSED = 'closed'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/components/signals_utility_bar/batch_actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/batch_actions.tsx similarity index 96% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/components/signals_utility_bar/batch_actions.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/batch_actions.tsx index a45a968a029c5e..bbbc7728e36a54 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/components/signals_utility_bar/batch_actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/batch_actions.tsx @@ -8,8 +8,8 @@ import { EuiContextMenuItem } from '@elastic/eui'; import React from 'react'; import * as i18n from './translations'; import { TimelineNonEcsData } from '../../../../../graphql/types'; -import { SendSignalsToTimeline, UpdateSignalsStatus } from '../../types'; -import { FILTER_CLOSED, FILTER_OPEN } from '../signals_filter_group/signals_filter_group'; +import { SendSignalsToTimeline, UpdateSignalsStatus } from '../types'; +import { FILTER_CLOSED, FILTER_OPEN } from '../signals_filter_group'; /** * Returns ViewInTimeline / UpdateSignalStatus actions to be display within an EuiContextMenuPanel diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/components/signals_utility_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx similarity index 98% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/components/signals_utility_bar/index.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx index 4e392da6443bac..8d754eb1d3e1b0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/components/signals_utility_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx @@ -19,7 +19,7 @@ import { getBatchItems } from './batch_actions'; import { useKibanaUiSetting } from '../../../../../lib/settings/use_kibana_ui_setting'; import { DEFAULT_NUMBER_FORMAT } from '../../../../../../common/constants'; import { TimelineNonEcsData } from '../../../../../graphql/types'; -import { SendSignalsToTimeline, UpdateSignalsStatus } from '../../types'; +import { SendSignalsToTimeline, UpdateSignalsStatus } from '../types'; interface SignalsUtilityBarProps { areEventsLoading: boolean; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/components/signals_utility_bar/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/components/signals_utility_bar/translations.ts rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/translations.ts rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/types.ts similarity index 89% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/types.ts rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/types.ts index 977fdf8a01f1c0..b02b8eb0ef976e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/types.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { esFilters } from '../../../../../../../../src/plugins/data/common/es_query'; -import { TimelineNonEcsData } from '../../../graphql/types'; -import { KueryFilterQuery, SerializedFilterQuery } from '../../../store'; +import { esFilters } from '../../../../../../../../../src/plugins/data/common/es_query'; +import { TimelineNonEcsData } from '../../../../graphql/types'; +import { KueryFilterQuery, SerializedFilterQuery } from '../../../../store'; export interface SetEventsLoadingProps { eventIds: string[]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_chart/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_chart/index.tsx new file mode 100644 index 00000000000000..094f17922af1ac --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_chart/index.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiPanel, EuiSelect } from '@elastic/eui'; +import { noop } from 'lodash/fp'; +import React, { memo } from 'react'; + +import { HeaderSection } from '../../../../components/header_section'; +import { HistogramSignals } from '../../../../components/page/detection_engine/histogram_signals'; + +export const sampleChartOptions = [ + { text: 'Risk scores', value: 'risk_scores' }, + { text: 'Severities', value: 'severities' }, + { text: 'Top destination IPs', value: 'destination_ips' }, + { text: 'Top event actions', value: 'event_actions' }, + { text: 'Top event categories', value: 'event_categories' }, + { text: 'Top host names', value: 'host_names' }, + { text: 'Top rule types', value: 'rule_types' }, + { text: 'Top rules', value: 'rules' }, + { text: 'Top source IPs', value: 'source_ips' }, + { text: 'Top users', value: 'users' }, +]; + +export const SignalsCharts = memo(() => ( + + + noop} + prepend="Stack by" + value={sampleChartOptions[0].value} + /> + + + + +)); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/index.tsx new file mode 100644 index 00000000000000..7f285a4a708c9a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/index.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiLoadingSpinner } from '@elastic/eui'; +import { FormattedRelative } from '@kbn/i18n/react'; +import React, { useState, useEffect } from 'react'; + +import { useQuerySignals } from '../../../../containers/detection_engine/signals/use_query'; +import { buildlastSignalsQuery } from './query.dsl'; +import { Aggs } from './types'; + +interface SignalInfo { + ruleId?: string | null; +} + +type Return = [React.ReactNode, React.ReactNode]; + +export const useSignalInfo = ({ ruleId = null }: SignalInfo): Return => { + const [lastSignals, setLastSignals] = useState( + + ); + const [totalSignals, setTotalSignals] = useState( + + ); + + let query = ''; + try { + query = JSON.stringify(buildlastSignalsQuery(ruleId)); + } catch { + query = ''; + } + + const [, signals] = useQuerySignals(query); + + useEffect(() => { + if (signals != null) { + const mySignals = signals; + setLastSignals( + mySignals.aggregations?.lastSeen.value != null ? ( + + ) : null + ); + setTotalSignals(<>{mySignals.hits.total.value}); + } + }, [signals]); + + return [lastSignals, totalSignals]; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/query.dsl.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/query.dsl.ts new file mode 100644 index 00000000000000..0b14aa17a94500 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/query.dsl.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; + * you may not use this file except in compliance with the Elastic License. + */ + +export const buildlastSignalsQuery = (ruleId: string | undefined | null) => { + const queryFilter = [ + { + bool: { should: [{ match: { 'signal.status': 'open' } }], minimum_should_match: 1 }, + }, + ]; + return { + aggs: { + lastSeen: { max: { field: '@timestamp' } }, + }, + query: { + bool: { + filter: + ruleId != null + ? [ + ...queryFilter, + { + bool: { + should: [{ match: { 'signal.rule.id': ruleId } }], + minimum_should_match: 1, + }, + }, + ] + : queryFilter, + }, + }, + size: 0, + track_total_hits: true, + }; +}; diff --git a/x-pack/legacy/plugins/infra/public/apps/kibana_app.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/types.ts similarity index 67% rename from x-pack/legacy/plugins/infra/public/apps/kibana_app.ts rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/types.ts index 5c5687c6c12837..68bc8dc3eed593 100644 --- a/x-pack/legacy/plugins/infra/public/apps/kibana_app.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/types.ts @@ -4,6 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { compose } from '../lib/compose/kibana_compose'; -import { startApp } from './start_app'; -startApp(compose()); +export interface Aggs { + lastSeen: { + value: number; + value_as_string: string; + }; +} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/index.tsx deleted file mode 100644 index aeb70061c44bf7..00000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/index.tsx +++ /dev/null @@ -1,161 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiButton, EuiHorizontalRule, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React, { memo, useCallback, useState } from 'react'; - -import { RuleStepProps, RuleStep, AboutStepRule } from '../../types'; -import * as CreateRuleI18n from '../../translations'; -import { Field, Form, FormDataProvider, getUseField, UseField, useForm } from '../shared_imports'; -import { AddItem } from '../add_item_form'; -import { defaultRiskScoreBySeverity, severityOptions, SeverityValue } from './data'; -import { defaultValue } from './default_value'; -import { schema } from './schema'; -import * as I18n from './translations'; -import { StepRuleDescription } from '../description_step'; -import { AddMitreThreat } from '../mitre'; - -const CommonUseField = getUseField({ component: Field }); - -export const StepAboutRule = memo(({ isEditView, isLoading, setStepData }) => { - const [myStepData, setMyStepData] = useState(defaultValue); - const { form } = useForm({ - defaultValue: myStepData, - options: { stripEmptyFields: false }, - schema, - }); - - const onSubmit = useCallback(async () => { - const { isValid, data } = await form.submit(); - if (isValid) { - setStepData(RuleStep.aboutRule, data, isValid); - setMyStepData({ ...data, isNew: false } as AboutStepRule); - } - }, [form]); - - return isEditView && myStepData != null ? ( - - ) : ( - <> -
- - - - - - - - - - {({ severity }) => { - const newRiskScore = defaultRiskScoreBySeverity[severity as SeverityValue]; - const riskScoreField = form.getFields().riskScore; - if (newRiskScore != null && riskScoreField.value !== newRiskScore) { - riskScoreField.setValue(newRiskScore); - } - return null; - }} - - - - - - - {myStepData.isNew ? CreateRuleI18n.CONTINUE : CreateRuleI18n.UPDATE} - - - - - ); -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/index.tsx deleted file mode 100644 index bd4d5aa4f8ca15..00000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/index.tsx +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; -import React, { memo, useCallback, useState } from 'react'; - -import { RuleStep, RuleStepProps, ScheduleStepRule } from '../../types'; -import { StepRuleDescription } from '../description_step'; -import { ScheduleItem } from '../schedule_item_form'; -import { Form, UseField, useForm } from '../shared_imports'; -import { schema } from './schema'; -import * as I18n from './translations'; - -export const StepScheduleRule = memo(({ isEditView, isLoading, setStepData }) => { - const [myStepData, setMyStepData] = useState({ - enabled: true, - interval: '5m', - isNew: true, - from: '0m', - }); - const { form } = useForm({ - schema, - defaultValue: myStepData, - options: { stripEmptyFields: false }, - }); - - const onSubmit = useCallback( - async (enabled: boolean) => { - const { isValid: newIsValid, data } = await form.submit(); - if (newIsValid) { - setStepData(RuleStep.scheduleRule, { ...data, enabled }, newIsValid); - setMyStepData({ ...data, isNew: false } as ScheduleStepRule); - } - }, - [form] - ); - - return isEditView && myStepData != null ? ( - - ) : ( - <> -
- - - - - - - - {I18n.COMPLETE_WITHOUT_ACTIVATING} - - - - - {I18n.COMPLETE_WITH_ACTIVATING} - - - - - ); -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/translations.ts deleted file mode 100644 index 5ff9dffb658ab4..00000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/translations.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.createRule.pageTitle', { - defaultMessage: 'Create new rule', -}); - -export const DEFINE_RULE = i18n.translate('xpack.siem.detectionEngine.createRule.defineRuleTitle', { - defaultMessage: 'Define Rule', -}); - -export const ABOUT_RULE = i18n.translate('xpack.siem.detectionEngine.createRule.aboutRuleTitle', { - defaultMessage: 'About Rule', -}); - -export const SCHEDULE_RULE = i18n.translate( - 'xpack.siem.detectionEngine.createRule.scheduleRuleTitle', - { - defaultMessage: 'Schedule Rule', - } -); - -export const OPTIONAL_FIELD = i18n.translate( - 'xpack.siem.detectionEngine.createRule.optionalFieldDescription', - { - defaultMessage: 'Optional', - } -); - -export const CONTINUE = i18n.translate( - 'xpack.siem.detectionEngine.createRule.continueButtonTitle', - { - defaultMessage: 'Continue', - } -); - -export const UPDATE = i18n.translate('xpack.siem.detectionEngine.createRule.updateButtonTitle', { - defaultMessage: 'Update', -}); - -export const DELETE = i18n.translate('xpack.siem.detectionEngine.createRule.deleteDescription', { - defaultMessage: 'Delete', -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/types.ts deleted file mode 100644 index 241e4530b4b577..00000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/types.ts +++ /dev/null @@ -1,88 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { FieldValueQueryBar } from './components/query_bar'; -import { esFilters } from '../../../../../../../../src/plugins/data/common'; - -export enum RuleStep { - defineRule = 'define-rule', - aboutRule = 'about-rule', - scheduleRule = 'schedule-rule', -} -export type RuleStatusType = 'passive' | 'active' | 'valid'; - -export interface RuleStepData { - data: unknown; - isValid: boolean; -} - -export interface RuleStepProps { - setStepData: (step: RuleStep, data: unknown, isValid: boolean) => void; - isEditView: boolean; - isLoading: boolean; - resizeParentContainer?: (height: number) => void; -} - -interface StepRuleData { - isNew: boolean; -} -export interface AboutStepRule extends StepRuleData { - name: string; - description: string; - severity: string; - riskScore: number; - references: string[]; - falsePositives: string[]; - tags: string[]; - threats: IMitreEnterpriseAttack[]; -} - -export interface DefineStepRule extends StepRuleData { - useIndicesConfig: string; - index: string[]; - queryBar: FieldValueQueryBar; -} - -export interface ScheduleStepRule extends StepRuleData { - enabled: boolean; - interval: string; - from: string; - to?: string; -} - -export interface DefineStepRuleJson { - index: string[]; - filters: esFilters.Filter[]; - saved_id?: string; - query: string; - language: string; -} - -export interface AboutStepRuleJson { - name: string; - description: string; - severity: string; - risk_score: number; - references: string[]; - false_positives: string[]; - tags: string[]; - threats: IMitreEnterpriseAttack[]; -} - -export type ScheduleStepRuleJson = ScheduleStepRule; - -export type FormatRuleType = 'query' | 'saved_query'; - -export interface IMitreAttack { - id: string; - name: string; - reference: string; -} -export interface IMitreEnterpriseAttack { - framework: string; - tactic: IMitreAttack; - techniques: IMitreAttack[]; -} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx index d17685aa326cfa..6e6b71729b07e9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx @@ -4,37 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButton, EuiPanel, EuiSelect, EuiSpacer } from '@elastic/eui'; +import { EuiButton, EuiSpacer } from '@elastic/eui'; import React from 'react'; import { StickyContainer } from 'react-sticky'; import { FiltersGlobal } from '../../components/filters_global'; import { HeaderPage } from '../../components/header_page'; -import { HeaderSection } from '../../components/header_section'; -import { HistogramSignals } from '../../components/page/detection_engine/histogram_signals'; import { SiemSearchBar } from '../../components/search_bar'; import { WrapperPage } from '../../components/wrapper_page'; +import { GlobalTime } from '../../containers/global_time'; import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; import { SpyRoute } from '../../utils/route/spy_routes'; + +import { SignalsTable } from './components/signals'; +import { SignalsCharts } from './components/signals_chart'; +import { useSignalInfo } from './components/signals_info'; import { DetectionEngineEmptyPage } from './detection_engine_empty_page'; import * as i18n from './translations'; -import { SignalsTable } from './signals'; -import { GlobalTime } from '../../containers/global_time'; export const DetectionEngineComponent = React.memo(() => { - const sampleChartOptions = [ - { text: 'Risk scores', value: 'risk_scores' }, - { text: 'Severities', value: 'severities' }, - { text: 'Top destination IPs', value: 'destination_ips' }, - { text: 'Top event actions', value: 'event_actions' }, - { text: 'Top event categories', value: 'event_categories' }, - { text: 'Top host names', value: 'host_names' }, - { text: 'Top rule types', value: 'rule_types' }, - { text: 'Top rules', value: 'rules' }, - { text: 'Top source IPs', value: 'source_ips' }, - { text: 'Top users', value: 'users' }, - ]; - + const [lastSignals] = useSignalInfo({}); return ( <> @@ -46,24 +35,25 @@ export const DetectionEngineComponent = React.memo(() => { - + + {i18n.LAST_SIGNAL} + {': '} + {lastSignals} + + ) + } + title={i18n.PAGE_TITLE} + > {i18n.BUTTON_MANAGE_RULES} - - - {}} - prepend="Stack by" - value={sampleChartOptions[0].value} - /> - - - - + {({ to, from }) => } @@ -72,7 +62,6 @@ export const DetectionEngineComponent = React.memo(() => { ) : ( - ); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/edit_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/edit_rule/index.tsx deleted file mode 100644 index 9b8607fdc7685f..00000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/edit_rule/index.tsx +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiSpacer, - EuiTabbedContent, -} from '@elastic/eui'; -import React from 'react'; - -import { HeaderPage } from '../../../components/header_page'; -import { HeaderSection } from '../../../components/header_section'; -import { WrapperPage } from '../../../components/wrapper_page'; -import { SpyRoute } from '../../../utils/route/spy_routes'; -import * as i18n from './translations'; - -const Define = React.memo(() => ( - <> - - - - - - -)); -Define.displayName = 'Define'; - -const About = React.memo(() => ( - <> - - - - - - -)); -About.displayName = 'About'; - -const Schedule = React.memo(() => ( - <> - - - - - - -)); -Schedule.displayName = 'Schedule'; - -export const EditRuleComponent = React.memo(() => { - return ( - <> - - - - - - {'Cancel'} - - - - - - {'Save changes'} - - - - - - , - }, - { - id: 'tabAbout', - name: 'About', - content: , - }, - { - id: 'tabSchedule', - name: 'Schedule', - content: , - }, - ]} - /> - - - - - - - {'Cancel'} - - - - - - {'Save changes'} - - - - - - - - ); -}); -EditRuleComponent.displayName = 'EditRuleComponent'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/helpers.ts new file mode 100644 index 00000000000000..1399df0fcf6d12 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/helpers.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; + * you may not use this file except in compliance with the Elastic License. + */ + +export const sampleChartOptions = [ + { text: 'Risk scores', value: 'risk_scores' }, + { text: 'Severities', value: 'severities' }, + { text: 'Top destination IPs', value: 'destination_ips' }, + { text: 'Top event actions', value: 'event_actions' }, + { text: 'Top event categories', value: 'event_categories' }, + { text: 'Top host names', value: 'host_names' }, + { text: 'Top rule types', value: 'rule_types' }, + { text: 'Top rules', value: 'rules' }, + { text: 'Top source IPs', value: 'source_ips' }, + { text: 'Top users', value: 'users' }, +]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx index 90524b4da0af44..21ebac2b4d3372 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx @@ -7,10 +7,10 @@ import React from 'react'; import { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom'; -import { CreateRuleComponent } from './create_rule'; +import { CreateRuleComponent } from './rules/create'; import { DetectionEngineComponent } from './detection_engine'; -import { EditRuleComponent } from './edit_rule'; -import { RuleDetailsComponent } from './rule_details'; +import { EditRuleComponent } from './rules/edit'; +import { RuleDetailsComponent } from './rules/details'; import { RulesComponent } from './rules'; const detectionEnginePath = `/:pageName(detection-engine)`; @@ -19,21 +19,21 @@ type Props = Partial> & { url: string }; export const DetectionEngineContainer = React.memo(() => ( - } strict /> - } /> - } - /> - } - /> - } - /> + + + + + + + + + + + + + + + ( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rule_details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rule_details/index.tsx deleted file mode 100644 index b1b5af701123ce..00000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rule_details/index.tsx +++ /dev/null @@ -1,663 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - EuiBasicTable as _EuiBasicTable, - EuiButton, - EuiButtonIcon, - EuiCallOut, - EuiFilterButton, - EuiFilterGroup, - EuiFlexGroup, - EuiFlexItem, - EuiIconTip, - EuiPanel, - EuiPopover, - EuiSelect, - EuiSpacer, - EuiSwitch, - EuiTabbedContent, - EuiTextColor, -} from '@elastic/eui'; -import moment from 'moment'; -import React, { useState } from 'react'; -import { StickyContainer } from 'react-sticky'; - -import { getEmptyTagValue } from '../../../components/empty_value'; -import { FiltersGlobal } from '../../../components/filters_global'; -import { HeaderPage } from '../../../components/header_page'; -import { HeaderSection } from '../../../components/header_section'; -import { HistogramSignals } from '../../../components/page/detection_engine/histogram_signals'; -import { ProgressInline } from '../../../components/progress_inline'; -import { SiemSearchBar } from '../../../components/search_bar'; -import { - UtilityBar, - UtilityBarAction, - UtilityBarGroup, - UtilityBarSection, - UtilityBarText, -} from '../../../components/detection_engine/utility_bar'; -import { WrapperPage } from '../../../components/wrapper_page'; -import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../../containers/source'; -import { SpyRoute } from '../../../utils/route/spy_routes'; -import { DetectionEngineEmptyPage } from '../detection_engine_empty_page'; -import * as i18n from './translations'; - -// there are a number of type mismatches across this file -const EuiBasicTable: any = _EuiBasicTable; // eslint-disable-line @typescript-eslint/no-explicit-any - -// Michael: Will need to change this to get the current datetime format from Kibana settings. -const dateTimeFormat = (value: string) => { - return moment(value).format('M/D/YYYY, h:mm A'); -}; - -const OpenSignals = React.memo(() => { - return ( - <> - - - - {'Showing: 439 signals'} - - - - {'Selected: 20 signals'} - -

{'Batch actions context menu here.'}

} - > - {'Batch actions'} -
- - - {'Select all signals on all pages'} - -
- - - {'Clear 7 filters'} - - {'Clear aggregation'} - -
- - - -

{'Customize columns context menu here.'}

} - > - {'Customize columns'} -
- - {'Aggregate data'} -
-
-
- - {/* Michael: Open signals datagrid here. Talk to Chandler Prall about possibility of early access. If not possible, use basic table. */} - - ); -}); - -const ClosedSignals = React.memo(() => { - return ( - <> - - - - {'Showing: 439 signals'} - - - - - -

{'Customize columns context menu here.'}

} - > - {'Customize columns'} -
- - {'Aggregate data'} -
-
-
- - {/* Michael: Closed signals datagrid here. Talk to Chandler Prall about possibility of early access. If not possible, use basic table. */} - - ); -}); - -const Signals = React.memo(() => { - const sampleChartOptions = [ - { text: 'Risk scores', value: 'risk_scores' }, - { text: 'Severities', value: 'severities' }, - { text: 'Top destination IPs', value: 'destination_ips' }, - { text: 'Top event actions', value: 'event_actions' }, - { text: 'Top event categories', value: 'event_categories' }, - { text: 'Top host names', value: 'host_names' }, - { text: 'Top source IPs', value: 'source_ips' }, - { text: 'Top users', value: 'users' }, - ]; - - const filterGroupOptions = ['open', 'closed']; - const [filterGroupState, setFilterGroupState] = useState(filterGroupOptions[0]); - - return ( - <> - - - - - {}} - prepend="Stack by" - value={sampleChartOptions[0].value} - /> - - - - - - - - - - - setFilterGroupState(filterGroupOptions[0])} - withNext - > - {'Open signals'} - - - setFilterGroupState(filterGroupOptions[1])} - > - {'Closed signals'} - - - - - {filterGroupState === filterGroupOptions[0] ? : } - - - ); -}); -Signals.displayName = 'Signals'; - -const ActivityMonitor = React.memo(() => { - interface ColumnTypes { - id: number; - ran: string; - lookedBackTo: string; - status: string; - response: string | undefined; - } - - interface PageTypes { - index: number; - size: number; - } - - interface SortTypes { - field: string; - direction: string; - } - - const actions = [ - { - available: (item: ColumnTypes) => item.status === 'Running', - description: 'Stop', - icon: 'stop', - isPrimary: true, - name: 'Stop', - onClick: () => {}, - type: 'icon', - }, - { - available: (item: ColumnTypes) => item.status === 'Stopped', - description: 'Resume', - icon: 'play', - isPrimary: true, - name: 'Resume', - onClick: () => {}, - type: 'icon', - }, - ]; - - // Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes? - const columns = [ - { - field: 'ran', - name: 'Ran', - render: (value: ColumnTypes['ran']) => , - sortable: true, - truncateText: true, - }, - { - field: 'lookedBackTo', - name: 'Looked back to', - render: (value: ColumnTypes['lookedBackTo']) => ( - - ), - sortable: true, - truncateText: true, - }, - { - field: 'status', - name: 'Status', - sortable: true, - truncateText: true, - }, - { - field: 'response', - name: 'Response', - render: (value: ColumnTypes['response']) => { - return value === undefined ? ( - getEmptyTagValue() - ) : ( - <> - {value === 'Fail' ? ( - - {value} - - ) : ( - {value} - )} - - ); - }, - sortable: true, - truncateText: true, - }, - { - actions, - width: '40px', - }, - ]; - - const sampleTableData = [ - { - id: 1, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Running', - }, - { - id: 2, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Stopped', - }, - { - id: 3, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Fail', - }, - { - id: 4, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 5, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 6, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 7, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 8, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 9, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 10, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 11, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 12, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 13, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 14, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 15, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 16, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 17, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 18, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 19, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 20, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 21, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - ]; - - const [itemsTotalState] = useState(sampleTableData.length); - const [pageState, setPageState] = useState({ index: 0, size: 20 }); - // const [selectedState, setSelectedState] = useState([]); - const [sortState, setSortState] = useState({ field: 'ran', direction: 'desc' }); - - return ( - <> - - - - - - - - - {'Showing: 39 activites'} - - - - {'Selected: 2 activities'} - - {'Stop selected'} - - - - {'Clear 7 filters'} - - - - - { - setPageState(page); - setSortState(sort); - }} - pagination={{ - pageIndex: pageState.index, - pageSize: pageState.size, - totalItemCount: itemsTotalState, - pageSizeOptions: [5, 10, 20], - }} - selection={{ - selectable: (item: ColumnTypes) => item.status !== 'Completed', - selectableMessage: (selectable: boolean) => - selectable ? undefined : 'Completed runs cannot be acted upon', - onSelectionChange: (selectedItems: ColumnTypes[]) => { - // setSelectedState(selectedItems); - }, - }} - sorting={{ - sort: sortState, - }} - /> - - - ); -}); -ActivityMonitor.displayName = 'ActivityMonitor'; - -export const RuleDetailsComponent = React.memo(() => { - const [popoverState, setPopoverState] = useState(false); - - return ( - <> - - {({ indicesExist, indexPattern }) => { - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - - - - - {'Status: Running'} - , - ]} - title="Automated exfiltration" - > - - - {}} /> - - - - - - - {'Edit rule settings'} - - - - - setPopoverState(!popoverState)} - /> - } - closePopover={() => setPopoverState(false)} - isOpen={popoverState} - > -

{'Overflow context menu here.'}

-
-
-
-
-
-
- - -

{'Full fail message here.'}

-
- - - - - - - - - - - - - - - {/*

{'Description'}

*/} - - {/* - -

{'Description'}

-
- - -

{'Severity'}

-
- - -

{'Risk score boost'}

-
- - -

{'References'}

-
- - -

{'False positives'}

-
- - -

{'Mitre ATT&CK types'}

-
- - -

{'Tags'}

-
-
*/} -
-
- - - - - - -
- - - - , - }, - { - id: 'tabActivityMonitor', - name: 'Activity monitor', - content: , - }, - ]} - /> -
-
- ) : ( - - - - - - ); - }} -
- - - - ); -}); -RuleDetailsComponent.displayName = 'RuleDetailsComponent'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx similarity index 80% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/actions.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx index a54296f65a3822..4de6136e9d3ded 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx @@ -4,16 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ +import * as H from 'history'; import React from 'react'; + +import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine'; import { deleteRules, duplicateRules, enableRules, -} from '../../../../containers/detection_engine/rules/api'; + Rule, +} from '../../../../containers/detection_engine/rules'; import { Action } from './reducer'; -import { Rule } from '../../../../containers/detection_engine/rules/types'; -export const editRuleAction = () => {}; +export const editRuleAction = (rule: Rule, history: H.History) => { + history.push(`/${DETECTION_ENGINE_PAGE_NAME}/rules/${rule.id}/edit`); +}; export const runRuleAction = () => {}; @@ -25,7 +30,7 @@ export const duplicateRuleAction = async ( dispatch({ type: 'updateLoading', ids: [rule.id], isLoading: true }); const duplicatedRule = await duplicateRules({ rules: [rule], kbnVersion }); dispatch({ type: 'updateLoading', ids: [rule.id], isLoading: false }); - dispatch({ type: 'updateRules', rules: duplicatedRule }); + dispatch({ type: 'updateRules', rules: duplicatedRule, appendRuleId: rule.id }); }; export const exportRulesAction = async (rules: Rule[], dispatch: React.Dispatch) => { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/batch_actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/batch_actions.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx similarity index 82% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/columns.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx index f716b0f83c8047..ad5d210efa42d5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx @@ -9,9 +9,11 @@ import { EuiHealth, EuiIconTip, EuiLink, - EuiTableActionsColumnType, EuiTextColor, + EuiBasicTableColumn, + EuiTableActionsColumnType, } from '@elastic/eui'; +import * as H from 'history'; import React from 'react'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { getEmptyTagValue } from '../../../../components/empty_value'; @@ -19,7 +21,6 @@ import { deleteRulesAction, duplicateRuleAction, editRuleAction, - enableRulesAction, exportRulesAction, runRuleAction, } from './actions'; @@ -28,20 +29,18 @@ import { Action } from './reducer'; import { TableData } from '../types'; import * as i18n from '../translations'; import { PreferenceFormattedDate } from '../../../../components/formatted_date'; -import { RuleSwitch, RuleStateChangeCallback } from '../components/rule_switch'; +import { RuleSwitch } from '../components/rule_switch'; -const getActions = (dispatch: React.Dispatch, kbnVersion: string) => [ +const getActions = (dispatch: React.Dispatch, kbnVersion: string, history: H.History) => [ { description: i18n.EDIT_RULE_SETTINGS, - type: 'icon', icon: 'visControls', name: i18n.EDIT_RULE_SETTINGS, - onClick: editRuleAction, - enabled: () => false, + onClick: (rowItem: TableData) => editRuleAction(rowItem.sourceRule, history), + enabled: (rowItem: TableData) => !rowItem.sourceRule.immutable, }, { description: i18n.RUN_RULE_MANUALLY, - type: 'icon', icon: 'play', name: i18n.RUN_RULE_MANUALLY, onClick: runRuleAction, @@ -49,21 +48,18 @@ const getActions = (dispatch: React.Dispatch, kbnVersion: string) => [ }, { description: i18n.DUPLICATE_RULE, - type: 'icon', icon: 'copy', name: i18n.DUPLICATE_RULE, onClick: (rowItem: TableData) => duplicateRuleAction(rowItem.sourceRule, dispatch, kbnVersion), }, { description: i18n.EXPORT_RULE, - type: 'icon', icon: 'exportAction', name: i18n.EXPORT_RULE, onClick: (rowItem: TableData) => exportRulesAction([rowItem.sourceRule], dispatch), }, { description: i18n.DELETE_RULE, - type: 'icon', icon: 'trash', name: i18n.DELETE_RULE, onClick: (rowItem: TableData) => deleteRulesAction([rowItem.id], dispatch, kbnVersion), @@ -71,7 +67,11 @@ const getActions = (dispatch: React.Dispatch, kbnVersion: string) => [ ]; // Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes? -export const getColumns = (dispatch: React.Dispatch, kbnVersion: string) => [ +export const getColumns = ( + dispatch: React.Dispatch, + kbnVersion: string, + history: H.History +): Array | EuiTableActionsColumnType> => [ { field: 'rule', name: i18n.COLUMN_RULE, @@ -156,28 +156,22 @@ export const getColumns = (dispatch: React.Dispatch, kbnVersion: string) width: '20%', }, { - align: 'center' as const, + align: 'center', field: 'activate', name: i18n.COLUMN_ACTIVATE, - render: (value: TableData['activate'], item: TableData) => { - const handleRuleStateChange: RuleStateChangeCallback = async (enabled, id) => { - await enableRulesAction([id], enabled, dispatch, kbnVersion); - }; - - return ( - - ); - }, + render: (value: TableData['activate'], item: TableData) => ( + + ), sortable: true, width: '85px', }, { - actions: getActions(dispatch, kbnVersion), + actions: getActions(dispatch, kbnVersion, history), width: '40px', } as EuiTableActionsColumnType, ]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts similarity index 91% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/helpers.ts rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts index db02d41771f68a..1909b75a85835e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Rule } from '../../../../containers/detection_engine/rules/types'; +import { Rule } from '../../../../containers/detection_engine/rules'; import { TableData } from '../types'; import { getEmptyValue } from '../../../../components/empty_value'; @@ -13,7 +13,7 @@ export const formatRules = (rules: Rule[], selectedIds?: string[]): TableData[] id: rule.id, rule_id: rule.rule_id, rule: { - href: `#/detection-engine/rules/rule-details/${encodeURIComponent(rule.id)}`, + href: `#/detection-engine/rules/${encodeURIComponent(rule.id)}`, name: rule.name, status: 'Status Placeholder', }, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/index.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx index b2adba28af20d9..4497474b387b1e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all_rules/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx @@ -12,6 +12,7 @@ import { EuiSpacer, } from '@elastic/eui'; import React, { useCallback, useEffect, useReducer, useState } from 'react'; +import { useHistory } from 'react-router-dom'; import uuid from 'uuid'; import { HeaderSection } from '../../../../components/header_section'; @@ -23,7 +24,7 @@ import { UtilityBarText, } from '../../../../components/detection_engine/utility_bar'; import { getColumns } from './columns'; -import { useRules } from '../../../../containers/detection_engine/rules/use_rules'; +import { useRules } from '../../../../containers/detection_engine/rules'; import { Loader } from '../../../../components/loader'; import { Panel } from '../../../../components/panel'; import { getBatchItems } from './batch_actions'; @@ -74,7 +75,7 @@ export const AllRules = React.memo<{ importCompleteToggle: boolean }>(importComp }, dispatch, ] = useReducer(allRulesReducer, initialState); - + const history = useHistory(); const [isInitialLoad, setIsInitialLoad] = useState(true); const [isLoadingRules, rulesData] = useRules(pagination, filterOptions, refreshToggle); const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION); @@ -184,7 +185,7 @@ export const AllRules = React.memo<{ importCompleteToggle: boolean }>(importComp { } const ruleIds = state.rules.map(r => r.rule_id); + const appendIdx = + action.appendRuleId != null ? state.rules.findIndex(r => r.id === action.appendRuleId) : -1; const updatedRules = action.rules.reduce( (rules, updatedRule) => ruleIds.includes(updatedRule.rule_id) ? rules.map(r => (updatedRule.rule_id === r.rule_id ? updatedRule : r)) + : appendIdx !== -1 + ? [ + ...rules.slice(0, appendIdx + 1), + updatedRule, + ...rules.slice(appendIdx + 1, rules.length - 1), + ] : [...rules, updatedRule], [...state.rules] ); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/accordion_title/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/accordion_title/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/accordion_title/index.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/accordion_title/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/index.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx index e972cd21b6be94..f090f6d97eaf9d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx @@ -8,8 +8,8 @@ import { EuiButtonEmpty, EuiButtonIcon, EuiFormRow, EuiFieldText, EuiSpacer } fr import { isEmpty } from 'lodash/fp'; import React, { ChangeEvent, useCallback, useEffect, useState, useRef } from 'react'; +import * as RuleI18n from '../../translations'; import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; -import * as CreateRuleI18n from '../../translations'; interface AddItemProps { addText: string; @@ -134,7 +134,7 @@ export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: Ad iconType="trash" isDisabled={isDisabled} onClick={() => removeItem(index)} - aria-label={CreateRuleI18n.DELETE} + aria-label={RuleI18n.DELETE} /> } onChange={e => updateItem(e, index)} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/filter_label.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/filter_label.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/filter_label.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/filter_label.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/filter_operator.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/filter_operator.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/filter_operator.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/filter_operator.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx similarity index 73% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/index.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx index e6fec597ed8eae..f2fbd373cf4bc7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx @@ -7,6 +7,7 @@ import { EuiBadge, EuiDescriptionList, + EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem, EuiTextArea, @@ -15,15 +16,16 @@ import { EuiListGroup, } from '@elastic/eui'; import { isEmpty, chunk, get, pick } from 'lodash/fp'; -import React, { memo, ReactNode } from 'react'; +import React, { memo, ReactNode, useState } from 'react'; import styled from 'styled-components'; import { IIndexPattern, esFilters, + FilterManager, Query, } from '../../../../../../../../../../src/plugins/data/public'; - +import { useKibanaCore } from '../../../../../lib/compose/kibana_core'; import { FilterLabel } from './filter_label'; import { FormSchema } from '../shared_imports'; import * as I18n from './translations'; @@ -32,6 +34,7 @@ import { IMitreEnterpriseAttack } from '../../types'; import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; interface StepRuleDescriptionProps { + direction?: 'row' | 'column'; data: unknown; indexPatterns?: IIndexPattern; schema: FormSchema; @@ -43,8 +46,8 @@ const EuiBadgeWrap = styled(EuiBadge)` } `; -const EuiFlexItemWidth = styled(EuiFlexItem)` - width: 50%; +const EuiFlexItemWidth = styled(EuiFlexItem)<{ direction: string }>` + ${props => (props.direction === 'row' ? 'width : 50%;' : 'width: 100%;')}; `; const MyEuiListGroup = styled(EuiListGroup)` @@ -60,21 +63,33 @@ const ThreatsEuiFlexGroup = styled(EuiFlexGroup)` } `; +const MyEuiTextArea = styled(EuiTextArea)` + max-width: 100%; + height: 80px; +`; + export const StepRuleDescription = memo( - ({ data, indexPatterns, schema }) => { + ({ data, direction = 'row', indexPatterns, schema }) => { + const core = useKibanaCore(); + const [filterManager] = useState(new FilterManager(core.uiSettings)); + const keys = Object.keys(schema); const listItems = keys.reduce( (acc: ListItems[], key: string) => [ ...acc, - ...buildListItems(data, pick(key, schema), indexPatterns), + ...buildListItems(data, pick(key, schema), filterManager, indexPatterns), ], [] ); return ( - + {chunk(Math.ceil(listItems.length / 2), listItems).map((chunckListItems, index) => ( - - + + ))} @@ -90,12 +105,19 @@ interface ListItems { const buildListItems = ( data: unknown, schema: FormSchema, + filterManager: FilterManager, indexPatterns?: IIndexPattern ): ListItems[] => Object.keys(schema).reduce( (acc, field) => [ ...acc, - ...getDescriptionItem(field, get([field, 'label'], schema), data, indexPatterns), + ...getDescriptionItem( + field, + get([field, 'label'], schema), + data, + filterManager, + indexPatterns + ), ], [] ); @@ -104,29 +126,35 @@ const getDescriptionItem = ( field: string, label: string, value: unknown, + filterManager: FilterManager, indexPatterns?: IIndexPattern ): ListItems[] => { if (field === 'useIndicesConfig') { return []; - } else if (field === 'queryBar' && indexPatterns != null) { + } else if (field === 'queryBar') { const filters = get('queryBar.filters', value) as esFilters.Filter[]; const query = get('queryBar.query', value) as Query; const savedId = get('queryBar.saved_id', value); let items: ListItems[] = []; if (!isEmpty(filters)) { + filterManager.setFilters(filters); items = [ ...items, { title: <>{I18n.FILTERS_LABEL}, description: ( - {filters.map((filter, index) => ( + {filterManager.getFilters().map((filter, index) => ( - + {indexPatterns != null ? ( + + ) : ( + + )} ))} @@ -202,7 +230,7 @@ const getDescriptionItem = ( return [ { title: label, - description: , + description: , }, ]; } else if (Array.isArray(get(field, value))) { @@ -212,7 +240,7 @@ const getDescriptionItem = ( { title: label, description: ( - + {values.map((val: string) => isEmpty(val) ? null : ( @@ -227,10 +255,14 @@ const getDescriptionItem = ( } return []; } - return [ - { - title: label, - description: get(field, value), - }, - ]; + const description: string = get(field, value); + if (!isEmpty(description)) { + return [ + { + title: label, + description, + }, + ]; + } + return []; }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/translations.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/translations.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/translations.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/translations.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx index 4c0f477ab525e9..a9ae9606db58a2 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx @@ -19,20 +19,20 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; - +import { noop } from 'lodash/fp'; import React, { useCallback, useState } from 'react'; import { failure } from 'io-ts/lib/PathReporter'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import uuid from 'uuid'; -import * as i18n from './translations'; -import { duplicateRules } from '../../../../../containers/detection_engine/rules/api'; + +import { duplicateRules, RulesSchema } from '../../../../../containers/detection_engine/rules'; import { useKibanaUiSetting } from '../../../../../lib/settings/use_kibana_ui_setting'; import { DEFAULT_KBN_VERSION } from '../../../../../../common/constants'; -import { ndjsonToJSON } from '../json_downloader'; -import { RulesSchema } from '../../../../../containers/detection_engine/rules/types'; import { useStateToaster } from '../../../../../components/toasters'; +import { ndjsonToJSON } from '../json_downloader'; +import * as i18n from './translations'; interface ImportRuleModalProps { showModal: boolean; @@ -138,7 +138,7 @@ export const ImportRuleModalComponent = ({ id="rule-overwrite-saved-object" label={i18n.OVERWRITE_WITH_SAME_NAME} disabled={true} - onChange={() => {}} + onChange={() => noop} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/mitre/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx similarity index 98% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/mitre/index.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx index 6ab4ca4b514475..a777506ee12ae5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/mitre/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx @@ -20,7 +20,7 @@ import React, { ChangeEvent, useCallback } from 'react'; import styled from 'styled-components'; import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; -import * as CreateRuleI18n from '../../translations'; +import * as RuleI18n from '../../translations'; import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; import * as I18n from './translations'; import { IMitreEnterpriseAttack } from '../../types'; @@ -154,7 +154,7 @@ export const AddMitreThreat = ({ dataTestSubj, field, idAria, isDisabled }: AddI iconType="trash" isDisabled={isDisabled} onClick={() => removeItem(index)} - aria-label={CreateRuleI18n.DELETE} + aria-label={RuleI18n.DELETE} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/mitre/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/mitre/translations.ts rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx similarity index 99% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/query_bar/index.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx index 8dc402f00e621c..d8d77fcf8abffb 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx @@ -217,6 +217,7 @@ export const QueryBarDefineRule = ({ onSubmitQuery={onSubmitQuery} savedQuery={savedQuery} onSavedQuery={onSavedQuery} + hideSavedQuery={false} />
)} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap index 98f8ae6a80e07f..f264dde07c5942 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap @@ -14,7 +14,7 @@ exports[`RuleSwitch renders correctly against snapshot 1`] = ` disabled={false} label="rule-switch" onChange={[Function]} - showLabel={false} + showLabel={true} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx index fcea7101ba54b3..6809acd4b33f62 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx @@ -7,17 +7,26 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; + +import { useKibanaCore } from '../../../../../lib/compose/kibana_core'; + import { RuleSwitchComponent } from './index'; +const mockUseKibanaCore = useKibanaCore as jest.Mock; +jest.mock('../../../../../lib/compose/kibana_core'); +mockUseKibanaCore.mockImplementation(() => ({ + uiSettings: { + get$: () => 'world', + }, + injectedMetadata: { + getKibanaVersion: () => '8.0.0', + }, +})); + describe('RuleSwitch', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( - + ); expect(toJson(wrapper)).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx index 6d9b0a36f8548b..a5d983ec8e224d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx @@ -4,9 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ +import { + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiSwitch, + EuiSwitchEvent, +} from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; import styled from 'styled-components'; -import React, { useCallback } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSwitch } from '@elastic/eui'; +import React, { useCallback, useState, useEffect } from 'react'; + +import { DEFAULT_KBN_VERSION } from '../../../../../../common/constants'; +import { enableRules } from '../../../../../containers/detection_engine/rules'; +import { useKibanaUiSetting } from '../../../../../lib/settings/use_kibana_ui_setting'; +import { enableRulesAction } from '../../all/actions'; +import { Action } from '../../all/reducer'; const StaticSwitch = styled(EuiSwitch)` .euiSwitch__thumb, @@ -17,43 +30,75 @@ const StaticSwitch = styled(EuiSwitch)` StaticSwitch.displayName = 'StaticSwitch'; -export type RuleStateChangeCallback = (isEnabled: boolean, id: string) => void; - export interface RuleSwitchProps { + dispatch?: React.Dispatch; id: string; enabled: boolean; - isLoading: boolean; - onRuleStateChange: RuleStateChangeCallback; + isLoading?: boolean; + optionLabel?: string; } /** * Basic switch component for displaying loader when enabled/disabled */ export const RuleSwitchComponent = ({ + dispatch, id, - enabled, isLoading, - onRuleStateChange, + enabled, + optionLabel, }: RuleSwitchProps) => { - const handleChange = useCallback( - e => { - onRuleStateChange(e.target.checked!, id); + const [myIsLoading, setMyIsLoading] = useState(false); + const [myEnabled, setMyEnabled] = useState(enabled ?? false); + const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION); + + const onRuleStateChange = useCallback( + async (event: EuiSwitchEvent) => { + setMyIsLoading(true); + if (dispatch != null) { + await enableRulesAction([id], event.target.checked!, dispatch, kbnVersion); + } else { + try { + const updatedRules = await enableRules({ + ids: [id], + enabled: event.target.checked!, + kbnVersion, + }); + setMyEnabled(updatedRules[0].enabled); + } catch { + setMyIsLoading(false); + } + } + setMyIsLoading(false); }, - [onRuleStateChange, id] + [dispatch, id, kbnVersion] ); + + useEffect(() => { + if (myEnabled !== enabled) { + setMyEnabled(enabled); + } + }, [enabled]); + + useEffect(() => { + if (myIsLoading !== isLoading) { + setMyIsLoading(isLoading ?? false); + } + }, [isLoading]); + return ( - {isLoading ? ( + {myIsLoading ? ( ) : ( )} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/schedule_item_form/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx similarity index 84% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/schedule_item_form/index.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx index ebb365f6087a92..2e57ff8ba2c4f0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/schedule_item_form/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx @@ -37,23 +37,25 @@ export const ScheduleItem = ({ dataTestSubj, field, idAria, isDisabled }: Schedu const [timeVal, setTimeVal] = useState(0); const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - const onChangeTimeType = useCallback(e => { - setTimeType(e.target.value); - }, []); - - const onChangeTimeVal = useCallback(e => { - const sanitizedValue: number = parseInt(e.target.value, 10); - setTimeVal(isNaN(sanitizedValue) ? 0 : sanitizedValue); - }, []); + const onChangeTimeType = useCallback( + e => { + setTimeType(e.target.value); + field.setValue(`${timeVal}${e.target.value}`); + }, + [timeVal] + ); - useEffect(() => { - if (!isEmpty(timeVal) && Number(timeVal) >= 0 && field.value !== `${timeVal}${timeType}`) { - field.setValue(`${timeVal}${timeType}`); - } - }, [field.value, timeType, timeVal]); + const onChangeTimeVal = useCallback( + e => { + const sanitizedValue: number = parseInt(e.target.value, 10); + setTimeVal(sanitizedValue); + field.setValue(`${sanitizedValue}${timeType}`); + }, + [timeType] + ); useEffect(() => { - if (!isEmpty(field.value)) { + if (field.value !== `${timeVal}${timeType}`) { const filterTimeVal = (field.value as string).match(/\d+/g); const filterTimeType = (field.value as string).match(/[a-zA-Z]+/g); if ( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/schedule_item_form/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/schedule_item_form/translations.ts rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/shared_imports.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/shared_imports.ts similarity index 97% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/shared_imports.ts rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/shared_imports.ts index 8eb85c9fe3faef..494da24be706ae 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/shared_imports.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/shared_imports.ts @@ -10,7 +10,9 @@ export { FieldHook, FIELD_TYPES, Form, + FormData, FormDataProvider, + FormHook, FormSchema, UseField, useForm, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/status_icon/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/status_icon/index.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/data.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/data.ts rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/default_value.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts similarity index 91% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/default_value.ts rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts index 504b5ca85a3ab6..c0c5ae77a1960e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/default_value.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts @@ -6,7 +6,7 @@ import { AboutStepRule } from '../../types'; -export const defaultValue: AboutStepRule = { +export const stepAboutDefaultValue: AboutStepRule = { name: '', description: '', isNew: true, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx new file mode 100644 index 00000000000000..e266c0b9ab47d6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx @@ -0,0 +1,214 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButton, EuiHorizontalRule, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { isEqual, get } from 'lodash/fp'; +import React, { memo, useCallback, useEffect, useState } from 'react'; + +import { RuleStepProps, RuleStep, AboutStepRule } from '../../types'; +import * as RuleI18n from '../../translations'; +import { Field, Form, FormDataProvider, getUseField, UseField, useForm } from '../shared_imports'; +import { AddItem } from '../add_item_form'; +import { defaultRiskScoreBySeverity, severityOptions, SeverityValue } from './data'; +import { stepAboutDefaultValue } from './default_value'; +import { schema } from './schema'; +import * as I18n from './translations'; +import { StepRuleDescription } from '../description_step'; +import { AddMitreThreat } from '../mitre'; + +const CommonUseField = getUseField({ component: Field }); + +interface StepAboutRuleProps extends RuleStepProps { + defaultValues?: AboutStepRule | null; +} + +export const StepAboutRule = memo( + ({ + defaultValues, + descriptionDirection = 'row', + isReadOnlyView, + isUpdateView = false, + isLoading, + setForm, + setStepData, + }) => { + const [myStepData, setMyStepData] = useState(stepAboutDefaultValue); + + const { form } = useForm({ + defaultValue: myStepData, + options: { stripEmptyFields: false }, + schema, + }); + + const onSubmit = useCallback(async () => { + if (setStepData) { + setStepData(RuleStep.aboutRule, null, false); + const { isValid, data } = await form.submit(); + if (isValid) { + setStepData(RuleStep.aboutRule, data, isValid); + setMyStepData({ ...data, isNew: false } as AboutStepRule); + } + } + }, [form]); + + useEffect(() => { + const { isNew, ...initDefaultValue } = myStepData; + if (defaultValues != null && !isEqual(initDefaultValue, defaultValues)) { + const myDefaultValues = { + ...defaultValues, + isNew: false, + }; + setMyStepData(myDefaultValues); + if (!isReadOnlyView) { + Object.keys(schema).forEach(key => { + const val = get(key, myDefaultValues); + if (val != null) { + form.setFieldValue(key, val); + } + }); + } + } + }, [defaultValues]); + + useEffect(() => { + if (setForm != null) { + setForm(RuleStep.aboutRule, form); + } + }, [form]); + + return isReadOnlyView && myStepData != null ? ( + + ) : ( + <> +
+ + + + + + + + + + {({ severity }) => { + const newRiskScore = defaultRiskScoreBySeverity[severity as SeverityValue]; + const riskScoreField = form.getFields().riskScore; + if (newRiskScore != null && riskScoreField.value !== newRiskScore) { + riskScoreField.setValue(newRiskScore); + } + return null; + }} + + + {!isUpdateView && ( + <> + + + + + {myStepData.isNew ? RuleI18n.CONTINUE : RuleI18n.UPDATE} + + + + + )} + + ); + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx similarity index 91% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/schema.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx index 534c3142335c52..c72312bb908361 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash/fp'; import React from 'react'; -import * as CreateRuleI18n from '../../translations'; +import * as RuleI18n from '../../translations'; import { IMitreEnterpriseAttack } from '../../types'; import { FIELD_TYPES, @@ -99,7 +99,7 @@ export const schema: FormSchema = { defaultMessage: 'Reference URLs', } ), - labelAppend: {CreateRuleI18n.OPTIONAL_FIELD}, + labelAppend: {RuleI18n.OPTIONAL_FIELD}, }, falsePositives: { label: i18n.translate( @@ -108,7 +108,7 @@ export const schema: FormSchema = { defaultMessage: 'False positives', } ), - labelAppend: {CreateRuleI18n.OPTIONAL_FIELD}, + labelAppend: {RuleI18n.OPTIONAL_FIELD}, }, threats: { label: i18n.translate( @@ -117,7 +117,7 @@ export const schema: FormSchema = { defaultMessage: 'MITRE ATT&CK', } ), - labelAppend: {CreateRuleI18n.OPTIONAL_FIELD}, + labelAppend: {RuleI18n.OPTIONAL_FIELD}, validations: [ { validator: ( @@ -146,6 +146,6 @@ export const schema: FormSchema = { label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTagsLabel', { defaultMessage: 'Tags', }), - labelAppend: {CreateRuleI18n.OPTIONAL_FIELD}, + labelAppend: {RuleI18n.OPTIONAL_FIELD}, }, }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/translations.ts rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx similarity index 57% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/index.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx index 6954bd6bf733f0..b94e3c35f3ea08 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx @@ -5,14 +5,14 @@ */ import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; -import { isEqual } from 'lodash/fp'; -import React, { memo, useCallback, useState } from 'react'; +import { isEqual, get } from 'lodash/fp'; +import React, { memo, useCallback, useState, useEffect } from 'react'; import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/public'; -import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules/fetch_index_patterns'; +import { useUiSetting$ } from '../../../../../../../../../../src/plugins/kibana_react/public'; +import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules'; import { DEFAULT_INDEX_KEY } from '../../../../../../common/constants'; -import { useKibanaUiSetting } from '../../../../../lib/settings/use_kibana_ui_setting'; -import * as CreateRuleI18n from '../../translations'; +import * as RuleI18n from '../../translations'; import { DefineStepRule, RuleStep, RuleStepProps } from '../../types'; import { StepRuleDescription } from '../description_step'; import { QueryBarDefineRule } from '../query_bar'; @@ -22,40 +22,102 @@ import * as I18n from './translations'; const CommonUseField = getUseField({ component: Field }); -export const StepDefineRule = memo( - ({ isEditView, isLoading, resizeParentContainer, setStepData }) => { +interface StepDefineRuleProps extends RuleStepProps { + defaultValues?: DefineStepRule | null; +} + +const stepDefineDefaultValue = { + index: [], + isNew: true, + queryBar: { + query: { query: '', language: 'kuery' }, + filters: [], + saved_id: null, + }, + useIndicesConfig: 'true', +}; + +const getStepDefaultValue = ( + indicesConfig: string[], + defaultValues: DefineStepRule | null +): DefineStepRule => { + if (defaultValues != null) { + return { + ...defaultValues, + isNew: false, + useIndicesConfig: `${isEqual(defaultValues.index, indicesConfig)}`, + }; + } else { + return { + ...stepDefineDefaultValue, + index: indicesConfig != null ? indicesConfig : [], + }; + } +}; + +export const StepDefineRule = memo( + ({ + defaultValues, + descriptionDirection = 'row', + isReadOnlyView, + isLoading, + isUpdateView = false, + resizeParentContainer, + setForm, + setStepData, + }) => { const [localUseIndicesConfig, setLocalUseIndicesConfig] = useState(''); + const [indicesConfig] = useUiSetting$(DEFAULT_INDEX_KEY); const [ { indexPatterns: indexPatternQueryBar, isLoading: indexPatternLoadingQueryBar }, setIndices, - ] = useFetchIndexPatterns(); - const [indicesConfig] = useKibanaUiSetting(DEFAULT_INDEX_KEY); - const [myStepData, setMyStepData] = useState({ - index: indicesConfig || [], - isNew: true, - queryBar: { - query: { query: '', language: 'kuery' }, - filters: [], - saved_id: null, - }, - useIndicesConfig: 'true', - }); + ] = useFetchIndexPatterns(defaultValues != null ? defaultValues.index : indicesConfig ?? []); + const [myStepData, setMyStepData] = useState(stepDefineDefaultValue); + const { form } = useForm({ - schema, defaultValue: myStepData, options: { stripEmptyFields: false }, + schema, }); const onSubmit = useCallback(async () => { - const { isValid, data } = await form.submit(); - if (isValid) { - setStepData(RuleStep.defineRule, data, isValid); - setMyStepData({ ...data, isNew: false } as DefineStepRule); + if (setStepData) { + setStepData(RuleStep.defineRule, null, false); + const { isValid, data } = await form.submit(); + if (isValid && setStepData) { + setStepData(RuleStep.defineRule, data, isValid); + setMyStepData({ ...data, isNew: false } as DefineStepRule); + } + } + }, [form]); + + useEffect(() => { + if (indicesConfig != null && defaultValues != null) { + const myDefaultValues = getStepDefaultValue(indicesConfig, defaultValues); + if (!isEqual(myDefaultValues, myStepData)) { + setMyStepData(myDefaultValues); + setLocalUseIndicesConfig(myDefaultValues.useIndicesConfig); + if (!isReadOnlyView) { + Object.keys(schema).forEach(key => { + const val = get(key, myDefaultValues); + if (val != null) { + form.setFieldValue(key, val); + } + }); + } + } + } + }, [defaultValues, indicesConfig]); + + useEffect(() => { + if (setForm != null) { + setForm(RuleStep.defineRule, form); } }, [form]); - return isEditView && myStepData != null ? ( + return isReadOnlyView && myStepData != null ? ( ( }} - - - - - {myStepData.isNew ? CreateRuleI18n.CONTINUE : CreateRuleI18n.UPDATE} - - - + {!isUpdateView && ( + <> + + + + + {myStepData.isNew ? RuleI18n.CONTINUE : RuleI18n.UPDATE} + + + + + )} ); } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx similarity index 95% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx index 0f6c5f72e16839..9b54ada8227c61 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx @@ -10,9 +10,7 @@ import { isEmpty } from 'lodash/fp'; import React from 'react'; import { esKuery } from '../../../../../../../../../../src/plugins/data/public'; - -import * as CreateRuleI18n from '../../translations'; - +import * as RuleI18n from '../../translations'; import { FieldValueQueryBar } from '../query_bar'; import { ERROR_CODE, @@ -40,7 +38,7 @@ export const schema: FormSchema = { label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.fiedIndicesLabel', { defaultMessage: 'Indices', }), - labelAppend: {CreateRuleI18n.OPTIONAL_FIELD}, + labelAppend: {RuleI18n.OPTIONAL_FIELD}, validations: [ { validator: emptyField( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/translations.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/translations.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/translations.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/translations.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/types.ts rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/types.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.tsx new file mode 100644 index 00000000000000..21b38a83dad9da --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiPanel, EuiProgress } from '@elastic/eui'; +import React, { memo } from 'react'; +import styled from 'styled-components'; + +import { HeaderSection } from '../../../../../components/header_section'; + +interface StepPanelProps { + children: React.ReactNode; + loading: boolean; + title: string; +} + +const MyPanel = styled(EuiPanel)` + poistion: relative; +`; + +export const StepPanel = memo(({ children, loading, title }) => { + return ( + + {loading && } + + {children} + + ); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx new file mode 100644 index 00000000000000..6f7e49bc8ab9a9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; +import { isEqual, get } from 'lodash/fp'; +import React, { memo, useCallback, useEffect, useState } from 'react'; + +import { RuleStep, RuleStepProps, ScheduleStepRule } from '../../types'; +import { StepRuleDescription } from '../description_step'; +import { ScheduleItem } from '../schedule_item_form'; +import { Form, UseField, useForm } from '../shared_imports'; +import { schema } from './schema'; +import * as I18n from './translations'; + +interface StepScheduleRuleProps extends RuleStepProps { + defaultValues?: ScheduleStepRule | null; +} + +const stepScheduleDefaultValue = { + enabled: true, + interval: '5m', + isNew: true, + from: '0m', +}; + +export const StepScheduleRule = memo( + ({ + defaultValues, + descriptionDirection = 'row', + isReadOnlyView, + isLoading, + isUpdateView = false, + setStepData, + setForm, + }) => { + const [myStepData, setMyStepData] = useState(stepScheduleDefaultValue); + + const { form } = useForm({ + defaultValue: myStepData, + options: { stripEmptyFields: false }, + schema, + }); + + const onSubmit = useCallback( + async (enabled: boolean) => { + if (setStepData) { + setStepData(RuleStep.scheduleRule, null, false); + const { isValid: newIsValid, data } = await form.submit(); + if (newIsValid) { + setStepData(RuleStep.scheduleRule, { ...data, enabled }, newIsValid); + setMyStepData({ ...data, isNew: false } as ScheduleStepRule); + } + } + }, + [form] + ); + + useEffect(() => { + const { isNew, ...initDefaultValue } = myStepData; + if (defaultValues != null && !isEqual(initDefaultValue, defaultValues)) { + const myDefaultValues = { + ...defaultValues, + isNew: false, + }; + setMyStepData(myDefaultValues); + if (!isReadOnlyView) { + Object.keys(schema).forEach(key => { + const val = get(key, myDefaultValues); + if (val != null) { + form.setFieldValue(key, val); + } + }); + } + } + }, [defaultValues]); + + useEffect(() => { + if (setForm != null) { + setForm(RuleStep.scheduleRule, form); + } + }, [form]); + + return isReadOnlyView && myStepData != null ? ( + + ) : ( + <> +
+ + + + + {!isUpdateView && ( + <> + + + + + {I18n.COMPLETE_WITHOUT_ACTIVATING} + + + + + {I18n.COMPLETE_WITH_ACTIVATING} + + + + + )} + + ); + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx similarity index 91% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/schema.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx index 7e7bd541bdb0b1..31e56265dec424 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx @@ -8,7 +8,7 @@ import { EuiText } from '@elastic/eui'; import React from 'react'; import { i18n } from '@kbn/i18n'; -import * as CreateRuleI18n from '../../translations'; +import * as RuleI18n from '../../translations'; import { FormSchema } from '../shared_imports'; export const schema: FormSchema = { @@ -33,7 +33,7 @@ export const schema: FormSchema = { defaultMessage: 'Additional look-back', } ), - labelAppend: {CreateRuleI18n.OPTIONAL_FIELD}, + labelAppend: {RuleI18n.OPTIONAL_FIELD}, helpText: i18n.translate( 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldAdditionalLookBackHelpText', { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/translations.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/translations.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_schedule_rule/translations.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/translations.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts similarity index 86% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/helpers.ts rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts index f6546a680ad81b..a25ccce569dd43 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts @@ -7,7 +7,7 @@ import { isEmpty } from 'lodash/fp'; import moment from 'moment'; -import { NewRule } from '../../../containers/detection_engine/rules/types'; +import { NewRule } from '../../../../containers/detection_engine/rules'; import { AboutStepRule, @@ -17,7 +17,7 @@ import { ScheduleStepRuleJson, AboutStepRuleJson, FormatRuleType, -} from './types'; +} from '../types'; const getTimeTypeValue = (time: string): { unit: string; value: number } => { const timeObj = { @@ -40,7 +40,7 @@ const getTimeTypeValue = (time: string): { unit: string; value: number } => { }; const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJson => { - const { queryBar, useIndicesConfig, ...rest } = defineStepData; + const { queryBar, useIndicesConfig, isNew, ...rest } = defineStepData; const { filters, query, saved_id: savedId } = queryBar; return { ...rest, @@ -52,8 +52,7 @@ const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJso }; const formatScheduleStepData = (scheduleData: ScheduleStepRule): ScheduleStepRuleJson => { - const formatScheduleData = scheduleData; - + const { isNew, ...formatScheduleData } = scheduleData; if (!isEmpty(formatScheduleData.interval) && !isEmpty(formatScheduleData.from)) { const { unit: intervalUnit, value: intervalValue } = getTimeTypeValue( formatScheduleData.interval @@ -64,12 +63,16 @@ const formatScheduleStepData = (scheduleData: ScheduleStepRule): ScheduleStepRul formatScheduleData.from = `now-${duration.asSeconds()}s`; formatScheduleData.to = 'now'; } - return formatScheduleData; + return { + ...formatScheduleData, + meta: { + from: scheduleData.from, + }, + }; }; const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRuleJson => { - const { falsePositives, references, riskScore, threats, ...rest } = aboutStepData; - + const { falsePositives, references, riskScore, threats, isNew, ...rest } = aboutStepData; return { false_positives: falsePositives.filter(item => !isEmpty(item)), references: references.filter(item => !isEmpty(item)), @@ -91,7 +94,8 @@ const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRuleJson => export const formatRule = ( defineStepData: DefineStepRule, aboutStepData: AboutStepRule, - scheduleData: ScheduleStepRule + scheduleData: ScheduleStepRule, + ruleId?: string ): NewRule => { const type: FormatRuleType = defineStepData.queryBar.saved_id != null ? 'saved_query' : 'query'; const persistData = { @@ -99,10 +103,6 @@ export const formatRule = ( ...formatDefineStepData(defineStepData), ...formatAboutStepData(aboutStepData), ...formatScheduleStepData(scheduleData), - meta: { - from: scheduleData.from, - }, }; - - return persistData; + return ruleId != null ? { id: ruleId, ...persistData } : persistData; }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx similarity index 73% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/index.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx index 393b72d16b0a49..3e8dbeba895461 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx @@ -9,19 +9,19 @@ import React, { useCallback, useRef, useState } from 'react'; import { Redirect } from 'react-router-dom'; import styled from 'styled-components'; -import { HeaderPage } from '../../../components/header_page'; -import { WrapperPage } from '../../../components/wrapper_page'; -import { AccordionTitle } from './components/accordion_title'; -import { StepAboutRule } from './components/step_about_rule'; -import { StepDefineRule } from './components/step_define_rule'; -import { StepScheduleRule } from './components/step_schedule_rule'; -import { usePersistRule } from '../../../containers/detection_engine/rules/persist_rule'; -import { SpyRoute } from '../../../utils/route/spy_routes'; - +import { HeaderPage } from '../../../../components/header_page'; +import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine'; +import { WrapperPage } from '../../../../components/wrapper_page'; +import { AccordionTitle } from '../components/accordion_title'; +import { StepAboutRule } from '../components/step_about_rule'; +import { StepDefineRule } from '../components/step_define_rule'; +import { StepScheduleRule } from '../components/step_schedule_rule'; +import { usePersistRule } from '../../../../containers/detection_engine/rules'; +import { SpyRoute } from '../../../../utils/route/spy_routes'; +import * as RuleI18n from '../translations'; +import { AboutStepRule, DefineStepRule, RuleStep, RuleStepData, ScheduleStepRule } from '../types'; import { formatRule } from './helpers'; import * as i18n from './translations'; -import { AboutStepRule, DefineStepRule, RuleStep, RuleStepData, ScheduleStepRule } from './types'; -import { DETECTION_ENGINE_PAGE_NAME } from '../../../components/link_to/redirect_to_detection_engine'; const stepsRuleOrder = [RuleStep.defineRule, RuleStep.aboutRule, RuleStep.scheduleRule]; @@ -44,41 +44,44 @@ export const CreateRuleComponent = React.memo(() => { [RuleStep.aboutRule]: { isValid: false, data: {} }, [RuleStep.scheduleRule]: { isValid: false, data: {} }, }); - const [isStepRuleInEditView, setIsStepRuleInEditView] = useState>({ + const [isStepRuleInReadOnlyView, setIsStepRuleInEditView] = useState>({ [RuleStep.defineRule]: false, [RuleStep.aboutRule]: false, [RuleStep.scheduleRule]: false, }); const [{ isLoading, isSaved }, setRule] = usePersistRule(); - const setStepData = (step: RuleStep, data: unknown, isValid: boolean) => { - stepsData.current[step] = { ...stepsData.current[step], data, isValid }; - if (isValid) { - const stepRuleIdx = stepsRuleOrder.findIndex(item => step === item); - if ([0, 1].includes(stepRuleIdx)) { - setIsStepRuleInEditView({ - ...isStepRuleInEditView, - [step]: true, - }); - if (openAccordionId !== stepsRuleOrder[stepRuleIdx + 1]) { - openCloseAccordion(stepsRuleOrder[stepRuleIdx + 1]); - setOpenAccordionId(stepsRuleOrder[stepRuleIdx + 1]); + const setStepData = useCallback( + (step: RuleStep, data: unknown, isValid: boolean) => { + stepsData.current[step] = { ...stepsData.current[step], data, isValid }; + if (isValid) { + const stepRuleIdx = stepsRuleOrder.findIndex(item => step === item); + if ([0, 1].includes(stepRuleIdx)) { + setIsStepRuleInEditView({ + ...isStepRuleInReadOnlyView, + [step]: true, + }); + if (openAccordionId !== stepsRuleOrder[stepRuleIdx + 1]) { + openCloseAccordion(stepsRuleOrder[stepRuleIdx + 1]); + setOpenAccordionId(stepsRuleOrder[stepRuleIdx + 1]); + } + } else if ( + stepRuleIdx === 2 && + stepsData.current[RuleStep.defineRule].isValid && + stepsData.current[RuleStep.aboutRule].isValid + ) { + setRule( + formatRule( + stepsData.current[RuleStep.defineRule].data as DefineStepRule, + stepsData.current[RuleStep.aboutRule].data as AboutStepRule, + stepsData.current[RuleStep.scheduleRule].data as ScheduleStepRule + ) + ); } - } else if ( - stepRuleIdx === 2 && - stepsData.current[RuleStep.defineRule].isValid && - stepsData.current[RuleStep.aboutRule].isValid - ) { - setRule( - formatRule( - stepsData.current[RuleStep.defineRule].data as DefineStepRule, - stepsData.current[RuleStep.aboutRule].data as AboutStepRule, - stepsData.current[RuleStep.scheduleRule].data as ScheduleStepRule - ) - ); } - } - }; + }, + [openAccordionId, stepsData.current, setRule] + ); const getAccordionType = useCallback( (accordionId: RuleStep) => { @@ -95,19 +98,23 @@ export const CreateRuleComponent = React.memo(() => { const defineRuleButton = ( ); const aboutRuleButton = ( - + ); const scheduleRuleButton = ( ); @@ -142,7 +149,7 @@ export const CreateRuleComponent = React.memo(() => { openAccordionId != null && openAccordionId !== id && !stepsData.current[openAccordionId].isValid && - !isStepRuleInEditView[id] && + !isStepRuleInReadOnlyView[id] && isOpen ) { openCloseAccordion(id); @@ -153,20 +160,20 @@ export const CreateRuleComponent = React.memo(() => { } } }, - [isStepRuleInEditView, openAccordionId] + [isStepRuleInReadOnlyView, openAccordionId] ); const manageIsEditable = useCallback( (id: RuleStep) => { setIsStepRuleInEditView({ - ...isStepRuleInEditView, + ...isStepRuleInReadOnlyView, [id]: false, }); }, - [isStepRuleInEditView] + [isStepRuleInReadOnlyView] ); - if (isSaved && stepsData.current[RuleStep.scheduleRule].isValid) { + if (isSaved) { return ; } @@ -201,7 +208,7 @@ export const CreateRuleComponent = React.memo(() => { > setHeightAccordion(height)} @@ -231,7 +238,7 @@ export const CreateRuleComponent = React.memo(() => { > @@ -260,7 +267,7 @@ export const CreateRuleComponent = React.memo(() => { > diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rule_details/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/translations.ts similarity index 83% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rule_details/translations.ts rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/translations.ts index 3dd5945ff597ca..884f3f3741228a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rule_details/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/translations.ts @@ -6,6 +6,6 @@ import { i18n } from '@kbn/i18n'; -export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.ruleDetails.pageTitle', { - defaultMessage: 'Rule details', +export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.createRule.pageTitle', { + defaultMessage: 'Create new rule', }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx new file mode 100644 index 00000000000000..b0cf183949dd91 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx @@ -0,0 +1,223 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButton, EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { memo, useMemo } from 'react'; +import { useParams } from 'react-router-dom'; +import { StickyContainer } from 'react-sticky'; + +import { FiltersGlobal } from '../../../../components/filters_global'; +import { FormattedDate } from '../../../../components/formatted_date'; +import { HeaderPage } from '../../../../components/header_page'; +import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine'; +import { SiemSearchBar } from '../../../../components/search_bar'; +import { WrapperPage } from '../../../../components/wrapper_page'; +import { useRule } from '../../../../containers/detection_engine/rules'; + +import { + indicesExistOrDataTemporarilyUnavailable, + WithSource, +} from '../../../../containers/source'; +import { SpyRoute } from '../../../../utils/route/spy_routes'; + +import { SignalsCharts } from '../../components/signals_chart'; +import { SignalsTable } from '../../components/signals'; +import { DetectionEngineEmptyPage } from '../../detection_engine_empty_page'; +import { useSignalInfo } from '../../components/signals_info'; +import { StepAboutRule } from '../components/step_about_rule'; +import { StepDefineRule } from '../components/step_define_rule'; +import { StepScheduleRule } from '../components/step_schedule_rule'; +import { buildSignalsRuleIdFilter } from '../../components/signals/default_config'; +import * as detectionI18n from '../../translations'; +import { RuleSwitch } from '../components/rule_switch'; +import { StepPanel } from '../components/step_panel'; +import { getStepsData } from '../helpers'; +import * as ruleI18n from '../translations'; +import * as i18n from './translations'; +import { GlobalTime } from '../../../../containers/global_time'; + +export const RuleDetailsComponent = memo(() => { + const { ruleId } = useParams(); + const [loading, rule] = useRule(ruleId); + const { aboutRuleData, defineRuleData, scheduleRuleData } = getStepsData({ + rule, + detailsView: true, + }); + const [lastSignals] = useSignalInfo({ ruleId }); + + const title = loading === true || rule === null ? : rule.name; + const subTitle = useMemo( + () => + loading === true || rule === null ? ( + + ) : ( + [ + + ), + }} + />, + rule?.updated_by != null ? ( + + ), + }} + /> + ) : ( + '' + ), + ] + ), + [loading, rule] + ); + + const signalDefaultFilters = useMemo( + () => (ruleId != null ? buildSignalsRuleIdFilter(ruleId) : []), + [ruleId] + ); + return ( + <> + + {({ indicesExist, indexPattern }) => { + return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + + {({ to, from }) => ( + + + + + + + + {detectionI18n.LAST_SIGNAL} + {': '} + {lastSignals} + + ) : null, + 'Status: Comming Soon', + ]} + title={title} + > + + + + + + + + + + {ruleI18n.EDIT_RULE_SETTINGS} + + + + + + + + + + + + + {defineRuleData != null && ( + + )} + + + + + + {aboutRuleData != null && ( + + )} + + + + + + {scheduleRuleData != null && ( + + )} + + + + + + + + + + + {ruleId != null && ( + + )} + + + )} + + ) : ( + + + + + + ); + }} + + + + + ); +}); +RuleDetailsComponent.displayName = 'RuleDetailsComponent'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/translations.ts new file mode 100644 index 00000000000000..9dbb3b0079b0b7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/translations.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.ruleDetails.pageTitle', { + defaultMessage: 'Rule details', +}); + +export const BACK_TO_RULES = i18n.translate( + 'xpack.siem.detectionEngine.ruleDetails.backToRulesDescription', + { + defaultMessage: 'Back to rules', + } +); + +export const EXPERIMENTAL = i18n.translate( + 'xpack.siem.detectionEngine.ruleDetails.experimentalDescription', + { + defaultMessage: 'Experimental', + } +); + +export const ACTIVATE_RULE = i18n.translate( + 'xpack.siem.detectionEngine.ruleDetails.activateRuleLabel', + { + defaultMessage: 'Activate', + } +); + +export const UNKNOWN = i18n.translate('xpack.siem.detectionEngine.ruleDetails.unknownDescription', { + defaultMessage: 'Unknown', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx new file mode 100644 index 00000000000000..8e32f82dff0b1c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx @@ -0,0 +1,323 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiButton, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTabbedContent, + EuiTabbedContentTab, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Redirect, useParams } from 'react-router-dom'; + +import { HeaderPage } from '../../../../components/header_page'; +import { WrapperPage } from '../../../../components/wrapper_page'; +import { SpyRoute } from '../../../../utils/route/spy_routes'; +import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine'; +import { useRule, usePersistRule } from '../../../../containers/detection_engine/rules'; +import { FormHook, FormData } from '../components/shared_imports'; +import { StepPanel } from '../components/step_panel'; +import { StepAboutRule } from '../components/step_about_rule'; +import { StepDefineRule } from '../components/step_define_rule'; +import { StepScheduleRule } from '../components/step_schedule_rule'; +import { formatRule } from '../create/helpers'; +import { getStepsData } from '../helpers'; +import * as ruleI18n from '../translations'; +import { RuleStep, DefineStepRule, AboutStepRule, ScheduleStepRule } from '../types'; +import * as i18n from './translations'; + +interface StepRuleForm { + isValid: boolean; +} +interface AboutStepRuleForm extends StepRuleForm { + data: AboutStepRule | null; +} +interface DefineStepRuleForm extends StepRuleForm { + data: DefineStepRule | null; +} +interface ScheduleStepRuleForm extends StepRuleForm { + data: ScheduleStepRule | null; +} + +export const EditRuleComponent = memo(() => { + const { ruleId } = useParams(); + const [loading, rule] = useRule(ruleId); + const [initForm, setInitForm] = useState(false); + const [myAboutRuleForm, setMyAboutRuleForm] = useState({ + data: null, + isValid: false, + }); + const [myDefineRuleForm, setMyDefineRuleForm] = useState({ + data: null, + isValid: false, + }); + const [myScheduleRuleForm, setMyScheduleRuleForm] = useState({ + data: null, + isValid: false, + }); + const [selectedTab, setSelectedTab] = useState(); + const stepsForm = useRef | null>>({ + [RuleStep.defineRule]: null, + [RuleStep.aboutRule]: null, + [RuleStep.scheduleRule]: null, + }); + const [{ isLoading, isSaved }, setRule] = usePersistRule(); + const [tabHasError, setTabHasError] = useState([]); + const setStepsForm = useCallback( + (step: RuleStep, form: FormHook) => { + stepsForm.current[step] = form; + if (initForm && step === (selectedTab?.id as RuleStep) && form.isSubmitted === false) { + setInitForm(false); + form.submit(); + } + }, + [initForm, selectedTab] + ); + const tabs = useMemo( + () => [ + { + id: RuleStep.defineRule, + name: ruleI18n.DEFINITION, + content: ( + <> + + + {myDefineRuleForm.data != null && ( + + )} + + + + ), + }, + { + id: RuleStep.aboutRule, + name: ruleI18n.ABOUT, + content: ( + <> + + + {myAboutRuleForm.data != null && ( + + )} + + + + ), + }, + { + id: RuleStep.scheduleRule, + name: ruleI18n.SCHEDULE, + content: ( + <> + + + {myScheduleRuleForm.data != null && ( + + )} + + + + ), + }, + ], + [ + loading, + isLoading, + myAboutRuleForm, + myDefineRuleForm, + myScheduleRuleForm, + setStepsForm, + stepsForm, + ] + ); + + const onSubmit = useCallback(async () => { + const activeFormId = selectedTab?.id as RuleStep; + const activeForm = await stepsForm.current[activeFormId]?.submit(); + + const invalidForms = [RuleStep.aboutRule, RuleStep.defineRule, RuleStep.scheduleRule].reduce< + RuleStep[] + >((acc, step) => { + if ( + (step === activeFormId && activeForm != null && !activeForm?.isValid) || + (step === RuleStep.aboutRule && !myAboutRuleForm.isValid) || + (step === RuleStep.defineRule && !myDefineRuleForm.isValid) || + (step === RuleStep.scheduleRule && !myScheduleRuleForm.isValid) + ) { + return [...acc, step]; + } + return acc; + }, []); + + if (invalidForms.length === 0 && activeForm != null) { + setTabHasError([]); + setRule( + formatRule( + (activeFormId === RuleStep.defineRule + ? activeForm.data + : myDefineRuleForm.data) as DefineStepRule, + (activeFormId === RuleStep.aboutRule + ? activeForm.data + : myAboutRuleForm.data) as AboutStepRule, + (activeFormId === RuleStep.scheduleRule + ? activeForm.data + : myScheduleRuleForm.data) as ScheduleStepRule, + ruleId + ) + ); + } else { + setTabHasError(invalidForms); + } + }, [stepsForm, myAboutRuleForm, myDefineRuleForm, myScheduleRuleForm, selectedTab, ruleId]); + + useEffect(() => { + if (rule != null) { + const { aboutRuleData, defineRuleData, scheduleRuleData } = getStepsData({ rule }); + setMyAboutRuleForm({ data: aboutRuleData, isValid: true }); + setMyDefineRuleForm({ data: defineRuleData, isValid: true }); + setMyScheduleRuleForm({ data: scheduleRuleData, isValid: true }); + } + }, [rule]); + + const onTabClick = useCallback( + async (tab: EuiTabbedContentTab) => { + if (selectedTab != null) { + const ruleStep = selectedTab.id as RuleStep; + const respForm = await stepsForm.current[ruleStep]?.submit(); + if (respForm != null) { + if (ruleStep === RuleStep.aboutRule) { + setMyAboutRuleForm({ + data: respForm.data as AboutStepRule, + isValid: respForm.isValid, + }); + } else if (ruleStep === RuleStep.defineRule) { + setMyDefineRuleForm({ + data: respForm.data as DefineStepRule, + isValid: respForm.isValid, + }); + } else if (ruleStep === RuleStep.scheduleRule) { + setMyScheduleRuleForm({ + data: respForm.data as ScheduleStepRule, + isValid: respForm.isValid, + }); + } + } + } + setInitForm(true); + setSelectedTab(tab); + }, + [selectedTab, stepsForm.current] + ); + + useEffect(() => { + if (rule != null) { + const { aboutRuleData, defineRuleData, scheduleRuleData } = getStepsData({ rule }); + setMyAboutRuleForm({ data: aboutRuleData, isValid: true }); + setMyDefineRuleForm({ data: defineRuleData, isValid: true }); + setMyScheduleRuleForm({ data: scheduleRuleData, isValid: true }); + } + }, [rule]); + + useEffect(() => { + setSelectedTab(tabs[0]); + }, []); + + if (isSaved || (rule != null && rule.immutable)) { + return ; + } + + return ( + <> + + + {tabHasError.length > 0 && ( + + { + if (t === RuleStep.aboutRule) { + return ruleI18n.ABOUT; + } else if (t === RuleStep.defineRule) { + return ruleI18n.DEFINITION; + } else if (t === RuleStep.scheduleRule) { + return ruleI18n.SCHEDULE; + } + return t; + }) + .join(', '), + }} + /> + + )} + + t.id === selectedTab?.id)} + onTabClick={onTabClick} + tabs={tabs} + /> + + + + + + + {i18n.CANCEL} + + + + + + {i18n.SAVE_CHANGES} + + + + + + + + ); +}); +EditRuleComponent.displayName = 'EditRuleComponent'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/translations.ts new file mode 100644 index 00000000000000..b81ae58e565f0b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/translations.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.editRule.pageTitle', { + defaultMessage: 'Edit rule settings', +}); + +export const CANCEL = i18n.translate('xpack.siem.detectionEngine.editRule.cancelTitle', { + defaultMessage: 'Cancel', +}); + +export const SAVE_CHANGES = i18n.translate('xpack.siem.detectionEngine.editRule.saveChangeTitle', { + defaultMessage: 'Save changes', +}); + +export const SORRY_ERRORS = i18n.translate( + 'xpack.siem.detectionEngine.editRule.errorMsgDescription', + { + defaultMessage: 'Sorry', + } +); + +export const BACK_TO = i18n.translate('xpack.siem.detectionEngine.editRule.backToDescription', { + defaultMessage: 'Back to', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx new file mode 100644 index 00000000000000..46301ae808919d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { pick } from 'lodash/fp'; + +import { esFilters } from '../../../../../../../../src/plugins/data/public'; +import { Rule } from '../../../containers/detection_engine/rules'; +import { AboutStepRule, DefineStepRule, IMitreEnterpriseAttack, ScheduleStepRule } from './types'; + +interface GetStepsData { + aboutRuleData: AboutStepRule | null; + defineRuleData: DefineStepRule | null; + scheduleRuleData: ScheduleStepRule | null; +} + +export const getStepsData = ({ + rule, + detailsView = false, +}: { + rule: Rule | null; + detailsView?: boolean; +}): GetStepsData => { + const defineRuleData: DefineStepRule | null = + rule != null + ? { + isNew: false, + index: rule.index, + queryBar: { + query: { query: rule.query as string, language: rule.language }, + filters: rule.filters as esFilters.Filter[], + saved_id: rule.saved_id ?? null, + }, + useIndicesConfig: 'true', + } + : null; + const aboutRuleData: AboutStepRule | null = + rule != null + ? { + isNew: false, + ...pick(['description', 'name', 'references', 'severity', 'tags', 'threats'], rule), + ...(detailsView ? { name: '' } : {}), + threats: rule.threats as IMitreEnterpriseAttack[], + falsePositives: rule.false_positives, + riskScore: rule.risk_score, + } + : null; + const scheduleRuleData: ScheduleStepRule | null = + rule != null + ? { + isNew: false, + ...pick(['enabled', 'interval'], rule), + from: + rule?.meta?.from != null + ? rule.meta.from.replace('now-', '') + : rule.from.replace('now-', ''), + } + : null; + + return { aboutRuleData, defineRuleData, scheduleRuleData }; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx index afff0f07dfac4c..8b4cc2a213589b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx @@ -4,20 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiTabbedContent } from '@elastic/eui'; +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; +import { FormattedRelativePreferenceDate } from '../../../components/formatted_date'; +import { getEmptyTagValue } from '../../../components/empty_value'; import { HeaderPage } from '../../../components/header_page'; - import { WrapperPage } from '../../../components/wrapper_page'; import { SpyRoute } from '../../../utils/route/spy_routes'; -import * as i18n from './translations'; -import { AllRules } from './all_rules'; -import { ActivityMonitor } from './activity_monitor'; -import { FormattedRelativePreferenceDate } from '../../../components/formatted_date'; -import { getEmptyTagValue } from '../../../components/empty_value'; + +import { AllRules } from './all'; import { ImportRuleModal } from './components/import_rule_modal'; +import * as i18n from './translations'; export const RulesComponent = React.memo(() => { const [showImportModal, setShowImportModal] = useState(false); @@ -62,27 +61,14 @@ export const RulesComponent = React.memo(() => { - + {i18n.ADD_NEW_RULE}
- , - }, - { - id: 'tabActivityMonitor', - name: i18n.ACTIVITY_MONITOR, - content: , - }, - ]} - /> + diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts index 9ae266e396f6de..ecd6bef942bfb1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts @@ -205,3 +205,46 @@ export const COLUMN_ACTIVATE = i18n.translate( defaultMessage: 'Activate', } ); + +export const DEFINE_RULE = i18n.translate('xpack.siem.detectionEngine.rules.defineRuleTitle', { + defaultMessage: 'Define Rule', +}); + +export const ABOUT_RULE = i18n.translate('xpack.siem.detectionEngine.rules.aboutRuleTitle', { + defaultMessage: 'About Rule', +}); + +export const SCHEDULE_RULE = i18n.translate('xpack.siem.detectionEngine.rules.scheduleRuleTitle', { + defaultMessage: 'Schedule Rule', +}); + +export const DEFINITION = i18n.translate('xpack.siem.detectionEngine.rules.stepDefinitionTitle', { + defaultMessage: 'Definition', +}); + +export const ABOUT = i18n.translate('xpack.siem.detectionEngine.rules.stepAboutTitle', { + defaultMessage: 'About', +}); + +export const SCHEDULE = i18n.translate('xpack.siem.detectionEngine.rules.stepScheduleTitle', { + defaultMessage: 'Schedule', +}); + +export const OPTIONAL_FIELD = i18n.translate( + 'xpack.siem.detectionEngine.rules.optionalFieldDescription', + { + defaultMessage: 'Optional', + } +); + +export const CONTINUE = i18n.translate('xpack.siem.detectionEngine.rules.continueButtonTitle', { + defaultMessage: 'Continue', +}); + +export const UPDATE = i18n.translate('xpack.siem.detectionEngine.rules.updateButtonTitle', { + defaultMessage: 'Update', +}); + +export const DELETE = i18n.translate('xpack.siem.detectionEngine.rules.deleteDescription', { + defaultMessage: 'Delete', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts index de3fa839ea91e4..9b535034810bd5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Rule } from '../../../containers/detection_engine/rules/types'; +import { esFilters } from '../../../../../../../../src/plugins/data/common'; +import { Rule } from '../../../containers/detection_engine/rules'; +import { FieldValueQueryBar } from './components/query_bar'; +import { FormData, FormHook } from './components/shared_imports'; export interface EuiBasicTableSortTypes { field: string; @@ -39,3 +42,96 @@ export interface TableData { isLoading: boolean; sourceRule: Rule; } + +export enum RuleStep { + defineRule = 'define-rule', + aboutRule = 'about-rule', + scheduleRule = 'schedule-rule', +} +export type RuleStatusType = 'passive' | 'active' | 'valid'; + +export interface RuleStepData { + data: unknown; + isValid: boolean; +} + +export interface RuleStepProps { + descriptionDirection?: 'row' | 'column'; + setStepData?: (step: RuleStep, data: unknown, isValid: boolean) => void; + isReadOnlyView: boolean; + isUpdateView?: boolean; + isLoading: boolean; + resizeParentContainer?: (height: number) => void; + setForm?: (step: RuleStep, form: FormHook) => void; +} + +interface StepRuleData { + isNew: boolean; +} +export interface AboutStepRule extends StepRuleData { + name: string; + description: string; + severity: string; + riskScore: number; + references: string[]; + falsePositives: string[]; + tags: string[]; + threats: IMitreEnterpriseAttack[]; +} + +export interface DefineStepRule extends StepRuleData { + useIndicesConfig: string; + index: string[]; + queryBar: FieldValueQueryBar; +} + +export interface ScheduleStepRule extends StepRuleData { + enabled: boolean; + interval: string; + from: string; + to?: string; +} + +export interface DefineStepRuleJson { + index: string[]; + filters: esFilters.Filter[]; + saved_id?: string; + query: string; + language: string; +} + +export interface AboutStepRuleJson { + name: string; + description: string; + severity: string; + risk_score: number; + references: string[]; + false_positives: string[]; + tags: string[]; + threats: IMitreEnterpriseAttack[]; +} + +export interface ScheduleStepRuleJson { + enabled: boolean; + interval: string; + from: string; + to?: string; + meta?: unknown; +} + +export type MyRule = Omit & { + immutable: boolean; +}; + +export type FormatRuleType = 'query' | 'saved_query'; + +export interface IMitreAttack { + id: string; + name: string; + reference: string; +} +export interface IMitreEnterpriseAttack { + framework: string; + tactic: IMitreAttack; + techniques: IMitreAttack[]; +} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts index a7e7fa5133a646..94adbfcde1e53a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts @@ -10,8 +10,16 @@ export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.pageTitle', defaultMessage: 'Detection engine', }); -export const PAGE_SUBTITLE = i18n.translate('xpack.siem.detectionEngine.pageSubtitle', { - defaultMessage: 'Last signal: X minutes ago', +export const LAST_SIGNAL = i18n.translate('xpack.siem.detectionEngine.lastSignalTitle', { + defaultMessage: 'Last signal:', +}); + +export const TOTAL_SIGNAL = i18n.translate('xpack.siem.detectionEngine.totalSignalTitle', { + defaultMessage: 'Total', +}); + +export const SIGNAL = i18n.translate('xpack.siem.detectionEngine.signalTitle', { + defaultMessage: 'Signals', }); export const BUTTON_MANAGE_RULES = i18n.translate('xpack.siem.detectionEngine.buttonManageRules', { diff --git a/x-pack/legacy/plugins/siem/server/kibana.index.ts b/x-pack/legacy/plugins/siem/server/kibana.index.ts index 647894e9e71872..59df6432468352 100644 --- a/x-pack/legacy/plugins/siem/server/kibana.index.ts +++ b/x-pack/legacy/plugins/siem/server/kibana.index.ts @@ -22,6 +22,9 @@ import { isAlertExecutor } from './lib/detection_engine/signals/types'; import { readTagsRoute } from './lib/detection_engine/routes/tags/read_tags_route'; import { readPrivilegesRoute } from './lib/detection_engine/routes/privileges/read_privileges_route'; import { addPrepackedRulesRoute } from './lib/detection_engine/routes/rules/add_prepackaged_rules_route'; +import { createRulesBulkRoute } from './lib/detection_engine/routes/rules/create_rules_bulk_route'; +import { updateRulesBulkRoute } from './lib/detection_engine/routes/rules/update_rules_bulk_route'; +import { deleteRulesBulkRoute } from './lib/detection_engine/routes/rules/delete_rules_bulk_route'; const APP_ID = 'siem'; @@ -44,6 +47,9 @@ export const initServerWithKibana = (context: PluginInitializerContext, __legacy deleteRulesRoute(__legacy); findRulesRoute(__legacy); addPrepackedRulesRoute(__legacy); + createRulesBulkRoute(__legacy); + updateRulesBulkRoute(__legacy); + deleteRulesBulkRoute(__legacy); // Detection Engine Signals routes that have the REST endpoints of /api/detection_engine/signals // POST /api/detection_engine/signals/status diff --git a/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts b/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts index 2e4dfbc31b65bc..30fdf7520a3ed1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts +++ b/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup, PluginInitializerContext } from 'src/core/server'; +import { CoreSetup, PluginInitializerContext } from '../../../../../../../src/core/server'; +import { PluginsSetup } from '../../plugin'; + import { Anomalies } from '../anomalies'; import { ElasticsearchAnomaliesAdapter } from '../anomalies/elasticsearch_adapter'; import { Authentications } from '../authentications'; @@ -33,8 +35,12 @@ import { PinnedEvent } from '../pinned_event/saved_object'; import { Timeline } from '../timeline/saved_object'; import { Alerts, ElasticsearchAlertsAdapter } from '../alerts'; -export function compose(core: CoreSetup, env: PluginInitializerContext['env']): AppBackendLibs { - const framework = new KibanaBackendFrameworkAdapter(core, env); +export function compose( + core: CoreSetup, + plugins: PluginsSetup, + env: PluginInitializerContext['env'] +): AppBackendLibs { + const framework = new KibanaBackendFrameworkAdapter(core, plugins, env); const sources = new Sources(new ConfigurationSourcesAdapter()); const sourceStatus = new SourceStatus(new ElasticsearchSourceStatusAdapter(framework)); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 8cdc56b07c1523..a78879924acd05 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -117,6 +117,42 @@ export const getFindRequest = (): ServerInjectOptions => ({ url: `${DETECTION_ENGINE_RULES_URL}/_find`, }); +export const getReadBulkRequest = (): ServerInjectOptions => ({ + method: 'POST', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, + payload: [typicalPayload()], +}); + +export const getUpdateBulkRequest = (): ServerInjectOptions => ({ + method: 'PUT', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + payload: [typicalPayload()], +}); + +export const getDeleteBulkRequest = (): ServerInjectOptions => ({ + method: 'DELETE', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, + payload: [{ rule_id: 'rule-1' }], +}); + +export const getDeleteBulkRequestById = (): ServerInjectOptions => ({ + method: 'DELETE', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, + payload: [{ id: 'rule-04128c15-0d1b-4716-a4c5-46997ac7f3bd' }], +}); + +export const getDeleteAsPostBulkRequestById = (): ServerInjectOptions => ({ + method: 'POST', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, + payload: [{ id: 'rule-04128c15-0d1b-4716-a4c5-46997ac7f3bd' }], +}); + +export const getDeleteAsPostBulkRequest = (): ServerInjectOptions => ({ + method: 'POST', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, + payload: [{ rule_id: 'rule-1' }], +}); + export const getPrivilegeRequest = (): ServerInjectOptions => ({ method: 'GET', url: `${DETECTION_ENGINE_PRIVILEGES_URL}`, @@ -278,6 +314,7 @@ export const getResult = (): RuleAlertType => ({ throttle: null, createdBy: 'elastic', updatedBy: 'elastic', + apiKey: null, apiKeyOwner: 'elastic', muteAll: false, mutedInstanceIds: [], diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts new file mode 100644 index 00000000000000..d3890f82c8abf7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + createMockServer, + createMockServerWithoutActionClientDecoration, + createMockServerWithoutAlertClientDecoration, + createMockServerWithoutActionOrAlertClientDecoration, +} from '../__mocks__/_mock_server'; +import { createRulesRoute } from './create_rules_route'; +import { ServerInjectOptions } from 'hapi'; +import { + getFindResult, + getResult, + createActionResult, + typicalPayload, + getReadBulkRequest, +} from '../__mocks__/request_responses'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { createRulesBulkRoute } from './create_rules_bulk_route'; + +describe('create_rules_bulk', () => { + let { server, alertsClient, actionsClient, elasticsearch } = createMockServer(); + + beforeEach(() => { + jest.resetAllMocks(); + ({ server, alertsClient, actionsClient, elasticsearch } = createMockServer()); + elasticsearch.getCluster = jest.fn().mockImplementation(() => ({ + callWithRequest: jest.fn().mockImplementation(() => true), + })); + + createRulesBulkRoute(server); + }); + + describe('status codes with actionClient and alertClient', () => { + test('returns 200 when creating a single rule with a valid actionClient and alertClient', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.create.mockResolvedValue(createActionResult()); + alertsClient.create.mockResolvedValue(getResult()); + const { statusCode } = await server.inject(getReadBulkRequest()); + expect(statusCode).toBe(200); + }); + + test('returns 404 if actionClient is not available on the route', async () => { + const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); + createRulesRoute(serverWithoutActionClient); + const { statusCode } = await serverWithoutActionClient.inject(getReadBulkRequest()); + expect(statusCode).toBe(404); + }); + + test('returns 404 if alertClient is not available on the route', async () => { + const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); + createRulesRoute(serverWithoutAlertClient); + const { statusCode } = await serverWithoutAlertClient.inject(getReadBulkRequest()); + expect(statusCode).toBe(404); + }); + + test('returns 404 if alertClient and actionClient are both not available on the route', async () => { + const { + serverWithoutActionOrAlertClient, + } = createMockServerWithoutActionOrAlertClientDecoration(); + createRulesRoute(serverWithoutActionOrAlertClient); + const { statusCode } = await serverWithoutActionOrAlertClient.inject(getReadBulkRequest()); + expect(statusCode).toBe(404); + }); + }); + + describe('validation', () => { + test('returns 200 if rule_id is not given as the id is auto generated from the alert framework', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.create.mockResolvedValue(createActionResult()); + alertsClient.create.mockResolvedValue(getResult()); + // missing rule_id should return 200 as it will be auto generated if not given + const { rule_id, ...noRuleId } = typicalPayload(); + const request: ServerInjectOptions = { + method: 'POST', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, + payload: [noRuleId], + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + }); + + test('returns 200 if type is query', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.create.mockResolvedValue(createActionResult()); + alertsClient.create.mockResolvedValue(getResult()); + const { type, ...noType } = typicalPayload(); + const request: ServerInjectOptions = { + method: 'POST', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, + payload: [ + { + ...noType, + type: 'query', + }, + ], + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + }); + + test('returns 400 if type is not filter or kql', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.create.mockResolvedValue(createActionResult()); + alertsClient.create.mockResolvedValue(getResult()); + const { type, ...noType } = typicalPayload(); + const request: ServerInjectOptions = { + method: 'POST', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, + payload: [ + { + ...noType, + type: 'something-made-up', + }, + ], + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(400); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts new file mode 100644 index 00000000000000..256b341fca656a --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; +import { isFunction } from 'lodash/fp'; +import uuid from 'uuid'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { createRules } from '../../rules/create_rules'; +import { BulkRulesRequest } from '../../rules/types'; +import { ServerFacade } from '../../../../types'; +import { readRules } from '../../rules/read_rules'; +import { transformOrBulkError } from './utils'; +import { getIndexExists } from '../../index/get_index_exists'; +import { + callWithRequestFactory, + getIndex, + transformBulkError, + createBulkErrorObject, +} from '../utils'; +import { createRulesBulkSchema } from '../schemas/create_rules_bulk_schema'; + +export const createCreateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRoute => { + return { + method: 'POST', + path: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, + options: { + tags: ['access:siem'], + validate: { + options: { + abortEarly: false, + }, + payload: createRulesBulkSchema, + }, + }, + async handler(request: BulkRulesRequest, headers) { + const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + const actionsClient = isFunction(request.getActionsClient) + ? request.getActionsClient() + : null; + + if (!alertsClient || !actionsClient) { + return headers.response().code(404); + } + + const rules = Promise.all( + request.payload.map(async payloadRule => { + const { + created_at: createdAt, + description, + enabled, + false_positives: falsePositives, + from, + immutable, + query, + language, + output_index: outputIndex, + saved_id: savedId, + meta, + filters, + rule_id: ruleId, + index, + interval, + max_signals: maxSignals, + risk_score: riskScore, + name, + severity, + tags, + threats, + to, + type, + updated_at: updatedAt, + references, + timeline_id: timelineId, + version, + } = payloadRule; + const ruleIdOrUuid = ruleId ?? uuid.v4(); + try { + const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server); + const callWithRequest = callWithRequestFactory(request, server); + const indexExists = await getIndexExists(callWithRequest, finalIndex); + if (!indexExists) { + return createBulkErrorObject({ + ruleId: ruleIdOrUuid, + statusCode: 409, + message: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`, + }); + } + if (ruleId != null) { + const rule = await readRules({ alertsClient, ruleId }); + if (rule != null) { + return createBulkErrorObject({ + ruleId, + statusCode: 409, + message: `rule_id: "${ruleId}" already exists`, + }); + } + } + const createdRule = await createRules({ + alertsClient, + actionsClient, + createdAt, + description, + enabled, + falsePositives, + from, + immutable, + query, + language, + outputIndex: finalIndex, + savedId, + timelineId, + meta, + filters, + ruleId: ruleIdOrUuid, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threats, + updatedAt, + references, + version, + }); + return transformOrBulkError(ruleIdOrUuid, createdRule); + } catch (err) { + return transformBulkError(ruleIdOrUuid, err); + } + }) + ); + return rules; + }, + }; +}; + +export const createRulesBulkRoute = (server: ServerFacade): void => { + server.route(createCreateRulesBulkRoute(server)); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts new file mode 100644 index 00000000000000..11a076951fd8cb --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + createMockServer, + createMockServerWithoutActionClientDecoration, + createMockServerWithoutAlertClientDecoration, + createMockServerWithoutActionOrAlertClientDecoration, +} from '../__mocks__/_mock_server'; + +import { ServerInjectOptions } from 'hapi'; +import { + getFindResult, + getResult, + getFindResultWithSingleHit, + getDeleteBulkRequest, + getDeleteBulkRequestById, + getDeleteAsPostBulkRequest, + getDeleteAsPostBulkRequestById, +} from '../__mocks__/request_responses'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; + +import { deleteRulesBulkRoute } from './delete_rules_bulk_route'; + +describe('delete_rules', () => { + let { server, alertsClient } = createMockServer(); + + beforeEach(() => { + ({ server, alertsClient } = createMockServer()); + deleteRulesBulkRoute(server); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('status codes with actionClient and alertClient', () => { + test('returns 200 when deleting a single rule with a valid actionClient and alertClient by alertId', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.delete.mockResolvedValue({}); + const { statusCode } = await server.inject(getDeleteBulkRequest()); + expect(statusCode).toBe(200); + }); + + test('returns 200 when deleting a single rule with a valid actionClient and alertClient by alertId using POST', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.delete.mockResolvedValue({}); + const { statusCode } = await server.inject(getDeleteAsPostBulkRequest()); + expect(statusCode).toBe(200); + }); + + test('returns 200 when deleting a single rule with a valid actionClient and alertClient by id', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.delete.mockResolvedValue({}); + const { statusCode } = await server.inject(getDeleteBulkRequestById()); + expect(statusCode).toBe(200); + }); + + test('returns 200 when deleting a single rule with a valid actionClient and alertClient by id using POST', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.delete.mockResolvedValue({}); + const { statusCode } = await server.inject(getDeleteAsPostBulkRequestById()); + expect(statusCode).toBe(200); + }); + + test('returns 200 because the error is in the payload when deleting a single rule that does not exist with a valid actionClient and alertClient', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.delete.mockResolvedValue({}); + const { statusCode } = await server.inject(getDeleteBulkRequest()); + expect(statusCode).toBe(200); + }); + + test('returns 404 in the payload when deleting a single rule that does not exist with a valid actionClient and alertClient', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.delete.mockResolvedValue({}); + const { payload } = await server.inject(getDeleteBulkRequest()); + const parsed = JSON.parse(payload); + expect(parsed).toEqual([ + { error: { message: 'rule_id: "rule-1" not found', statusCode: 404 }, id: 'rule-1' }, + ]); + }); + + test('returns 404 if actionClient is not available on the route', async () => { + const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); + deleteRulesBulkRoute(serverWithoutActionClient); + const { statusCode } = await serverWithoutActionClient.inject(getDeleteBulkRequest()); + expect(statusCode).toBe(404); + }); + + test('returns 404 if alertClient is not available on the route', async () => { + const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); + deleteRulesBulkRoute(serverWithoutAlertClient); + const { statusCode } = await serverWithoutAlertClient.inject(getDeleteBulkRequest()); + expect(statusCode).toBe(404); + }); + + test('returns 404 if alertClient and actionClient are both not available on the route', async () => { + const { + serverWithoutActionOrAlertClient, + } = createMockServerWithoutActionOrAlertClientDecoration(); + deleteRulesBulkRoute(serverWithoutActionOrAlertClient); + const { statusCode } = await serverWithoutActionOrAlertClient.inject(getDeleteBulkRequest()); + expect(statusCode).toBe(404); + }); + }); + + describe('validation', () => { + test('returns 400 if given a non-existent id in the payload', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + alertsClient.delete.mockResolvedValue({}); + const request: ServerInjectOptions = { + method: 'DELETE', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(400); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts new file mode 100644 index 00000000000000..a0801930f879a0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; +import { isFunction } from 'lodash/fp'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { deleteRules } from '../../rules/delete_rules'; +import { ServerFacade } from '../../../../types'; +import { queryRulesBulkSchema } from '../schemas/query_rules_bulk_schema'; +import { transformOrBulkError, getIdBulkError } from './utils'; +import { transformBulkError } from '../utils'; +import { QueryBulkRequest } from '../../rules/types'; + +export const createDeleteRulesBulkRoute: Hapi.ServerRoute = { + method: ['POST', 'DELETE'], // allow both POST and DELETE in case their client does not support bodies in DELETE + path: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, + options: { + tags: ['access:siem'], + validate: { + options: { + abortEarly: false, + }, + payload: queryRulesBulkSchema, + }, + }, + async handler(request: QueryBulkRequest, headers) { + const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; + + if (alertsClient == null || actionsClient == null) { + return headers.response().code(404); + } + const rules = Promise.all( + request.payload.map(async payloadRule => { + const { id, rule_id: ruleId } = payloadRule; + const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; + try { + const rule = await deleteRules({ + actionsClient, + alertsClient, + id, + ruleId, + }); + + if (rule != null) { + return transformOrBulkError(idOrRuleIdOrUnknown, rule); + } else { + return getIdBulkError({ id, ruleId }); + } + } catch (err) { + return transformBulkError(idOrRuleIdOrUnknown, err); + } + }) + ); + return rules; + }, +}; + +export const deleteRulesBulkRoute = (server: ServerFacade): void => { + server.route(createDeleteRulesBulkRoute); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts index c2b2e2fdbbaef7..8f771236269d95 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts @@ -13,7 +13,7 @@ import { ServerFacade } from '../../../../types'; import { queryRulesSchema } from '../schemas/query_rules_schema'; import { getIdError, transformOrError } from './utils'; import { transformError } from '../utils'; -import { QueryRequest } from './types'; +import { QueryRequest } from '../../rules/types'; export const createDeleteRulesRoute: Hapi.ServerRoute = { method: 'DELETE', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts index a842e68b6b7fe4..4ff26ae99bd136 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts @@ -13,7 +13,7 @@ import { transformError } from '../utils'; import { readRules } from '../../rules/read_rules'; import { ServerFacade } from '../../../../types'; import { queryRulesSchema } from '../schemas/query_rules_schema'; -import { QueryRequest } from './types'; +import { QueryRequest } from '../../rules/types'; export const createReadRulesRoute: Hapi.ServerRoute = { method: 'GET', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts new file mode 100644 index 00000000000000..9ae2941e6e5f24 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + createMockServer, + createMockServerWithoutActionClientDecoration, + createMockServerWithoutAlertClientDecoration, + createMockServerWithoutActionOrAlertClientDecoration, +} from '../__mocks__/_mock_server'; + +import { updateRulesRoute } from './update_rules_route'; +import { ServerInjectOptions } from 'hapi'; +import { + getFindResult, + getResult, + updateActionResult, + typicalPayload, + getFindResultWithSingleHit, + getUpdateBulkRequest, +} from '../__mocks__/request_responses'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { updateRulesBulkRoute } from './update_rules_bulk_route'; + +describe('update_rules_bulk', () => { + let { server, alertsClient, actionsClient } = createMockServer(); + + beforeEach(() => { + jest.resetAllMocks(); + ({ server, alertsClient, actionsClient } = createMockServer()); + updateRulesBulkRoute(server); + }); + + describe('status codes with actionClient and alertClient', () => { + test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const { statusCode } = await server.inject(getUpdateBulkRequest()); + expect(statusCode).toBe(200); + }); + + test('returns 200 as a response when updating a single rule that does not exist', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const { statusCode } = await server.inject(getUpdateBulkRequest()); + expect(statusCode).toBe(200); + }); + + test('returns 404 within the payload when updating a single rule that does not exist', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const { payload } = await server.inject(getUpdateBulkRequest()); + const parsed = JSON.parse(payload); + expect(parsed).toEqual([ + { error: { message: 'rule_id: "rule-1" not found', statusCode: 404 }, id: 'rule-1' }, + ]); + }); + + test('returns 404 if actionClient is not available on the route', async () => { + const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); + updateRulesRoute(serverWithoutActionClient); + const { statusCode } = await serverWithoutActionClient.inject(getUpdateBulkRequest()); + expect(statusCode).toBe(404); + }); + + test('returns 404 if alertClient is not available on the route', async () => { + const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); + updateRulesRoute(serverWithoutAlertClient); + const { statusCode } = await serverWithoutAlertClient.inject(getUpdateBulkRequest()); + expect(statusCode).toBe(404); + }); + + test('returns 404 if alertClient and actionClient are both not available on the route', async () => { + const { + serverWithoutActionOrAlertClient, + } = createMockServerWithoutActionOrAlertClientDecoration(); + updateRulesRoute(serverWithoutActionOrAlertClient); + const { statusCode } = await serverWithoutActionOrAlertClient.inject(getUpdateBulkRequest()); + expect(statusCode).toBe(404); + }); + }); + + describe('validation', () => { + test('returns 400 if id is not given in either the body or the url', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + const { rule_id, ...noId } = typicalPayload(); + const request: ServerInjectOptions = { + method: 'PUT', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + payload: [noId], + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(400); + }); + + test('returns errors as 200 to just indicate ok something happened', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const request: ServerInjectOptions = { + method: 'PUT', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + payload: [typicalPayload()], + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toEqual(200); + }); + + test('returns 404 in the payload if the record does not exist yet', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const request: ServerInjectOptions = { + method: 'PUT', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + payload: [typicalPayload()], + }; + const { payload } = await server.inject(request); + const parsed = JSON.parse(payload); + expect(parsed).toEqual([ + { error: { message: 'rule_id: "rule-1" not found', statusCode: 404 }, id: 'rule-1' }, + ]); + }); + + test('returns 200 if type is query', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const request: ServerInjectOptions = { + method: 'PUT', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + payload: [typicalPayload()], + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + }); + + test('returns 400 if type is not filter or kql', async () => { + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.update.mockResolvedValue(updateActionResult()); + alertsClient.update.mockResolvedValue(getResult()); + const { type, ...noType } = typicalPayload(); + const request: ServerInjectOptions = { + method: 'PUT', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + payload: [ + { + ...noType, + type: 'something-made-up', + }, + ], + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(400); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts new file mode 100644 index 00000000000000..b30b6c791522b0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; +import { isFunction } from 'lodash/fp'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { BulkUpdateRulesRequest } from '../../rules/types'; +import { ServerFacade } from '../../../../types'; +import { transformOrBulkError, getIdBulkError } from './utils'; +import { transformBulkError } from '../utils'; +import { updateRulesBulkSchema } from '../schemas/update_rules_bulk_schema'; +import { updateRules } from '../../rules/update_rules'; + +export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRoute => { + return { + method: 'PUT', + path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + options: { + tags: ['access:siem'], + validate: { + options: { + abortEarly: false, + }, + payload: updateRulesBulkSchema, + }, + }, + async handler(request: BulkUpdateRulesRequest, headers) { + const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + const actionsClient = isFunction(request.getActionsClient) + ? request.getActionsClient() + : null; + + if (!alertsClient || !actionsClient) { + return headers.response().code(404); + } + + const rules = Promise.all( + request.payload.map(async payloadRule => { + const { + description, + enabled, + false_positives: falsePositives, + from, + immutable, + query, + language, + output_index: outputIndex, + saved_id: savedId, + timeline_id: timelineId, + meta, + filters, + rule_id: ruleId, + id, + index, + interval, + max_signals: maxSignals, + risk_score: riskScore, + name, + severity, + tags, + to, + type, + threats, + references, + version, + } = payloadRule; + const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; + try { + const rule = await updateRules({ + alertsClient, + actionsClient, + description, + enabled, + falsePositives, + from, + immutable, + query, + language, + outputIndex, + savedId, + timelineId, + meta, + filters, + id, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threats, + references, + version, + }); + if (rule != null) { + return transformOrBulkError(rule.id, rule); + } else { + return getIdBulkError({ id, ruleId }); + } + } catch (err) { + return transformBulkError(idOrRuleIdOrUnknown, err); + } + }) + ); + return rules; + }, + }; +}; + +export const updateRulesBulkRoute = (server: ServerFacade): void => { + server.route(createUpdateRulesBulkRoute(server)); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts index e22c8737413927..b1f61d11458fe3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts @@ -12,10 +12,13 @@ import { transformFindAlertsOrError, transformOrError, transformTags, + getIdBulkError, + transformOrBulkError, } from './utils'; import { getResult } from '../__mocks__/request_responses'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; import { OutputRuleAlertRest } from '../../types'; +import { BulkError } from '../utils'; describe('utils', () => { describe('transformAlertToRule', () => { @@ -738,4 +741,151 @@ describe('utils', () => { ]); }); }); + + describe('getIdBulkError', () => { + test('outputs message about id not being found if only id is defined and ruleId is undefined', () => { + const error = getIdBulkError({ id: '123', ruleId: undefined }); + const expected: BulkError = { + id: '123', + error: { message: 'id: "123" not found', statusCode: 404 }, + }; + expect(error).toEqual(expected); + }); + + test('outputs message about id not being found if only id is defined and ruleId is null', () => { + const error = getIdBulkError({ id: '123', ruleId: null }); + const expected: BulkError = { + id: '123', + error: { message: 'id: "123" not found', statusCode: 404 }, + }; + expect(error).toEqual(expected); + }); + + test('outputs message about ruleId not being found if only ruleId is defined and id is undefined', () => { + const error = getIdBulkError({ id: undefined, ruleId: 'rule-id-123' }); + const expected: BulkError = { + id: 'rule-id-123', + error: { message: 'rule_id: "rule-id-123" not found', statusCode: 404 }, + }; + expect(error).toEqual(expected); + }); + + test('outputs message about ruleId not being found if only ruleId is defined and id is null', () => { + const error = getIdBulkError({ id: null, ruleId: 'rule-id-123' }); + const expected: BulkError = { + id: 'rule-id-123', + error: { message: 'rule_id: "rule-id-123" not found', statusCode: 404 }, + }; + expect(error).toEqual(expected); + }); + + test('outputs message about both being not defined when both are undefined', () => { + const error = getIdBulkError({ id: undefined, ruleId: undefined }); + const expected: BulkError = { + id: '(unknown id)', + error: { message: 'id or rule_id should have been defined', statusCode: 404 }, + }; + expect(error).toEqual(expected); + }); + + test('outputs message about both being not defined when both are null', () => { + const error = getIdBulkError({ id: null, ruleId: null }); + const expected: BulkError = { + id: '(unknown id)', + error: { message: 'id or rule_id should have been defined', statusCode: 404 }, + }; + expect(error).toEqual(expected); + }); + + test('outputs message about both being not defined when id is null and ruleId is undefined', () => { + const error = getIdBulkError({ id: null, ruleId: undefined }); + const expected: BulkError = { + id: '(unknown id)', + error: { message: 'id or rule_id should have been defined', statusCode: 404 }, + }; + expect(error).toEqual(expected); + }); + + test('outputs message about both being not defined when id is undefined and ruleId is null', () => { + const error = getIdBulkError({ id: undefined, ruleId: null }); + const expected: BulkError = { + id: '(unknown id)', + error: { message: 'id or rule_id should have been defined', statusCode: 404 }, + }; + expect(error).toEqual(expected); + }); + }); + + describe('transformOrBulkError', () => { + test('outputs 200 if the data is of type siem alert', () => { + const output = transformOrBulkError('rule-1', getResult()); + const expected: OutputRuleAlertRest = { + created_by: 'elastic', + created_at: '2019-12-13T16:40:33.400Z', + updated_at: '2019-12-13T16:40:33.400Z', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + output_index: '.siem-signals', + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + rule_id: 'rule-1', + risk_score: 50, + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + threats: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + techniques: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + ], + filters: [ + { + query: { + match_phrase: { + 'host.name': 'some-host', + }, + }, + }, + ], + meta: { + someMeta: 'someField', + }, + saved_id: 'some-id', + timeline_id: 'some-timeline-id', + version: 1, + }; + expect(output).toEqual(expected); + }); + + test('returns 500 if the data is not of type siem alert', () => { + const output = transformOrBulkError('rule-1', { data: [{ random: 1 }] }); + expect(output).toEqual({ + id: 'rule-1', + error: { message: 'Internal error transforming', statusCode: 500 }, + }); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts index dad22c74398d2d..b9bf3f8a942fcb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts @@ -9,6 +9,7 @@ import { pickBy } from 'lodash/fp'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; import { RuleAlertType, isAlertType, isAlertTypes } from '../../rules/types'; import { OutputRuleAlertRest } from '../../types'; +import { createBulkErrorObject, BulkError } from '../utils'; export const getIdError = ({ id, @@ -26,6 +27,34 @@ export const getIdError = ({ } }; +export const getIdBulkError = ({ + id, + ruleId, +}: { + id: string | undefined | null; + ruleId: string | undefined | null; +}): BulkError => { + if (id != null) { + return createBulkErrorObject({ + ruleId: id, + statusCode: 404, + message: `id: "${id}" not found`, + }); + } else if (ruleId != null) { + return createBulkErrorObject({ + ruleId, + statusCode: 404, + message: `rule_id: "${ruleId}" not found`, + }); + } else { + return createBulkErrorObject({ + ruleId: '(unknown id)', + statusCode: 404, + message: `id or rule_id should have been defined`, + }); + } +}; + export const transformTags = (tags: string[]): string[] => { return tags.filter(tag => !tag.startsWith(INTERNAL_IDENTIFIER)); }; @@ -83,3 +112,18 @@ export const transformOrError = (alert: unknown): Partial | return new Boom('Internal error transforming', { statusCode: 500 }); } }; + +export const transformOrBulkError = ( + ruleId: string, + alert: unknown +): Partial | BulkError => { + if (isAlertType(alert)) { + return transformAlertToRule(alert); + } else { + return createBulkErrorObject({ + ruleId, + statusCode: 500, + message: 'Internal error transforming', + }); + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts new file mode 100644 index 00000000000000..17fb5320daa013 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createRulesBulkSchema } from './create_rules_bulk_schema'; +import { UpdateRuleAlertParamsRest } from '../../rules/types'; + +// only the basics of testing are here. +// see: create_rules_schema.test.ts for the bulk of the validation tests +// this just wraps createRulesSchema in an array +describe('create_rules_bulk_schema', () => { + test('can take an empty array and validate it', () => { + expect( + createRulesBulkSchema.validate>>([]).error + ).toBeFalsy(); + }); + + test('made up values do not validate', () => { + expect( + createRulesBulkSchema.validate<[{ madeUp: string }]>([ + { + madeUp: 'hi', + }, + ]).error + ).toBeTruthy(); + }); + + test('single array of [id] does validate', () => { + expect( + createRulesBulkSchema.validate>>([ + { + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + type: 'query', + query: 'some query', + index: ['index-1'], + interval: '5m', + }, + ]).error + ).toBeFalsy(); + }); + + test('two values of [id] does validate', () => { + expect( + createRulesBulkSchema.validate>>([ + { + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + type: 'query', + query: 'some query', + index: ['index-1'], + interval: '5m', + }, + { + rule_id: 'rule-2', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + type: 'query', + query: 'some query', + index: ['index-1'], + interval: '5m', + }, + ]).error + ).toBeFalsy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.ts new file mode 100644 index 00000000000000..bcc4475f2d9f09 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import Joi from 'joi'; + +import { createRulesSchema } from './create_rules_schema'; + +export const createRulesBulkSchema = Joi.array().items(createRulesSchema); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts new file mode 100644 index 00000000000000..6450da37699d82 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { queryRulesBulkSchema } from './query_rules_bulk_schema'; +import { UpdateRuleAlertParamsRest } from '../../rules/types'; + +// only the basics of testing are here. +// see: query_rules_bulk_schema.test.ts for the bulk of the validation tests +// this just wraps queryRulesSchema in an array +describe('query_rules_bulk_schema', () => { + test('can take an empty array and validate it', () => { + expect( + queryRulesBulkSchema.validate>>([]).error + ).toBeFalsy(); + }); + + test('both rule_id and id being supplied do not validate', () => { + expect( + queryRulesBulkSchema.validate>>([ + { + rule_id: '1', + id: '1', + }, + ]).error + ).toBeTruthy(); + }); + + test('both rule_id and id being supplied do not validate if one array element works but the second does not', () => { + expect( + queryRulesBulkSchema.validate>>([ + { + id: '1', + }, + { + rule_id: '1', + id: '1', + }, + ]).error + ).toBeTruthy(); + }); + + test('only id validates', () => { + expect( + queryRulesBulkSchema.validate>>([{ id: '1' }]).error + ).toBeFalsy(); + }); + + test('only id validates with two elements', () => { + expect( + queryRulesBulkSchema.validate>>([ + { id: '1' }, + { id: '2' }, + ]).error + ).toBeFalsy(); + }); + + test('only rule_id validates', () => { + expect( + queryRulesBulkSchema.validate>>([{ rule_id: '1' }]) + .error + ).toBeFalsy(); + }); + + test('only rule_id validates with two elements', () => { + expect( + queryRulesBulkSchema.validate>>([ + { rule_id: '1' }, + { rule_id: '2' }, + ]).error + ).toBeFalsy(); + }); + + test('both id and rule_id validates with two separate elements', () => { + expect( + queryRulesBulkSchema.validate>>([ + { id: '1' }, + { rule_id: '2' }, + ]).error + ).toBeFalsy(); + }); +}); diff --git a/x-pack/legacy/plugins/infra/public/apps/testing_app.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.ts similarity index 61% rename from x-pack/legacy/plugins/infra/public/apps/testing_app.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.ts index bcd7d0e5926441..13ccac282281df 100644 --- a/x-pack/legacy/plugins/infra/public/apps/testing_app.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.ts @@ -3,7 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import Joi from 'joi'; -import { compose } from '../lib/compose/testing_compose'; -import { startApp } from './start_app'; -startApp(compose()); +import { queryRulesSchema } from './query_rules_schema'; + +export const queryRulesBulkSchema = Joi.array().items(queryRulesSchema); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.test.ts index 4f0dbf10f4559e..5c293f4825b95b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.test.ts @@ -7,12 +7,15 @@ import { querySignalsSchema } from './query_signals_index_schema'; import { SignalsQueryRestParams } from '../../signals/types'; -describe('query and aggs on signals index', () => { - test('query and aggs simultaneously', () => { +describe('query, aggs, size, _source and track_total_hits on signals index', () => { + test('query, aggs, size, _source and track_total_hits simultaneously', () => { expect( querySignalsSchema.validate>({ query: {}, aggs: {}, + size: 1, + track_total_hits: true, + _source: ['field'], }).error ).toBeFalsy(); }); @@ -33,7 +36,31 @@ describe('query and aggs on signals index', () => { ).toBeFalsy(); }); - test('missing query and aggs is invalid', () => { + test('size only', () => { + expect( + querySignalsSchema.validate>({ + size: 1, + }).error + ).toBeFalsy(); + }); + + test('track_total_hits only', () => { + expect( + querySignalsSchema.validate>({ + track_total_hits: true, + }).error + ).toBeFalsy(); + }); + + test('_source only', () => { + expect( + querySignalsSchema.validate>({ + _source: ['field'], + }).error + ).toBeFalsy(); + }); + + test('missing query, aggs, size, _source and track_total_hits is invalid', () => { expect(querySignalsSchema.validate>({}).error).toBeTruthy(); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.ts index 53ce50692e84aa..0a6fceb44f845a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.ts @@ -9,4 +9,7 @@ import Joi from 'joi'; export const querySignalsSchema = Joi.object({ query: Joi.object(), aggs: Joi.object(), + size: Joi.number(), + track_total_hits: Joi.boolean(), + _source: Joi.array().items(Joi.string()), }).min(1); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts new file mode 100644 index 00000000000000..2b1bad39eb6861 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { updateRulesBulkSchema } from './update_rules_bulk_schema'; +import { UpdateRuleAlertParamsRest } from '../../rules/types'; + +// only the basics of testing are here. +// see: update_rules_schema.test.ts for the bulk of the validation tests +// this just wraps updateRulesSchema in an array +describe('update_rules_bulk_schema', () => { + test('can take an empty array and validate it', () => { + expect( + updateRulesBulkSchema.validate>>([]).error + ).toBeFalsy(); + }); + + test('made up values do not validate', () => { + expect( + updateRulesBulkSchema.validate<[{ madeUp: string }]>([ + { + madeUp: 'hi', + }, + ]).error + ).toBeTruthy(); + }); + + test('single array of [id] does validate', () => { + expect( + updateRulesBulkSchema.validate>>([ + { + id: 'rule-1', + }, + ]).error + ).toBeFalsy(); + }); + + test('two values of [id] does validate', () => { + expect( + updateRulesBulkSchema.validate>>([ + { + id: 'rule-1', + }, + { + id: 'rule-2', + }, + ]).error + ).toBeFalsy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.ts new file mode 100644 index 00000000000000..123ec2d5b7e15c --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import Joi from 'joi'; + +import { updateRulesSchema } from './update_rules_schema'; + +export const updateRulesBulkSchema = Joi.array().items(updateRulesSchema); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts index 89ffed259cf77a..6d1896b1a81710 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts @@ -25,14 +25,14 @@ export const querySignalsRouteDef = (server: ServerFacade): Hapi.ServerRoute => }, }, async handler(request: SignalsQueryRequest) { - const { query, aggs } = request.payload; - const body = { query, aggs }; + const { query, aggs, _source, track_total_hits, size } = request.payload; const index = getIndex(request, server); const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); + try { return callWithRequest(request, 'search', { index, - body, + body: { query, aggs, _source, track_total_hits, size }, }); } catch (exc) { // error while getting or updating signal with id: id in signal index .siem-signals diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts index 437c0a43624307..fa95c77f646d61 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; -import { transformError } from './utils'; +import { transformError, transformBulkError, BulkError } from './utils'; describe('utils', () => { describe('transformError', () => { @@ -57,4 +57,53 @@ describe('utils', () => { expect(transformed.output.statusCode).toBe(400); }); }); + + describe('transformBulkError', () => { + test('returns transformed object if it is a boom object', () => { + const boom = new Boom('some boom message', { statusCode: 400 }); + const transformed = transformBulkError('rule-1', boom); + const expected: BulkError = { + id: 'rule-1', + error: { message: 'some boom message', statusCode: 400 }, + }; + expect(transformed).toEqual(expected); + }); + + test('returns a normal error if it is some non boom object that has a statusCode', () => { + const error: Error & { statusCode?: number } = { + statusCode: 403, + name: 'some name', + message: 'some message', + }; + const transformed = transformBulkError('rule-1', error); + const expected: BulkError = { + id: 'rule-1', + error: { message: 'some message', statusCode: 403 }, + }; + expect(transformed).toEqual(expected); + }); + + test('returns a 500 if the status code is not set', () => { + const error: Error & { statusCode?: number } = { + name: 'some name', + message: 'some message', + }; + const transformed = transformBulkError('rule-1', error); + const expected: BulkError = { + id: 'rule-1', + error: { message: 'some message', statusCode: 500 }, + }; + expect(transformed).toEqual(expected); + }); + + test('it detects a TypeError and returns a Boom status of 400', () => { + const error: TypeError = new TypeError('I have a type error'); + const transformed = transformBulkError('rule-1', error); + const expected: BulkError = { + id: 'rule-1', + error: { message: 'I have a type error', statusCode: 400 }, + }; + expect(transformed).toEqual(expected); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index 62281c7ebaacb0..d9a8efd6738837 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -26,6 +26,56 @@ export const transformError = (err: Error & { statusCode?: number }) => { } }; +export interface BulkError { + id: string; + error: { + statusCode: number; + message: string; + }; +} +export const createBulkErrorObject = ({ + ruleId, + statusCode, + message, +}: { + ruleId: string; + statusCode: number; + message: string; +}): BulkError => { + return { + id: ruleId, + error: { + statusCode, + message, + }, + }; +}; + +export const transformBulkError = ( + ruleId: string, + err: Error & { statusCode?: number } +): BulkError => { + if (Boom.isBoom(err)) { + return createBulkErrorObject({ + ruleId, + statusCode: err.output.statusCode, + message: err.message, + }); + } else if (err instanceof TypeError) { + return createBulkErrorObject({ + ruleId, + statusCode: 400, + message: err.message, + }); + } else { + return createBulkErrorObject({ + ruleId, + statusCode: err.statusCode ?? 500, + message: err.message, + }); + } +}; + export const getIndex = (request: RequestFacade, server: ServerFacade): string => { const spaceId = server.plugins.spaces.getSpaceId(request); const signalsIndex = server.config().get(`xpack.${APP_ID}.${SIGNALS_INDEX_KEY}`); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_adding_the_hidden_file_attribute_with_via_attribexe.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_adding_the_hidden_file_attribute_with_via_attribexe.json new file mode 100644 index 00000000000000..7bddffb4734ef0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_adding_the_hidden_file_attribute_with_via_attribexe.json @@ -0,0 +1,17 @@ +{ + "rule_id": "4630d948-40d4-4cef-ac69-4002e29bc3db", + "risk_score": 50, + "description": "EQL - Adding the Hidden File Attribute with via attrib.exe", + "immutable": true, + "interval": "5m", + "name": "EQL - Adding the Hidden File Attribute with via attrib.exe", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": " event.action:\"Process Create (rule: ProcessCreate)\" and process.name:\"attrib.exe\" and process.args:\"+h\"", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_adobe_hijack_persistence.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_adobe_hijack_persistence.json new file mode 100644 index 00000000000000..d57e5c7709b246 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_adobe_hijack_persistence.json @@ -0,0 +1,17 @@ +{ + "rule_id": "2bf78aa2-9c56-48de-b139-f169bf99cf86", + "risk_score": 50, + "description": "EQL - Adobe Hijack Persistence", + "immutable": true, + "interval": "5m", + "name": "EQL - Adobe Hijack Persistence", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "file.path:(\"C:\\Program Files (x86)\\Adobe\\Acrobat Reader DC\\Reader\\AcroCEF\\RdrCEF.exe\" or \"C:\\Program Files\\Adobe\\Acrobat Reader DC\\Reader\\AcroCEF\\RdrCEF.exe\") and event.action:\"File created (rule: FileCreate)\" and not process.name:msiexeec.exe", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_audio_capture_via_powershell.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_audio_capture_via_powershell.json new file mode 100644 index 00000000000000..da3cf0fb460259 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_audio_capture_via_powershell.json @@ -0,0 +1,17 @@ +{ + "rule_id": "b27b9f47-0a20-4807-8377-7f899b4fbada", + "risk_score": 50, + "description": "EQL - Audio Capture via PowerShell", + "immutable": true, + "interval": "5m", + "name": "EQL - Audio Capture via PowerShell", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.action:\"Process Create (rule: ProcessCreate)\" and process.name:\"SoundRecorder.exe\" and process.args:\"/FILE\"", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_audio_capture_via_soundrecorder.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_audio_capture_via_soundrecorder.json new file mode 100644 index 00000000000000..cc0091feb290d8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_audio_capture_via_soundrecorder.json @@ -0,0 +1,17 @@ +{ + "rule_id": "f8e06892-ed10-4452-892e-2c5a38d552f1", + "risk_score": 50, + "description": "EQL - Audio Capture via SoundRecorder", + "immutable": true, + "interval": "5m", + "name": "EQL - Audio Capture via SoundRecorder", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.action:\"Process Create (rule: ProcessCreate)\" and process.name:\"SoundRecorder.exe\" and process.args:\"/FILE\"", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_bypass_uac_event_viewer.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_bypass_uac_event_viewer.json new file mode 100644 index 00000000000000..bdc85045009cb6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_bypass_uac_event_viewer.json @@ -0,0 +1,17 @@ +{ + "rule_id": "59547add-a400-4baa-aa0c-66c72efdb77f", + "risk_score": 50, + "description": "EQL -Bypass UAC Event Viewer", + "immutable": true, + "interval": "5m", + "name": "EQL -Bypass UAC Event Viewer", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "process.parent.name:eventvwr.exe and event.action:\"Process Create (rule: ProcessCreate)\" and not process.executable:(\"C:\\Windows\\System32\\mmc.exe\" or \"C:\\Windows\\SysWOW64\\mmc.exe\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_bypass_uac_via_cmstp.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_bypass_uac_via_cmstp.json new file mode 100644 index 00000000000000..c3b28e6dce849e --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_bypass_uac_via_cmstp.json @@ -0,0 +1,17 @@ +{ + "rule_id": "2f7403da-1a4c-46bb-8ecc-c1a596e10cd0", + "risk_score": 50, + "description": "EQL - Bypass UAC via CMSTP", + "immutable": true, + "interval": "5m", + "name": "EQL - Bypass UAC via CMSTP", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.action:\"Process Create (rule: ProcessCreate)\" and process.parent.name:\"cmstp.exe\" and process.parent.args:(\"/s\" and \"/au\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_bypass_uac_via_sdclt.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_bypass_uac_via_sdclt.json new file mode 100644 index 00000000000000..d79c551ffb9cba --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_bypass_uac_via_sdclt.json @@ -0,0 +1,17 @@ +{ + "rule_id": "f68d83a1-24cb-4b8d-825b-e8af400b9670", + "risk_score": 50, + "description": "EQL -Bypass UAC Via sdclt", + "immutable": true, + "interval": "5m", + "name": "EQL -Bypass UAC Via sdclt", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": " event.action:\"Process Create (rule: ProcessCreate)\" and process.name:\"sdclt.exe\" and process.args:\"/kickoffelev\" and not process.executable:(\"C:\\Windows\\System32\\sdclt.exe\" or \"C:\\Windows\\System32\\control.exe\" or \"C:\\Windows\\SysWOW64\\sdclt.exe\" or \"C:\\Windows\\SysWOW64\\control.exe\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_clearing_windows_event_logs.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_clearing_windows_event_logs.json new file mode 100644 index 00000000000000..d7eb663297a637 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_clearing_windows_event_logs.json @@ -0,0 +1,17 @@ +{ + "rule_id": "d331bbe2-6db4-4941-80a5-8270db72eb61", + "risk_score": 50, + "description": "EQL - Clearing Windows Event Logs", + "immutable": true, + "interval": "5m", + "name": "EQL - Clearing Windows Event Logs", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.action:\"Process Create (rule: ProcessCreate)\" and (process.name:\"wevtutil.exe\" and process.args:\"cl\") or (process.name:\"powershell.exe\" and process.args:\"Clear-EventLog\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_delete_volume_usn_journal_with_fsutil.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_delete_volume_usn_journal_with_fsutil.json new file mode 100644 index 00000000000000..2155c2fa12913a --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_delete_volume_usn_journal_with_fsutil.json @@ -0,0 +1,17 @@ +{ + "rule_id": "f675872f-6d85-40a3-b502-c0d2ef101e92", + "risk_score": 50, + "description": "EQL - Delete Volume USN Journal with fsutil", + "immutable": true, + "interval": "5m", + "name": "EQL - Delete Volume USN Journal with fsutil", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.action:\"Process Create (rule: ProcessCreate)\" and process.name:\"fsutil.exe\" and process.args:(\"usn\" and \"deletejournal\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_deleting_backup_catalogs_with_wbadmin.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_deleting_backup_catalogs_with_wbadmin.json new file mode 100644 index 00000000000000..4bf7ae5ee1a5a2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_deleting_backup_catalogs_with_wbadmin.json @@ -0,0 +1,17 @@ +{ + "rule_id": "581add16-df76-42bb-af8e-c979bfb39a59", + "risk_score": 50, + "description": "EQL - Deleting Backup Catalogs with wbadmin", + "immutable": true, + "interval": "5m", + "name": "EQL - Deleting Backup Catalogs with wbadmin", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.action:\"Process Create (rule: ProcessCreate)\" and process.name:\"wbadmin.exe\" and process.args:(\"delete\" and \"catalog\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_direct_outbound_smb_connection.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_direct_outbound_smb_connection.json new file mode 100644 index 00000000000000..8a7733d069154f --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_direct_outbound_smb_connection.json @@ -0,0 +1,17 @@ +{ + "rule_id": "c82c7d8f-fb9e-4874-a4bd-fd9e3f9becf1", + "risk_score": 50, + "description": "EQL - Direct Outbound SMB Connection", + "immutable": true, + "interval": "5m", + "name": "EQL - Direct Outbound SMB Connection", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": " event.action:\"Network connection detected (rule: NetworkConnect)\" and destination.port:445 and not process.pid:4 and not destination.ip:(\"127.0.0.1\" or \"::1\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_disable_windows_firewall_rules_with_netsh.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_disable_windows_firewall_rules_with_netsh.json new file mode 100644 index 00000000000000..2ed22ed4e59a05 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_disable_windows_firewall_rules_with_netsh.json @@ -0,0 +1,17 @@ +{ + "rule_id": "4b438734-3793-4fda-bd42-ceeada0be8f9", + "risk_score": 50, + "description": "EQL - Disable Windows Firewall Rules with Netsh", + "immutable": true, + "interval": "5m", + "name": "EQL - Disable Windows Firewall Rules with Netsh", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.action:\"Process Create (rule: ProcessCreate)\" and process.name:\"netsh.exe\" and process.args:(\"firewall\" and \"set\" and \"disable\") or process.args:(\"advfirewall\" and \"state\" and \"off\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_dll_search_order_hijack.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_dll_search_order_hijack.json new file mode 100644 index 00000000000000..e59286339290af --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_dll_search_order_hijack.json @@ -0,0 +1,17 @@ +{ + "rule_id": "73fbc44c-c3cd-48a8-a473-f4eb2065c716", + "risk_score": 50, + "description": "EQL - DLL Search Order Hijack", + "immutable": true, + "interval": "5m", + "name": "EQL - DLL Search Order Hijack", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": " event.action:\"File created (rule: FileCreate)\" and not winlog.user.identifier:(\"S-1-5-18\" or \"S-1-5-19\" or \"S-1-5-20\") and file.path:(\"C\\Windows\\ehome\\cryptbase.dll\" or \"C\\Windows\\System32\\Sysprep\\cryptbase.dll\" or \"C\\Windows\\System32\\Sysprep\\cryptsp.dll\" or \"C\\Windows\\System32\\Sysprep\\rpcrtremote.dll\" or \"C\\Windows\\System32\\Sysprep\\uxtheme.dll\" or \"C\\Windows\\System32\\Sysprep\\dwmapi.dll\" or \"C\\Windows\\System32\\Sysprep\\shcore.dll\" or \"C\\Windows\\System32\\Sysprep\\oleacc.dll\" or \"C\\Windows\\System32\\ntwdblib.dll\") ", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_encoding_or_decoding_files_via_certutil.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_encoding_or_decoding_files_via_certutil.json new file mode 100644 index 00000000000000..2ad0a53b6c9b4d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_encoding_or_decoding_files_via_certutil.json @@ -0,0 +1,17 @@ +{ + "rule_id": "fd70c98a-c410-42dc-a2e3-761c71848acf", + "risk_score": 50, + "description": "EQL - Encoding or Decoding Files via CertUtil", + "immutable": true, + "interval": "5m", + "name": "EQL - Encoding or Decoding Files via CertUtil", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.action:\"Process Create (rule: ProcessCreate)\" and process.name:\"certutil.exe\" and process.args:(\"-encode\" or \"/encode\" or \"-decode\" or \"/decode\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_local_scheduled_task_commands.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_local_scheduled_task_commands.json new file mode 100644 index 00000000000000..bb005643031bd1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_local_scheduled_task_commands.json @@ -0,0 +1,17 @@ +{ + "rule_id": "afcce5ad-65de-4ed2-8516-5e093d3ac99a", + "risk_score": 50, + "description": "EQL - Local Scheduled Task Commands", + "immutable": true, + "interval": "5m", + "name": "EQL - Local Scheduled Task Commands", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": " event.action:\"Process Create (rule: ProcessCreate)\" and process.name:schtasks.exe and process.args:(\"/create\" or \"-create\" or \"/S\" or \"-s\" or \"/run\" or \"-run\" or \"/change\" or \"-change\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_local_service_commands.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_local_service_commands.json new file mode 100644 index 00000000000000..1254d0971f1084 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_local_service_commands.json @@ -0,0 +1,17 @@ +{ + "rule_id": "e8571d5f-bea1-46c2-9f56-998de2d3ed95", + "risk_score": 50, + "description": "EQL - Local Service Commands", + "immutable": true, + "interval": "5m", + "name": "EQL - Local Service Commands", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.action:\"Process Create (rule: ProcessCreate)\" and process.name:sc.exe and process.args:(\"create\" or \"config\" or \"failure\" or \"start\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_modification_of_boot_configuration.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_modification_of_boot_configuration.json new file mode 100644 index 00000000000000..62b07f1f4ed378 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_modification_of_boot_configuration.json @@ -0,0 +1,17 @@ +{ + "rule_id": "b9ab2f7f-f719-4417-9599-e0252fffe2d8", + "risk_score": 50, + "description": "EQL - Modification of Boot Configuration", + "immutable": true, + "interval": "5m", + "name": "EQL - Modification of Boot Configuration", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.action:\"Process Create (rule: ProcessCreate)\" and process.name:\"bcdedit.exe\" and process.args:\"set\" and process.args:( (\"bootstatuspolicy\" and \"ignoreallfailures\") or (\"recoveryenabled\" and \"no\") ) ", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_msbuild_making_network_connections.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_msbuild_making_network_connections.json new file mode 100644 index 00000000000000..a3c0a8c0960efe --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_msbuild_making_network_connections.json @@ -0,0 +1,17 @@ +{ + "rule_id": "0e79980b-4250-4a50-a509-69294c14e84b", + "risk_score": 50, + "description": "EQL - MsBuild Making Network Connections", + "immutable": true, + "interval": "5m", + "name": "EQL - MsBuild Making Network Connections", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": " event.action:\"Network connection detected (rule: NetworkConnect)\" and process.name:msbuild.exe and not destination.ip:(\"127.0.0.1\" or \"::1\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_mshta_making_network_connections.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_mshta_making_network_connections.json new file mode 100644 index 00000000000000..2d5e73c50a73c7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_mshta_making_network_connections.json @@ -0,0 +1,17 @@ +{ + "rule_id": "a4ec1382-4557-452b-89ba-e413b22ed4b8", + "risk_score": 50, + "description": "EQL - Mshta Making Network Connections", + "immutable": true, + "interval": "5m", + "name": "EQL - Mshta Making Network Connections", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": " event.action:\"Network connection detected (rule: NetworkConnect)\" and process.name:\"mshta.exe\" and not process.name:\"mshta.exe\" and not parent.process.name:\"Microsoft.ConfigurationManagement.exe\"", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_msxsl_making_network_connections.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_msxsl_making_network_connections.json new file mode 100644 index 00000000000000..04c88def26d61d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_msxsl_making_network_connections.json @@ -0,0 +1,17 @@ +{ + "rule_id": "d7351b03-135d-43ba-8b36-cc9b07854525", + "risk_score": 50, + "description": "EQL - MsXsl Making Network Connections", + "immutable": true, + "interval": "5m", + "name": "EQL - MsXsl Making Network Connections", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "process.name:msxml.exe and event.action:\"Network connection detected (rule: NetworkConnect)\" and not destination.ip:10.0.0.0/8 and not destination.ip:172.16.0.0/12 and not destination.ip:192.168.0.0/16", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_psexec_lateral_movement_command.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_psexec_lateral_movement_command.json new file mode 100644 index 00000000000000..fe87c83c0403c4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_psexec_lateral_movement_command.json @@ -0,0 +1,17 @@ +{ + "rule_id": "55d551c6-333b-4665-ab7e-5d14a59715ce", + "risk_score": 50, + "description": "EQL - PsExec Lateral Movement Command", + "immutable": true, + "interval": "5m", + "name": "EQL - PsExec Lateral Movement Command", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "process.name:psexec.exe and event.action:\"Network connection detected (rule: NetworkConnect)\" ", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_suspicious_ms_office_child_process.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_suspicious_ms_office_child_process.json new file mode 100644 index 00000000000000..41deb57145abcd --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_suspicious_ms_office_child_process.json @@ -0,0 +1,17 @@ +{ + "rule_id": "a624863f-a70d-417f-a7d2-7a404638d47f", + "risk_score": 50, + "description": "EQL - Suspicious MS Office Child Process", + "immutable": true, + "interval": "5m", + "name": "EQL - Suspicious MS Office Child Process", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": " event.action:\"Process Create (rule: ProcessCreate)\" and process.parent.name:(\"winword.exe\" or \"excel.exe\" or \"powerpnt.exe\" or \"eqnedt32.exe\" or \"fltldr.exe\" or \"mspub.exe\" or \"msaccess.exe\") and process.name:(\"arp.exe\" or \"dsquery.exe\" or \"dsget.exe\" or \"gpresult.exe\" or \"hostname.exe\" or \"ipconfig.exe\" or \"nbtstat.exe\" or \"net.exe\" or \"net1.exe\" or \"netsh.exe\" or \"netstat.exe\" or \"nltest.exe\" or \"ping.exe\" or \"qprocess.exe\" or \"quser.exe\" or \"qwinsta.exe\" or \"reg.exe\" or \"sc.exe\" or \"systeminfo.exe\" or \"tasklist.exe\" or \"tracert.exe\" or \"whoami.exe\" or \"bginfo.exe\" or \"cdb.exe\" or \"cmstp.exe\" or \"csi.exe\" or \"dnx.exe\" or \"fsi.exe\" or \"ieexec.exe\" or \"iexpress.exe\" or \"installutil.exe\" or \"Microsoft.Workflow.Compiler.exe\" or \"msbuild.exe\" or \"mshta.exe\" or \"msxsl.exe\" or \"odbcconf.exe\" or \"rcsi.exe\" or \"regsvr32.exe\" or \"xwizard.exe\" or \"atbroker.exe\" or \"forfiles.exe\" or \"schtasks.exe\" or \"regasm.exe\" or \"regsvcs.exe\" or \"cmd.exe\" or \"cscript.exe\" or \"powershell.exe\" or \"pwsh.exe\" or \"wmic.exe\" or \"wscript.exe\" or \"bitsadmin.exe\" or \"certutil.exe\" or \"ftp.exe\") ", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_suspicious_ms_outlook_child_process.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_suspicious_ms_outlook_child_process.json new file mode 100644 index 00000000000000..bbcc987c3b6aec --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_suspicious_ms_outlook_child_process.json @@ -0,0 +1,17 @@ +{ + "rule_id": "32f4675e-6c49-4ace-80f9-97c9259dca2e", + "risk_score": 50, + "description": "EQL - Suspicious MS Outlook Child Process", + "immutable": true, + "interval": "5m", + "name": "EQL - Suspicious MS Outlook Child Process", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": " event.action:\"Process Create (rule: ProcessCreate)\" and process.parent.name:\"outlook.exe\" and process.name:(\"arp.exe\" or \"dsquery.exe\" or \"dsget.exe\" or \"gpresult.exe\" or \"hostname.exe\" or \"ipconfig.exe\" or \"nbtstat.exe\" or \"net.exe\" or \"net1.exe\" or \"netsh.exe\" or \"netstat.exe\" or \"nltest.exe\" or \"ping.exe\" or \"qprocess.exe\" or \"quser.exe\" or \"qwinsta.exe\" or \"reg.exe\" or \"sc.exe\" or \"systeminfo.exe\" or \"tasklist.exe\" or \"tracert.exe\" or \"whoami.exe\" or \"bginfo.exe\" or \"cdb.exe\" or \"cmstp.exe\" or \"csi.exe\" or \"dnx.exe\" or \"fsi.exe\" or \"ieexec.exe\" or \"iexpress.exe\" or \"installutil.exe\" or \"Microsoft.Workflow.Compiler.exe\" or \"msbuild.exe\" or \"mshta.exe\" or \"msxsl.exe\" or \"odbcconf.exe\" or \"rcsi.exe\" or \"regsvr32.exe\" or \"xwizard.exe\" or \"atbroker.exe\" or \"forfiles.exe\" or \"schtasks.exe\" or \"regasm.exe\" or \"regsvcs.exe\" or \"cmd.exe\" or \"cscript.exe\" or \"powershell.exe\" or \"pwsh.exe\" or \"wmic.exe\" or \"wscript.exe\" or \"bitsadmin.exe\" or \"certutil.exe\" or \"ftp.exe\") ", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_suspicious_pdf_reader_child_process.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_suspicious_pdf_reader_child_process.json new file mode 100644 index 00000000000000..488dc04a3b02e5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_suspicious_pdf_reader_child_process.json @@ -0,0 +1,17 @@ +{ + "rule_id": "afcac7b1-d092-43ff-a136-aa7accbda38f", + "risk_score": 50, + "description": "EQL - Suspicious PDF Reader Child Process", + "immutable": true, + "interval": "5m", + "name": "EQL - Suspicious PDF Reader Child Process", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": " event.action:\"Process Create (rule: ProcessCreate)\" and process.parent.name:(\"acrord32.exe\" or \"rdrcef.exe\" or \"foxitphantomPDF.exe\" or \"foxitreader.exe\") and process.name:(\"arp.exe\" or \"dsquery.exe\" or \"dsget.exe\" or \"gpresult.exe\" or \"hostname.exe\" or \"ipconfig.exe\" or \"nbtstat.exe\" or \"net.exe\" or \"net1.exe\" or \"netsh.exe\" or \"netstat.exe\" or \"nltest.exe\" or \"ping.exe\" or \"qprocess.exe\" or \"quser.exe\" or \"qwinsta.exe\" or \"reg.exe\" or \"sc.exe\" or \"systeminfo.exe\" or \"tasklist.exe\" or \"tracert.exe\" or \"whoami.exe\" or \"bginfo.exe\" or \"cdb.exe\" or \"cmstp.exe\" or \"csi.exe\" or \"dnx.exe\" or \"fsi.exe\" or \"ieexec.exe\" or \"iexpress.exe\" or \"installutil.exe\" or \"Microsoft.Workflow.Compiler.exe\" or \"msbuild.exe\" or \"mshta.exe\" or \"msxsl.exe\" or \"odbcconf.exe\" or \"rcsi.exe\" or \"regsvr32.exe\" or \"xwizard.exe\" or \"atbroker.exe\" or \"forfiles.exe\" or \"schtasks.exe\" or \"regasm.exe\" or \"regsvcs.exe\" or \"cmd.exe\" or \"cscript.exe\" or \"powershell.exe\" or \"pwsh.exe\" or \"wmic.exe\" or \"wscript.exe\" or \"bitsadmin.exe\" or \"certutil.exe\" or \"ftp.exe\") ", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_system_shells_via_services.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_system_shells_via_services.json new file mode 100644 index 00000000000000..810aa79ce25af1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_system_shells_via_services.json @@ -0,0 +1,17 @@ +{ + "rule_id": "0022d47d-39c7-4f69-a232-4fe9dc7a3acd", + "risk_score": 50, + "description": "EQL - System Shells via Services", + "immutable": true, + "interval": "5m", + "name": "EQL - System Shells via Services", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": " event.action:\"Process Create (rule: ProcessCreate)\" and process.parent.name:\"services.exe\" and process.name:(\"cmd.exe\" or \"powershell.exe\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_unusual_network_connection_via_rundll32.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_unusual_network_connection_via_rundll32.json new file mode 100644 index 00000000000000..6918d996256c03 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_unusual_network_connection_via_rundll32.json @@ -0,0 +1,17 @@ +{ + "rule_id": "52aaab7b-b51c-441a-89ce-4387b3aea886", + "risk_score": 50, + "description": "EQL - Unusual Network Connection via RunDLL32", + "immutable": true, + "interval": "5m", + "name": "EQL - Unusual Network Connection via RunDLL32", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "process.name:rundll32.exe and event.action:\"Network connection detected (rule: NetworkConnect)\" and not destination.ip:10.0.0.0/8 and not destination.ip:172.16.0.0/12 and not destination.ip:192.168.0.0/16", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_unusual_parentchild_relationship.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_unusual_parentchild_relationship.json new file mode 100644 index 00000000000000..007487ec91eed1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_unusual_parentchild_relationship.json @@ -0,0 +1,17 @@ +{ + "rule_id": "35df0dd8-092d-4a83-88c1-5151a804f31b", + "risk_score": 50, + "description": "EQL - Unusual Parent-Child Relationship ", + "immutable": true, + "interval": "5m", + "name": "EQL - Unusual Parent-Child Relationship ", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": " event.action:\"Process Create (rule: ProcessCreate)\" and process.parent.executable:* and ( (process.name:\"smss.exe\" and not process.parent.name:(\"System\" or \"smss.exe\")) or (process.name:\"csrss.exe\" and not process.parent.name:(\"smss.exe\" or \"svchost.exe\")) or (process.name:\"wininit.exe\" and not process.parent.name:\"smss.exe\") or (process.name:\"winlogon.exe\" and not process.parent.name:\"smss.exe\") or (process.name:\"lsass.exe\" and not process.parent.name:\"wininit.exe\") or (process.name:\"LogonUI.exe\" and not process.parent.name:(\"winlogon.exe\" or \"wininit.exe\")) or (process.name:\"services.exe\" and not process.parent.name:\"wininit.exe\") or (process.name:\"svchost.exe\" and not process.parent.name:(\"services.exe\" or \"MsMpEng.exe\")) or (process.name:\"spoolsv.exe\" and not process.parent.name:\"services.exe\") or (process.name:\"taskhost.exe\" and not process.parent.name:(\"services.exe\" or \"svchost.exe\")) or (process.name:\"taskhostw.exe\" and not process.parent.name:(\"services.exe\" or \"svchost.exe\")) or (process.name:\"userinit.exe\" and not process.parent.name:(\"dwm.exe\" or \"winlogon.exe\")) )", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_unusual_process_network_connection.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_unusual_process_network_connection.json new file mode 100644 index 00000000000000..7aabc9ed604161 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_unusual_process_network_connection.json @@ -0,0 +1,17 @@ +{ + "rule_id": "610949a1-312f-4e04-bb55-3a79b8c95267", + "risk_score": 50, + "description": "EQL - Unusual Process Network Connection", + "immutable": true, + "interval": "5m", + "name": "EQL - Unusual Process Network Connection", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": " event.action:\"Network connection detected (rule: NetworkConnect)\" and process.name:(bginfo.exe or cdb.exe or cmstp.exe or csi.exe or dnx.exe or fsi.exe or ieexec.exe or iexpress.exe or Microsoft.Workflow.Compiler.exe or odbcconf.exe or rcsi.exe or xwizard.exe)", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_user_account_creation.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_user_account_creation.json new file mode 100644 index 00000000000000..cbe1b7fb7af4f9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_user_account_creation.json @@ -0,0 +1,17 @@ +{ + "rule_id": "1aa9181a-492b-4c01-8b16-fa0735786b2b", + "risk_score": 50, + "description": "EQL - User Account Creation", + "immutable": true, + "interval": "5m", + "name": "EQL - User Account Creation", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": " event.action:\"Process Create (rule: ProcessCreate)\" and process.name:(\"net.exe\" or \"net1.exe\") and not process.parent.name:\"net.exe\" and process.args:(\"user\" and (\"/add\" or \"/ad\")) ", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_user_added_to_administrator_group.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_user_added_to_administrator_group.json new file mode 100644 index 00000000000000..ed8fa5276ef343 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_user_added_to_administrator_group.json @@ -0,0 +1,17 @@ +{ + "rule_id": "4426de6f-6103-44aa-a77e-49d672836c27", + "risk_score": 50, + "description": "EQL - User Added to Administrator Group", + "immutable": true, + "interval": "5m", + "name": "EQL - User Added to Administrator Group", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": " event.action:\"Process Create (rule: ProcessCreate)\" and process.name:(\"net.exe\" or \"net1.exe\") and not process.parent.name:\"net.exe\" and process.args:(\"group\" and \"admin\" and \"/add\") ", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_volume_shadow_copy_deletion_via_vssadmin.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_volume_shadow_copy_deletion_via_vssadmin.json new file mode 100644 index 00000000000000..186c688d21d8fc --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_volume_shadow_copy_deletion_via_vssadmin.json @@ -0,0 +1,17 @@ +{ + "rule_id": "b5ea4bfe-a1b2-421f-9d47-22a75a6f2921", + "risk_score": 50, + "description": "EQL - Volume Shadow Copy Deletion via VssAdmin", + "immutable": true, + "interval": "5m", + "name": "EQL - Volume Shadow Copy Deletion via VssAdmin", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": " event.action:\"Process Create (rule: ProcessCreate)\" and process.name:\"vssadmin.exe\" and process.args:(\"delete\" and \"shadows\") ", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_volume_shadow_copy_deletion_via_wmic.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_volume_shadow_copy_deletion_via_wmic.json new file mode 100644 index 00000000000000..9f75cb3ab26a86 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_volume_shadow_copy_deletion_via_wmic.json @@ -0,0 +1,17 @@ +{ + "rule_id": "dc9c1f74-dac3-48e3-b47f-eb79db358f57", + "risk_score": 50, + "description": "EQL - Volume Shadow Copy Deletion via WMIC", + "immutable": true, + "interval": "5m", + "name": "EQL - Volume Shadow Copy Deletion via WMIC", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": " event.action:\"Process Create (rule: ProcessCreate)\" and process.name:\"wmic.exe\" and process.args:(\"shadowcopy\" and \"delete\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_windows_script_executing_powershell.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_windows_script_executing_powershell.json new file mode 100644 index 00000000000000..034651d94d0ea8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_windows_script_executing_powershell.json @@ -0,0 +1,17 @@ +{ + "rule_id": "f545ff26-3c94-4fd0-bd33-3c7f95a3a0fc", + "risk_score": 50, + "description": "EQL - Windows Script Executing PowerShell", + "immutable": true, + "interval": "5m", + "name": "EQL - Windows Script Executing PowerShell", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.action:\"Process Create (rule: ProcessCreate)\" and process.parent.name:(\"wscript.exe\" or \"cscript.exe\") and process.name:\"powershell.exe\"", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_wmic_command_lateral_movement.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_wmic_command_lateral_movement.json new file mode 100644 index 00000000000000..eb1f3f4dca08e5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/eql_wmic_command_lateral_movement.json @@ -0,0 +1,17 @@ +{ + "rule_id": "9616587f-6396-42d0-bd31-ef8dbd806210", + "risk_score": 50, + "description": "EQL - WMIC Command Lateral Movement", + "immutable": true, + "interval": "5m", + "name": "EQL - WMIC Command Lateral Movement", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": " event.action:\"Process Create (rule: ProcessCreate)\" and process.name:\"wmic.exe\" and process.args:(\"/node\" or \"-node\") and process.args:(\"call\" or \"set\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/index.ts index 406432bcbda001..69fc9a26e921a7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/index.ts @@ -7,147 +7,223 @@ // Auto generated file from scripts/convert_saved_search_rules.js // Do not hand edit. Run the script against a set of saved searches instead -import rule1 from './windows_powershell_connecting_to_the_internet.json'; -import rule2 from './windows_net_user_command_activity.json'; -import rule3 from './windows_image_load_from_a_temp_directory.json'; -import rule4 from './network_ssh_secure_shell_to_the_internet.json'; -import rule5 from './suricata_nonhttp_traffic_on_tcp_port_80.json'; -import rule6 from './windows_misc_lolbin_connecting_to_the_internet.json'; -import rule7 from './linux_strace_activity.json'; -import rule8 from './suricata_directory_reversal_characters_in_an_http_request.json'; -import rule9 from './suricata_dns_traffic_on_unusual_udp_port.json'; -import rule10 from './network_telnet_port_activity.json'; -import rule11 from './suricata_directory_traversal_in_downloaded_zip_file.json'; -import rule12 from './windows_execution_via_microsoft_html_application_hta.json'; -import rule13 from './windows_credential_dumping_commands.json'; -import rule14 from './windows_net_command_activity_by_the_system_account.json'; -import rule15 from './windows_register_server_program_connecting_to_the_internet.json'; -import rule16 from './linux_java_process_connecting_to_the_internet.json'; -import rule17 from './suricata_imap_traffic_on_unusual_port_internet_destination.json'; -import rule18 from './suricata_double_encoded_characters_in_a_uri.json'; -import rule19 from './network_tor_activity_to_the_internet.json'; -import rule20 from './windows_registry_query_local.json'; -import rule21 from './linux_netcat_network_connection.json'; -import rule22 from './windows_defense_evasion_via_filter_manager.json'; -import rule23 from './suricata_nondns_traffic_on_udp_port_53.json'; -import rule24 from './suricata_double_encoded_characters_in_an_http_post.json'; -import rule25 from './command_shell_started_by_internet_explorer.json'; -import rule26 from './network_vnc_virtual_network_computing_from_the_internet.json'; -import rule27 from './windows_nmap_activity.json'; -import rule28 from './suspicious_process_started_by_a_script.json'; -import rule29 from './windows_network_anomalous_windows_process_using_https_ports.json'; -import rule30 from './powershell_network_connection.json'; -import rule31 from './windows_signed_binary_proxy_execution.json'; -import rule32 from './linux_kernel_module_activity.json'; -import rule33 from './network_vnc_virtual_network_computing_to_the_internet.json'; -import rule34 from './suricata_mimikatz_string_detected_in_http_response.json'; -import rule35 from './command_shell_started_by_svchost.json'; -import rule36 from './linux_tcpdump_activity.json'; -import rule37 from './process_started_by_ms_office_program_possible_payload.json'; -import rule38 from './windows_signed_binary_proxy_execution_download.json'; -import rule39 from './suricata_base64_encoded_startprocess_powershell_execution.json'; -import rule40 from './suricata_base64_encoded_invokecommand_powershell_execution.json'; -import rule41 from './suricata_directory_traversal_characters_in_http_response.json'; -import rule42 from './windows_microsoft_html_application_hta_connecting_to_the_internet.json'; -import rule43 from './suricata_tls_traffic_on_unusual_port_internet_destination.json'; -import rule44 from './process_started_by_acrobat_reader_possible_payload.json'; -import rule45 from './suricata_http_traffic_on_unusual_port_internet_destination.json'; -import rule46 from './windows_persistence_via_modification_of_existing_service.json'; -import rule47 from './windows_defense_evasion_or_persistence_via_hidden_files.json'; -import rule48 from './windows_execution_via_compiled_html_file.json'; -import rule49 from './linux_ptrace_activity.json'; -import rule50 from './suricata_nonimap_traffic_on_port_1443_imap.json'; -import rule51 from './windows_scheduled_task_activity.json'; -import rule52 from './suricata_ftp_traffic_on_unusual_port_internet_destination.json'; -import rule53 from './windows_wireshark_activity.json'; -import rule54 from './windows_execution_via_trusted_developer_utilities.json'; -import rule55 from './suricata_rpc_traffic_on_http_ports.json'; -import rule56 from './windows_process_discovery_via_tasklist_command.json'; -import rule57 from './suricata_cobaltstrike_artifact_in_an_dns_request.json'; -import rule58 from './suricata_serialized_php_detected.json'; -import rule59 from './windows_background_intelligent_transfer_service_bits_connecting_to_the_internet.json'; -import rule60 from './windows_registry_query_network.json'; -import rule61 from './windows_persistence_via_application_shimming.json'; -import rule62 from './network_proxy_port_activity_to_the_internet.json'; -import rule63 from './windows_whoami_command_activity.json'; -import rule64 from './suricata_shell_exec_php_function_in_an_http_post.json'; -import rule65 from './windump_activity.json'; -import rule66 from './windows_management_instrumentation_wmi_execution.json'; -import rule67 from './network_rdp_remote_desktop_protocol_from_the_internet.json'; -import rule68 from './windows_priv_escalation_via_accessibility_features.json'; -import rule69 from './psexec_activity.json'; -import rule70 from './linux_rawshark_activity.json'; -import rule71 from './suricata_nonftp_traffic_on_port_21.json'; -import rule72 from './network_ftp_file_transfer_protocol_activity_to_the_internet.json'; -import rule73 from './windows_certutil_connecting_to_the_internet.json'; -import rule74 from './suricata_nonsmb_traffic_on_tcp_port_139_smb.json'; -import rule75 from './network_rdp_remote_desktop_protocol_to_the_internet.json'; -import rule76 from './linux_whoami_commmand.json'; -import rule77 from './windows_persistence_or_priv_escalation_via_hooking.json'; -import rule78 from './linux_lzop_activity_possible_julianrunnels.json'; -import rule79 from './suricata_nontls_on_tls_port.json'; -import rule80 from './network_irc_internet_relay_chat_protocol_activity_to_the_internet.json'; -import rule81 from './linux_network_anomalous_process_using_https_ports.json'; -import rule82 from './windows_credential_dumping_via_registry_save.json'; -import rule83 from './network_rpc_remote_procedure_call_from_the_internet.json'; -import rule84 from './windows_credential_dumping_via_imageload.json'; -import rule85 from './windows_burp_ce_activity.json'; -import rule86 from './linux_hping_activity.json'; -import rule87 from './windows_command_prompt_connecting_to_the_internet.json'; -import rule88 from './network_nat_traversal_port_activity.json'; -import rule89 from './network_rpc_remote_procedure_call_to_the_internet.json'; -import rule90 from './suricata_possible_cobalt_strike_malleable_c2_null_response.json'; -import rule91 from './windows_remote_management_execution.json'; -import rule92 from './suricata_lazagne_artifact_in_an_http_post.json'; -import rule93 from './windows_netcat_network_activity.json'; -import rule94 from './windows_iodine_activity.json'; -import rule95 from './network_port_26_activity.json'; -import rule96 from './windows_execution_via_connection_manager.json'; -import rule97 from './linux_process_started_in_temp_directory.json'; -import rule98 from './suricata_eval_php_function_in_an_http_request.json'; -import rule99 from './linux_web_download.json'; -import rule100 from './suricata_ssh_traffic_not_on_port_22_internet_destination.json'; -import rule101 from './network_port_8000_activity.json'; -import rule102 from './windows_process_started_by_the_java_runtime.json'; -import rule103 from './suricata_possible_sql_injection_sql_commands_in_http_transactions.json'; -import rule104 from './network_smb_windows_file_sharing_activity_to_the_internet.json'; -import rule105 from './network_port_8000_activity_to_the_internet.json'; -import rule106 from './command_shell_started_by_powershell.json'; -import rule107 from './linux_nmap_activity.json'; -import rule108 from './search_windows_10.json'; -import rule109 from './network_smtp_to_the_internet.json'; -import rule110 from './windows_payload_obfuscation_via_certutil.json'; -import rule111 from './network_pptp_point_to_point_tunneling_protocol_activity.json'; -import rule112 from './linux_unusual_shell_activity.json'; -import rule113 from './linux_mknod_activity.json'; -import rule114 from './network_sql_server_port_activity_to_the_internet.json'; -import rule115 from './suricata_commonly_abused_dns_domain_detected.json'; -import rule116 from './linux_iodine_activity.json'; -import rule117 from './suricata_mimikatz_artifacts_in_an_http_post.json'; -import rule118 from './windows_execution_via_net_com_assemblies.json'; -import rule119 from './suricata_dns_traffic_on_unusual_tcp_port.json'; -import rule120 from './suricata_base64_encoded_newobject_powershell_execution.json'; -import rule121 from './windows_netcat_activity.json'; -import rule122 from './windows_persistence_via_bits_jobs.json'; -import rule123 from './linux_nping_activity.json'; -import rule124 from './windows_execution_via_regsvr32.json'; -import rule125 from './process_started_by_windows_defender.json'; -import rule126 from './windows_indirect_command_execution.json'; -import rule127 from './network_ssh_secure_shell_from_the_internet.json'; -import rule128 from './windows_html_help_executable_program_connecting_to_the_internet.json'; -import rule129 from './suricata_windows_executable_served_by_jpeg_web_content.json'; -import rule130 from './network_dns_directly_to_the_internet.json'; -import rule131 from './windows_defense_evasion_via_windows_event_log_tools.json'; -import rule132 from './suricata_nondns_traffic_on_tcp_port_53.json'; -import rule133 from './windows_persistence_via_netshell_helper_dll.json'; -import rule134 from './windows_script_interpreter_connecting_to_the_internet.json'; -import rule135 from './windows_defense_evasion_decoding_using_certutil.json'; -import rule136 from './linux_shell_activity_by_web_server.json'; -import rule137 from './linux_ldso_process_activity.json'; -import rule138 from './windows_mimikatz_activity.json'; -import rule139 from './suricata_nonssh_traffic_on_port_22.json'; -import rule140 from './windows_data_compression_using_powershell.json'; -import rule141 from './windows_nmap_scan_activity.json'; +import rule1 from './eql_bypass_uac_via_sdclt.json'; +import rule2 from './eql_clearing_windows_event_logs.json'; +import rule3 from './eql_suspicious_ms_office_child_process.json'; +import rule4 from './eql_bypass_uac_event_viewer.json'; +import rule5 from './eql_volume_shadow_copy_deletion_via_wmic.json'; +import rule6 from './eql_adobe_hijack_persistence.json'; +import rule7 from './eql_unusual_network_connection_via_rundll32.json'; +import rule8 from './eql_delete_volume_usn_journal_with_fsutil.json'; +import rule9 from './eql_mshta_making_network_connections.json'; +import rule10 from './eql_unusual_process_network_connection.json'; +import rule11 from './eql_suspicious_ms_outlook_child_process.json'; +import rule12 from './eql_audio_capture_via_soundrecorder.json'; +import rule13 from './eql_direct_outbound_smb_connection.json'; +import rule14 from './eql_windows_script_executing_powershell.json'; +import rule15 from './eql_deleting_backup_catalogs_with_wbadmin.json'; +import rule16 from './eql_suspicious_pdf_reader_child_process.json'; +import rule17 from './eql_local_service_commands.json'; +import rule18 from './eql_dll_search_order_hijack.json'; +import rule19 from './eql_bypass_uac_via_cmstp.json'; +import rule20 from './eql_user_account_creation.json'; +import rule21 from './eql_wmic_command_lateral_movement.json'; +import rule22 from './eql_system_shells_via_services.json'; +import rule23 from './eql_msxsl_making_network_connections.json'; +import rule24 from './eql_local_scheduled_task_commands.json'; +import rule25 from './eql_msbuild_making_network_connections.json'; +import rule26 from './eql_encoding_or_decoding_files_via_certutil.json'; +import rule27 from './eql_disable_windows_firewall_rules_with_netsh.json'; +import rule28 from './eql_adding_the_hidden_file_attribute_with_via_attribexe.json'; +import rule29 from './eql_psexec_lateral_movement_command.json'; +import rule30 from './eql_user_added_to_administrator_group.json'; +import rule31 from './eql_audio_capture_via_powershell.json'; +import rule32 from './eql_unusual_parentchild_relationship.json'; +import rule33 from './eql_modification_of_boot_configuration.json'; +import rule34 from './eql_volume_shadow_copy_deletion_via_vssadmin.json'; +import rule35 from './suricata_category_large_scale_information_leak.json'; +import rule36 from './suricata_category_attempted_information_leak.json'; +import rule37 from './suricata_category_not_suspicious_traffic.json'; +import rule38 from './suricata_category_potentially_bad_traffic.json'; +import rule39 from './suricata_category_information_leak.json'; +import rule40 from './suricata_category_unknown_traffic.json'; +import rule41 from './suricata_category_successful_administrator_privilege_gain.json'; +import rule42 from './suricata_category_attempted_administrator_privilege_gain.json'; +import rule43 from './suricata_category_unsuccessful_user_privilege_gain.json'; +import rule44 from './suricata_category_successful_user_privilege_gain.json'; +import rule45 from './suricata_category_attempted_user_privilege_gain.json'; +import rule46 from './suricata_category_attempted_denial_of_service.json'; +import rule47 from './suricata_category_decode_of_an_rpc_query.json'; +import rule48 from './suricata_category_denial_of_service.json'; +import rule49 from './suricata_category_attempted_login_with_suspicious_username.json'; +import rule50 from './suricata_category_client_using_unusual_port.json'; +import rule51 from './suricata_category_suspicious_filename_detected.json'; +import rule52 from './suricata_category_a_suspicious_string_was_detected.json'; +import rule53 from './suricata_category_tcp_connection_detected.json'; +import rule54 from './suricata_category_executable_code_was_detected.json'; +import rule55 from './suricata_category_network_trojan_detected.json'; +import rule56 from './suricata_category_system_call_detected.json'; +import rule57 from './suricata_category_potentially_vulnerable_web_application_access.json'; +import rule58 from './suricata_category_nonstandard_protocol_or_event.json'; +import rule59 from './suricata_category_denial_of_service_attack.json'; +import rule60 from './suricata_category_generic_protocol_command_decode.json'; +import rule61 from './suricata_category_network_scan_detected.json'; +import rule62 from './suricata_category_web_application_attack.json'; +import rule63 from './suricata_category_generic_icmp_event.json'; +import rule64 from './suricata_category_misc_attack.json'; +import rule65 from './suricata_category_default_username_and_password_login_attempt.json'; +import rule66 from './suricata_category_external_ip_address_retrieval.json'; +import rule67 from './suricata_category_potential_corporate_privacy_violation.json'; +import rule68 from './suricata_category_targeted_malicious_activity.json'; +import rule69 from './suricata_category_observed_c2_domain.json'; +import rule70 from './suricata_category_exploit_kit_activity.json'; +import rule71 from './suricata_category_possibly_unwanted_program.json'; +import rule72 from './suricata_category_successful_credential_theft.json'; +import rule73 from './suricata_category_possible_social_engineering_attempted.json'; +import rule74 from './suricata_category_crypto_currency_mining_activity.json'; +import rule75 from './suricata_category_malware_command_and_control_activity.json'; +import rule76 from './suricata_category_misc_activity.json'; +import rule77 from './windows_powershell_connecting_to_the_internet.json'; +import rule78 from './windows_net_user_command_activity.json'; +import rule79 from './windows_image_load_from_a_temp_directory.json'; +import rule80 from './network_ssh_secure_shell_to_the_internet.json'; +import rule81 from './suricata_nonhttp_traffic_on_tcp_port_80.json'; +import rule82 from './windows_misc_lolbin_connecting_to_the_internet.json'; +import rule83 from './linux_strace_activity.json'; +import rule84 from './suricata_directory_reversal_characters_in_an_http_request.json'; +import rule85 from './suricata_dns_traffic_on_unusual_udp_port.json'; +import rule86 from './network_telnet_port_activity.json'; +import rule87 from './suricata_directory_traversal_in_downloaded_zip_file.json'; +import rule88 from './windows_execution_via_microsoft_html_application_hta.json'; +import rule89 from './windows_credential_dumping_commands.json'; +import rule90 from './windows_net_command_activity_by_the_system_account.json'; +import rule91 from './windows_register_server_program_connecting_to_the_internet.json'; +import rule92 from './linux_java_process_connecting_to_the_internet.json'; +import rule93 from './suricata_imap_traffic_on_unusual_port_internet_destination.json'; +import rule94 from './suricata_double_encoded_characters_in_a_uri.json'; +import rule95 from './network_tor_activity_to_the_internet.json'; +import rule96 from './windows_registry_query_local.json'; +import rule97 from './linux_netcat_network_connection.json'; +import rule98 from './windows_defense_evasion_via_filter_manager.json'; +import rule99 from './suricata_nondns_traffic_on_udp_port_53.json'; +import rule100 from './suricata_double_encoded_characters_in_an_http_post.json'; +import rule101 from './command_shell_started_by_internet_explorer.json'; +import rule102 from './network_vnc_virtual_network_computing_from_the_internet.json'; +import rule103 from './windows_nmap_activity.json'; +import rule104 from './suspicious_process_started_by_a_script.json'; +import rule105 from './windows_network_anomalous_windows_process_using_https_ports.json'; +import rule106 from './powershell_network_connection.json'; +import rule107 from './windows_signed_binary_proxy_execution.json'; +import rule108 from './linux_kernel_module_activity.json'; +import rule109 from './network_vnc_virtual_network_computing_to_the_internet.json'; +import rule110 from './suricata_mimikatz_string_detected_in_http_response.json'; +import rule111 from './command_shell_started_by_svchost.json'; +import rule112 from './linux_tcpdump_activity.json'; +import rule113 from './process_started_by_ms_office_program_possible_payload.json'; +import rule114 from './windows_signed_binary_proxy_execution_download.json'; +import rule115 from './suricata_base64_encoded_startprocess_powershell_execution.json'; +import rule116 from './suricata_base64_encoded_invokecommand_powershell_execution.json'; +import rule117 from './suricata_directory_traversal_characters_in_http_response.json'; +import rule118 from './windows_microsoft_html_application_hta_connecting_to_the_internet.json'; +import rule119 from './suricata_tls_traffic_on_unusual_port_internet_destination.json'; +import rule120 from './process_started_by_acrobat_reader_possible_payload.json'; +import rule121 from './suricata_http_traffic_on_unusual_port_internet_destination.json'; +import rule122 from './windows_persistence_via_modification_of_existing_service.json'; +import rule123 from './windows_defense_evasion_or_persistence_via_hidden_files.json'; +import rule124 from './windows_execution_via_compiled_html_file.json'; +import rule125 from './linux_ptrace_activity.json'; +import rule126 from './suricata_nonimap_traffic_on_port_1443_imap.json'; +import rule127 from './windows_scheduled_task_activity.json'; +import rule128 from './suricata_ftp_traffic_on_unusual_port_internet_destination.json'; +import rule129 from './windows_wireshark_activity.json'; +import rule130 from './windows_execution_via_trusted_developer_utilities.json'; +import rule131 from './suricata_rpc_traffic_on_http_ports.json'; +import rule132 from './windows_process_discovery_via_tasklist_command.json'; +import rule133 from './suricata_cobaltstrike_artifact_in_an_dns_request.json'; +import rule134 from './suricata_serialized_php_detected.json'; +import rule135 from './windows_background_intelligent_transfer_service_bits_connecting_to_the_internet.json'; +import rule136 from './windows_registry_query_network.json'; +import rule137 from './windows_persistence_via_application_shimming.json'; +import rule138 from './network_proxy_port_activity_to_the_internet.json'; +import rule139 from './windows_whoami_command_activity.json'; +import rule140 from './suricata_shell_exec_php_function_in_an_http_post.json'; +import rule141 from './windump_activity.json'; +import rule142 from './windows_management_instrumentation_wmi_execution.json'; +import rule143 from './network_rdp_remote_desktop_protocol_from_the_internet.json'; +import rule144 from './windows_priv_escalation_via_accessibility_features.json'; +import rule145 from './psexec_activity.json'; +import rule146 from './linux_rawshark_activity.json'; +import rule147 from './suricata_nonftp_traffic_on_port_21.json'; +import rule148 from './network_ftp_file_transfer_protocol_activity_to_the_internet.json'; +import rule149 from './windows_certutil_connecting_to_the_internet.json'; +import rule150 from './suricata_nonsmb_traffic_on_tcp_port_139_smb.json'; +import rule151 from './network_rdp_remote_desktop_protocol_to_the_internet.json'; +import rule152 from './linux_whoami_commmand.json'; +import rule153 from './windows_persistence_or_priv_escalation_via_hooking.json'; +import rule154 from './linux_lzop_activity_possible_julianrunnels.json'; +import rule155 from './suricata_nontls_on_tls_port.json'; +import rule156 from './network_irc_internet_relay_chat_protocol_activity_to_the_internet.json'; +import rule157 from './linux_network_anomalous_process_using_https_ports.json'; +import rule158 from './windows_credential_dumping_via_registry_save.json'; +import rule159 from './network_rpc_remote_procedure_call_from_the_internet.json'; +import rule160 from './windows_credential_dumping_via_imageload.json'; +import rule161 from './windows_burp_ce_activity.json'; +import rule162 from './linux_hping_activity.json'; +import rule163 from './windows_command_prompt_connecting_to_the_internet.json'; +import rule164 from './network_nat_traversal_port_activity.json'; +import rule165 from './network_rpc_remote_procedure_call_to_the_internet.json'; +import rule166 from './suricata_possible_cobalt_strike_malleable_c2_null_response.json'; +import rule167 from './windows_remote_management_execution.json'; +import rule168 from './suricata_lazagne_artifact_in_an_http_post.json'; +import rule169 from './windows_netcat_network_activity.json'; +import rule170 from './windows_iodine_activity.json'; +import rule171 from './network_port_26_activity.json'; +import rule172 from './windows_execution_via_connection_manager.json'; +import rule173 from './linux_process_started_in_temp_directory.json'; +import rule174 from './suricata_eval_php_function_in_an_http_request.json'; +import rule175 from './linux_web_download.json'; +import rule176 from './suricata_ssh_traffic_not_on_port_22_internet_destination.json'; +import rule177 from './network_port_8000_activity.json'; +import rule178 from './windows_process_started_by_the_java_runtime.json'; +import rule179 from './suricata_possible_sql_injection_sql_commands_in_http_transactions.json'; +import rule180 from './network_smb_windows_file_sharing_activity_to_the_internet.json'; +import rule181 from './network_port_8000_activity_to_the_internet.json'; +import rule182 from './command_shell_started_by_powershell.json'; +import rule183 from './linux_nmap_activity.json'; +import rule184 from './search_windows_10.json'; +import rule185 from './network_smtp_to_the_internet.json'; +import rule186 from './windows_payload_obfuscation_via_certutil.json'; +import rule187 from './network_pptp_point_to_point_tunneling_protocol_activity.json'; +import rule188 from './linux_unusual_shell_activity.json'; +import rule189 from './linux_mknod_activity.json'; +import rule190 from './network_sql_server_port_activity_to_the_internet.json'; +import rule191 from './suricata_commonly_abused_dns_domain_detected.json'; +import rule192 from './linux_iodine_activity.json'; +import rule193 from './suricata_mimikatz_artifacts_in_an_http_post.json'; +import rule194 from './windows_execution_via_net_com_assemblies.json'; +import rule195 from './suricata_dns_traffic_on_unusual_tcp_port.json'; +import rule196 from './suricata_base64_encoded_newobject_powershell_execution.json'; +import rule197 from './windows_netcat_activity.json'; +import rule198 from './windows_persistence_via_bits_jobs.json'; +import rule199 from './linux_nping_activity.json'; +import rule200 from './windows_execution_via_regsvr32.json'; +import rule201 from './process_started_by_windows_defender.json'; +import rule202 from './windows_indirect_command_execution.json'; +import rule203 from './network_ssh_secure_shell_from_the_internet.json'; +import rule204 from './windows_html_help_executable_program_connecting_to_the_internet.json'; +import rule205 from './suricata_windows_executable_served_by_jpeg_web_content.json'; +import rule206 from './network_dns_directly_to_the_internet.json'; +import rule207 from './windows_defense_evasion_via_windows_event_log_tools.json'; +import rule208 from './suricata_nondns_traffic_on_tcp_port_53.json'; +import rule209 from './windows_persistence_via_netshell_helper_dll.json'; +import rule210 from './windows_script_interpreter_connecting_to_the_internet.json'; +import rule211 from './windows_defense_evasion_decoding_using_certutil.json'; +import rule212 from './linux_shell_activity_by_web_server.json'; +import rule213 from './linux_ldso_process_activity.json'; +import rule214 from './windows_mimikatz_activity.json'; +import rule215 from './suricata_nonssh_traffic_on_port_22.json'; +import rule216 from './windows_data_compression_using_powershell.json'; +import rule217 from './windows_nmap_scan_activity.json'; export const rawRules = [ rule1, @@ -291,4 +367,80 @@ export const rawRules = [ rule139, rule140, rule141, + rule142, + rule143, + rule144, + rule145, + rule146, + rule147, + rule148, + rule149, + rule150, + rule151, + rule152, + rule153, + rule154, + rule155, + rule156, + rule157, + rule158, + rule159, + rule160, + rule161, + rule162, + rule163, + rule164, + rule165, + rule166, + rule167, + rule168, + rule169, + rule170, + rule171, + rule172, + rule173, + rule174, + rule175, + rule176, + rule177, + rule178, + rule179, + rule180, + rule181, + rule182, + rule183, + rule184, + rule185, + rule186, + rule187, + rule188, + rule189, + rule190, + rule191, + rule192, + rule193, + rule194, + rule195, + rule196, + rule197, + rule198, + rule199, + rule200, + rule201, + rule202, + rule203, + rule204, + rule205, + rule206, + rule207, + rule208, + rule209, + rule210, + rule211, + rule212, + rule213, + rule214, + rule215, + rule216, + rule217, ]; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_a_suspicious_string_was_detected.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_a_suspicious_string_was_detected.json new file mode 100644 index 00000000000000..eb4fa0fe411a9f --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_a_suspicious_string_was_detected.json @@ -0,0 +1,17 @@ +{ + "rule_id": "2a3d91c1-5065-46ab-bed0-93f80835b1d5", + "risk_score": 50, + "description": "Suricata Category - A suspicious string was detected", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - A suspicious string was detected", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"A suspicious string was detected\" or rule.category: \"A suspicious string was detected\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_attempted_administrator_privilege_gain.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_attempted_administrator_privilege_gain.json new file mode 100644 index 00000000000000..3fc61c50927c77 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_attempted_administrator_privilege_gain.json @@ -0,0 +1,17 @@ +{ + "rule_id": "f840129e-9089-4f46-8af1-0745e8f54713", + "risk_score": 50, + "description": "Suricata Category - Attempted Administrator Privilege Gain", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Attempted Administrator Privilege Gain", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Attempted Administrator Privilege Gain\" or rule.category: \"Attempted Administrator Privilege Gain\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_attempted_denial_of_service.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_attempted_denial_of_service.json new file mode 100644 index 00000000000000..e888b2076f137f --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_attempted_denial_of_service.json @@ -0,0 +1,17 @@ +{ + "rule_id": "a62927f4-2488-4679-b56f-cda1a7f4c9e1", + "risk_score": 50, + "description": "Suricata Category - Attempted Denial of Service", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Attempted Denial of Service", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Attempted Denial of Service\" or rule.category: \"Attempted Denial of Service\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_attempted_information_leak.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_attempted_information_leak.json new file mode 100644 index 00000000000000..ae93e8bce78012 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_attempted_information_leak.json @@ -0,0 +1,17 @@ +{ + "rule_id": "88d69362-f496-41d6-8e6b-a2dbaed3513f", + "risk_score": 50, + "description": "Suricata Category - Attempted Information Leak", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Attempted Information Leak", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Attempted Information Leak\" or rule.category: \"Attempted Information Leak\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_attempted_login_with_suspicious_username.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_attempted_login_with_suspicious_username.json new file mode 100644 index 00000000000000..c00e7a42aee06d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_attempted_login_with_suspicious_username.json @@ -0,0 +1,17 @@ +{ + "rule_id": "a84cd36c-dd5a-4e86-a2ce-44556c21cef0", + "risk_score": 50, + "description": "Suricata Category - Attempted Login with Suspicious Username", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Attempted Login with Suspicious Username", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"An attempted login using a suspicious username was detected\" or rule.category: \"An attempted login using a suspicious username was detected\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_attempted_user_privilege_gain.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_attempted_user_privilege_gain.json new file mode 100644 index 00000000000000..1b2fcbee310da6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_attempted_user_privilege_gain.json @@ -0,0 +1,17 @@ +{ + "rule_id": "eabce895-4602-4d20-8bf9-11c903bb3e08", + "risk_score": 50, + "description": "Suricata Category - Attempted User Privilege Gain", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Attempted User Privilege Gain", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Attempted User Privilege Gain\" or rule.category: \"Attempted User Privilege Gain\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_client_using_unusual_port.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_client_using_unusual_port.json new file mode 100644 index 00000000000000..feedffeaacc9c4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_client_using_unusual_port.json @@ -0,0 +1,17 @@ +{ + "rule_id": "00503a3c-304c-421c-bfea-e5d8fdfd9726", + "risk_score": 50, + "description": "Suricata Category - Client Using Unusual Port", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Client Using Unusual Port", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"A client was using an unusual port\" or rule.category: \"A client was using an unusual port\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_crypto_currency_mining_activity.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_crypto_currency_mining_activity.json new file mode 100644 index 00000000000000..e05461baf36de6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_crypto_currency_mining_activity.json @@ -0,0 +1,17 @@ +{ + "rule_id": "74cd4920-a441-41d2-8a23-5bee70626e60", + "risk_score": 50, + "description": "Suricata Category - Crypto Currency Mining Activity", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Crypto Currency Mining Activity", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Crypto Currency Mining Activity Detected\" or rule.category: \"Crypto Currency Mining Activity Detected\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_decode_of_an_rpc_query.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_decode_of_an_rpc_query.json new file mode 100644 index 00000000000000..0e22aa66ca04dd --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_decode_of_an_rpc_query.json @@ -0,0 +1,17 @@ +{ + "rule_id": "e9fc5bd3-c8a1-442c-be6d-032da07c508b", + "risk_score": 50, + "description": "Suricata Category - Decode of an RPC Query", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Decode of an RPC Query", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Decode of an RPC Query\" or rule.category: \"Decode of an RPC Query\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_default_username_and_password_login_attempt.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_default_username_and_password_login_attempt.json new file mode 100644 index 00000000000000..0810168bbaf158 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_default_username_and_password_login_attempt.json @@ -0,0 +1,17 @@ +{ + "rule_id": "190bd112-f831-4813-98b2-e45a934277c2", + "risk_score": 50, + "description": "Suricata Category - Default Username and Password Login Attempt", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Default Username and Password Login Attempt", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Attempt to login by a default username and password\" or rule.category: \"Attempt to login by a default username and password\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_denial_of_service.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_denial_of_service.json new file mode 100644 index 00000000000000..d6ef10a86c1845 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_denial_of_service.json @@ -0,0 +1,17 @@ +{ + "rule_id": "0e97e390-84db-4725-965a-a8b0b600f7be", + "risk_score": 50, + "description": "Suricata Category - Denial of Service", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Denial of Service", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Denial of Service\" or rule.category: \"Denial of Service\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_denial_of_service_attack.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_denial_of_service_attack.json new file mode 100644 index 00000000000000..3f4975bcdfb144 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_denial_of_service_attack.json @@ -0,0 +1,17 @@ +{ + "rule_id": "42a60eaa-fd20-479b-b6ca-bdb88d47b34b", + "risk_score": 50, + "description": "Suricata Category - Denial of Service Attack", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Denial of Service Attack", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Detection of a Denial of Service Attack\" or rule.category: \"Detection of a Denial of Service Attack\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_executable_code_was_detected.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_executable_code_was_detected.json new file mode 100644 index 00000000000000..f1f6177e015035 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_executable_code_was_detected.json @@ -0,0 +1,17 @@ +{ + "rule_id": "4699296b-5127-475a-9d83-8434fcd18136", + "risk_score": 50, + "description": "Suricata Category - Executable code was detected", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Executable code was detected", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Executable code was detected\" or rule.category: \"Executable code was detected\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_exploit_kit_activity.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_exploit_kit_activity.json new file mode 100644 index 00000000000000..025f0f4d266f93 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_exploit_kit_activity.json @@ -0,0 +1,17 @@ +{ + "rule_id": "b3111af8-79bf-4ec3-97ae-28d9ed9fbd38", + "risk_score": 50, + "description": "Suricata Category - Exploit Kit Activity", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Exploit Kit Activity", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Exploit Kit Activity Detected\" or rule.category: \"Exploit Kit Activity Detected\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_external_ip_address_retrieval.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_external_ip_address_retrieval.json new file mode 100644 index 00000000000000..eab3cb59108617 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_external_ip_address_retrieval.json @@ -0,0 +1,17 @@ +{ + "rule_id": "c7df9ecf-d6be-4ef8-9871-cb317dfff0b4", + "risk_score": 50, + "description": "Suricata Category - External IP Address Retrieval", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - External IP Address Retrieval", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Device Retrieving External IP Address Detected\" or rule.category: \"Device Retrieving External IP Address Detected\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_generic_icmp_event.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_generic_icmp_event.json new file mode 100644 index 00000000000000..37b93ce6886d89 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_generic_icmp_event.json @@ -0,0 +1,17 @@ +{ + "rule_id": "3309bffa-7c43-409a-acea-6631c1b077e5", + "risk_score": 50, + "description": "Suricata Category - Generic ICMP event", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Generic ICMP event", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Generic ICMP event\" or rule.category: \"Generic ICMP event\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_generic_protocol_command_decode.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_generic_protocol_command_decode.json new file mode 100644 index 00000000000000..ed5a6dbe47f5a4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_generic_protocol_command_decode.json @@ -0,0 +1,17 @@ +{ + "rule_id": "6fd2deb4-a7a9-4221-8b7b-8d26836a8c30", + "risk_score": 50, + "description": "Suricata Category - Generic Protocol Command Decode", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Generic Protocol Command Decode", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Generic Protocol Command Decode\" or rule.category: \"Generic Protocol Command Decode\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_information_leak.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_information_leak.json new file mode 100644 index 00000000000000..7cec0f24570ec5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_information_leak.json @@ -0,0 +1,17 @@ +{ + "rule_id": "95df8ff4-7169-4c84-ae50-3561b1d1bc91", + "risk_score": 50, + "description": "Suricata Category - Information Leak", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Information Leak", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Information Leak\" or rule.category: \"Information Leak\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_large_scale_information_leak.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_large_scale_information_leak.json new file mode 100644 index 00000000000000..c871624f86d9f1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_large_scale_information_leak.json @@ -0,0 +1,17 @@ +{ + "rule_id": "ca98de30-c703-4170-97ae-ab2b340f6080", + "risk_score": 50, + "description": "Suricata Category - Large Scale Information Leak", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Large Scale Information Leak", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Large Scale Information Leak\" or rule.category: \"Large Scale Information Leak\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_malware_command_and_control_activity.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_malware_command_and_control_activity.json new file mode 100644 index 00000000000000..e0b7e41b67b92d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_malware_command_and_control_activity.json @@ -0,0 +1,17 @@ +{ + "rule_id": "56656341-2940-4a69-b8fe-acf3c734f540", + "risk_score": 50, + "description": "Suricata Category - Malware Command and Control Activity", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Malware Command and Control Activity", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Malware Command and Control Activity Detected\" or rule.category: \"Malware Command and Control Activity Detected\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_misc_activity.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_misc_activity.json new file mode 100644 index 00000000000000..aad3b2c5057cef --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_misc_activity.json @@ -0,0 +1,17 @@ +{ + "rule_id": "403ddbde-a486-4dd7-b932-cee4ebef88b6", + "risk_score": 50, + "description": "Suricata Category - Misc Activity", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Misc Activity", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Misc activity\" or rule.category: \"Misc activity\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_misc_attack.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_misc_attack.json new file mode 100644 index 00000000000000..eea27b6fa8ae2e --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_misc_attack.json @@ -0,0 +1,17 @@ +{ + "rule_id": "83277123-749f-49da-ad3d-d59f35490db1", + "risk_score": 50, + "description": "Suricata Category - Misc Attack", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Misc Attack", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Misc Attack\" or rule.category: \"Misc Attack\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_network_scan_detected.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_network_scan_detected.json new file mode 100644 index 00000000000000..0eb2b136bbef9d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_network_scan_detected.json @@ -0,0 +1,17 @@ +{ + "rule_id": "7e969b45-d005-4173-aee7-a7aaa79bc372", + "risk_score": 50, + "description": "Suricata Category - Network Scan Detected", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Network Scan Detected", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Detection of a Network Scan\" or rule.category: \"Detection of a Network Scan\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_network_trojan_detected.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_network_trojan_detected.json new file mode 100644 index 00000000000000..f3aeb8393c13f1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_network_trojan_detected.json @@ -0,0 +1,17 @@ +{ + "rule_id": "76ffa464-ec03-42e1-87ee-87760c331061", + "risk_score": 50, + "description": "Suricata Category - Network Trojan Detected", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Network Trojan Detected", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"A Network Trojan was detected\" or rule.category: \"A Network Trojan was detected\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_nonstandard_protocol_or_event.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_nonstandard_protocol_or_event.json new file mode 100644 index 00000000000000..c3b696afa8e439 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_nonstandard_protocol_or_event.json @@ -0,0 +1,17 @@ +{ + "rule_id": "82f9f485-873b-4eeb-b231-052ab81e05b8", + "risk_score": 50, + "description": "Suricata Category - Non-Standard Protocol or Event", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Non-Standard Protocol or Event", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Detection of a non-standard protocol or event\" or rule.category: \"Detection of a non-standard protocol or event\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_not_suspicious_traffic.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_not_suspicious_traffic.json new file mode 100644 index 00000000000000..e26180a429a812 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_not_suspicious_traffic.json @@ -0,0 +1,17 @@ +{ + "rule_id": "c0f684ff-4f15-44e7-912d-aa8b8f08a910", + "risk_score": 50, + "description": "Suricata Category - Not Suspicious Traffic", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Not Suspicious Traffic", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Not Suspicious Traffic\" or rule.category: \"Not Suspicious Traffic\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_observed_c2_domain.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_observed_c2_domain.json new file mode 100644 index 00000000000000..7a11a3738b7a41 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_observed_c2_domain.json @@ -0,0 +1,17 @@ +{ + "rule_id": "8adfa89f-aa90-4d26-9d7a-7da652cae902", + "risk_score": 50, + "description": "Suricata Category - Observed C2 Domain", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Observed C2 Domain", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Domain Observed Used for C2 Detected\" or rule.category: \"Domain Observed Used for C2 Detected\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_possible_social_engineering_attempted.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_possible_social_engineering_attempted.json new file mode 100644 index 00000000000000..f21da57a4d7b74 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_possible_social_engineering_attempted.json @@ -0,0 +1,17 @@ +{ + "rule_id": "7d2d5a5f-f590-407d-933a-42adb1a7bcef", + "risk_score": 50, + "description": "Suricata Category - Possible Social Engineering Attempted", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Possible Social Engineering Attempted", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Possible Social Engineering Attempted\" or rule.category: \"Possible Social Engineering Attempted\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_possibly_unwanted_program.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_possibly_unwanted_program.json new file mode 100644 index 00000000000000..7303185c6e9a4f --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_possibly_unwanted_program.json @@ -0,0 +1,17 @@ +{ + "rule_id": "1b9a31e8-fdfa-400e-aa4e-79a7f1a1da18", + "risk_score": 50, + "description": "Suricata Category - Possibly Unwanted Program", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Possibly Unwanted Program", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Possibly Unwanted Program Detected\" or rule.category: \"Possibly Unwanted Program Detected\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_potential_corporate_privacy_violation.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_potential_corporate_privacy_violation.json new file mode 100644 index 00000000000000..d3f867778bb43b --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_potential_corporate_privacy_violation.json @@ -0,0 +1,17 @@ +{ + "rule_id": "1c70f5d5-eae0-4d00-b35a-d34ca607094e", + "risk_score": 50, + "description": "Suricata Category - Potential Corporate Privacy Violation", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Potential Corporate Privacy Violation", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Potential Corporate Privacy Violation\" or rule.category: \"Potential Corporate Privacy Violation\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_potentially_bad_traffic.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_potentially_bad_traffic.json new file mode 100644 index 00000000000000..f77fe14014db30 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_potentially_bad_traffic.json @@ -0,0 +1,17 @@ +{ + "rule_id": "197cdd5a-9880-4780-a87c-594d0ed2b7b4", + "risk_score": 50, + "description": "Suricata Category - Potentially Bad Traffic", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Potentially Bad Traffic", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Potentially Bad Traffic\" or rule.category: \"Potentially Bad Traffic\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_potentially_vulnerable_web_application_access.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_potentially_vulnerable_web_application_access.json new file mode 100644 index 00000000000000..1665f8ca824249 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_potentially_vulnerable_web_application_access.json @@ -0,0 +1,17 @@ +{ + "rule_id": "0993e926-1a01-4c28-918a-cdd5741a19a8", + "risk_score": 50, + "description": "Suricata Category - Potentially Vulnerable Web Application Access", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Potentially Vulnerable Web Application Access", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"access to a potentially vulnerable web application\" or rule.category: \"access to a potentially vulnerable web application\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_successful_administrator_privilege_gain.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_successful_administrator_privilege_gain.json new file mode 100644 index 00000000000000..e7b636c421c161 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_successful_administrator_privilege_gain.json @@ -0,0 +1,17 @@ +{ + "rule_id": "f068e655-1f52-4d81-839a-9c08c6543ceb", + "risk_score": 50, + "description": "Suricata Category - Successful Administrator Privilege Gain", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Successful Administrator Privilege Gain", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Successful Administrator Privilege Gain\" or rule.category: \"Successful Administrator Privilege Gain\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_successful_credential_theft.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_successful_credential_theft.json new file mode 100644 index 00000000000000..bb87b86a75860a --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_successful_credential_theft.json @@ -0,0 +1,17 @@ +{ + "rule_id": "90f3e735-2187-4e8e-8d28-6e3249964851", + "risk_score": 50, + "description": "Suricata Category - Successful Credential Theft", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Successful Credential Theft", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Successful Credential Theft Detected\" or rule.category: \"Successful Credential Theft Detected\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_successful_user_privilege_gain.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_successful_user_privilege_gain.json new file mode 100644 index 00000000000000..d6af6e2baabea2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_successful_user_privilege_gain.json @@ -0,0 +1,17 @@ +{ + "rule_id": "f8ebd022-6e92-4b80-ac49-7ee011ba2ce0", + "risk_score": 50, + "description": "Suricata Category - Successful User Privilege Gain", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Successful User Privilege Gain", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Successful User Privilege Gain\" or rule.category: \"Successful User Privilege Gain\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_suspicious_filename_detected.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_suspicious_filename_detected.json new file mode 100644 index 00000000000000..205940bb7d0bc3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_suspicious_filename_detected.json @@ -0,0 +1,17 @@ +{ + "rule_id": "d0489b07-8140-4e3d-a2b7-52f2c06fdc7c", + "risk_score": 50, + "description": "Suricata Category - Suspicious Filename Detected", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Suspicious Filename Detected", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"A suspicious filename was detected\" or rule.category: \"A suspicious filename was detected\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_system_call_detected.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_system_call_detected.json new file mode 100644 index 00000000000000..a86ea16ddf2077 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_system_call_detected.json @@ -0,0 +1,17 @@ +{ + "rule_id": "44a5c55a-a34f-43c3-8f21-df502862aa9b", + "risk_score": 50, + "description": "Suricata Category - System Call Detected", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - System Call Detected", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"A system call was detected\" or rule.category: \"A system call was detected\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_targeted_malicious_activity.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_targeted_malicious_activity.json new file mode 100644 index 00000000000000..8923c07341b935 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_targeted_malicious_activity.json @@ -0,0 +1,17 @@ +{ + "rule_id": "d299379d-41de-4640-96b6-77aaa9adfa6f", + "risk_score": 50, + "description": "Suricata Category - Targeted Malicious Activity", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Targeted Malicious Activity", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Targeted Malicious Activity was Detected\" or rule.category: \"Targeted Malicious Activity was Detected\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_tcp_connection_detected.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_tcp_connection_detected.json new file mode 100644 index 00000000000000..a1e400c71b8be2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_tcp_connection_detected.json @@ -0,0 +1,17 @@ +{ + "rule_id": "ddf402cf-307d-4f46-a25d-dce3aee1ad13", + "risk_score": 50, + "description": "Suricata Category - TCP Connection Detected", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - TCP Connection Detected", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"A TCP connection was detected\" or rule.category: \"A TCP connection was detected\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_unknown_traffic.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_unknown_traffic.json new file mode 100644 index 00000000000000..28ae09a6cbe5c8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_unknown_traffic.json @@ -0,0 +1,17 @@ +{ + "rule_id": "827ea90c-00c2-45f7-b873-dd060297b2d2", + "risk_score": 50, + "description": "Suricata Category - Unknown Traffic", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Unknown Traffic", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Unknown Traffic\" or rule.category: \"Unknown Traffic\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_unsuccessful_user_privilege_gain.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_unsuccessful_user_privilege_gain.json new file mode 100644 index 00000000000000..5eba26752f7177 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_unsuccessful_user_privilege_gain.json @@ -0,0 +1,17 @@ +{ + "rule_id": "85471d30-78c9-48f6-b2db-ab5b2547e450", + "risk_score": 50, + "description": "Suricata Category - Unsuccessful User Privilege Gain", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Unsuccessful User Privilege Gain", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Unsuccessful User Privilege Gain\" or rule.category: \"Unsuccessful User Privilege Gain\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_web_application_attack.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_web_application_attack.json new file mode 100644 index 00000000000000..6cd7b2d87ac1aa --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/suricata_category_web_application_attack.json @@ -0,0 +1,17 @@ +{ + "rule_id": "e856918b-f26e-4893-84b9-3deb65046fb7", + "risk_score": 50, + "description": "Suricata Category - Web Application Attack", + "immutable": true, + "interval": "5m", + "name": "Suricata Category - Web Application Attack", + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "event.module: suricata and event.kind: alert and (suricata.eve.alert.category: \"Web Application Attack\" or rule.category: \"Web Application Attack\")", + "language": "kuery", + "filters": [], + "enabled": false, + "version": 1 +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts index caeec68c504e69..b0578174e1f658 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts @@ -31,6 +31,10 @@ export interface UpdateRulesRequest extends RequestFacade { payload: UpdateRuleAlertParamsRest; } +export interface BulkUpdateRulesRequest extends RequestFacade { + payload: UpdateRuleAlertParamsRest[]; +} + export type RuleAlertType = Alert & { id: string; params: RuleTypeParams; @@ -40,6 +44,18 @@ export interface RulesRequest extends RequestFacade { payload: RuleAlertParamsRest; } +export interface BulkRulesRequest extends RequestFacade { + payload: RuleAlertParamsRest[]; +} + +export type QueryRequest = Omit & { + query: { id: string | undefined; rule_id: string | undefined }; +}; + +export interface QueryBulkRequest extends RequestFacade { + payload: Array; +} + export interface FindRuleParams { alertsClient: AlertsClient; perPage?: number; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_bulk.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_bulk.sh new file mode 100755 index 00000000000000..8f540e14ecdf18 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_bulk.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; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +RULES=${1:-./rules/bulk/delete_by_rule_id.json} + +# Example: ./delete_rule_bulk.sh +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X DELETE ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_bulk_delete \ + -d @${RULES} \ + | jq .; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_rule_bulk.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_rule_bulk.sh new file mode 100755 index 00000000000000..ad10ffc31aa949 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_rule_bulk.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; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +RULES=${1:-./rules/bulk/multiple_ruleid_queries.json} + +# Example: ./post_rule_bulk.sh +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_bulk_create \ + -d @${RULES} \ + | jq .; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/delete_by_rule_id.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/delete_by_rule_id.json new file mode 100644 index 00000000000000..1bd9bdc7033315 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/delete_by_rule_id.json @@ -0,0 +1,8 @@ +[ + { + "rule_id": "query-rule-id-1" + }, + { + "rule_id": "query-rule-id-2" + } +] diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/multiple_ruleid_queries.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/multiple_ruleid_queries.json new file mode 100644 index 00000000000000..bee2363aa5e0c8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/multiple_ruleid_queries.json @@ -0,0 +1,24 @@ +[ + { + "name": "Query with a rule id Number 1", + "description": "Query with a rule_id that acts like an external id", + "rule_id": "query-rule-id-1", + "risk_score": 1, + "severity": "high", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "user.name: root or user.name: admin" + }, + { + "name": "Query with a rule id Number 2", + "description": "Query with a rule_id that acts like an external id", + "rule_id": "query-rule-id-2", + "risk_score": 2, + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "user.name: root or user.name: admin" + } +] diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/multiple_simplest_queries.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/multiple_simplest_queries.json new file mode 100644 index 00000000000000..9e5328ffabe2ec --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/multiple_simplest_queries.json @@ -0,0 +1,22 @@ +[ + { + "name": "Simplest Query Number 1", + "description": "Simplest query with the least amount of fields required", + "risk_score": 1, + "severity": "high", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "user.name: root or user.name: admin" + }, + { + "name": "Simplest Query Number 2", + "description": "Simplest query with the least amount of fields required", + "risk_score": 2, + "severity": "low", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "user.name: root or user.name: admin" + } +] diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/update_names.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/update_names.json new file mode 100644 index 00000000000000..4dca8f88a7e67c --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/bulk/update_names.json @@ -0,0 +1,10 @@ +[ + { + "name": "Rule id Number 1 with an updated name", + "rule_id": "query-rule-id-1" + }, + { + "name": "Rule id Number 2 with an updated name", + "rule_id": "query-rule-id-2" + } +] diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule_bulk.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule_bulk.sh new file mode 100755 index 00000000000000..c9cb0676821c59 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule_bulk.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; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +RULES=${1:-./rules/bulk/update_names.json} + +# Example: ./update_rule_bulk.sh +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X PUT ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_bulk_update \ + -d @${RULES} \ + | jq .; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.test.ts index 43b5ce4b590a39..534215f5a12280 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.test.ts @@ -509,5 +509,140 @@ describe('get_filter', () => { }) ).rejects.toThrow('savedId parameter should be defined'); }); + + test('it works with references and does not add indexes', () => { + const esQuery = getQueryFilter( + '(event.module:suricata and event.kind:alert) and suricata.eve.alert.signature_id: (2610182 or 2610183 or 2610184 or 2610185 or 2610186 or 2610187)', + 'kuery', + [], + ['my custom index'] + ); + expect(esQuery).toEqual({ + bool: { + must: [], + filter: [ + { + bool: { + filter: [ + { + bool: { + filter: [ + { + bool: { + should: [{ match: { 'event.module': 'suricata' } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [{ match: { 'event.kind': 'alert' } }], + minimum_should_match: 1, + }, + }, + ], + }, + }, + { + bool: { + should: [ + { + bool: { + should: [{ match: { 'suricata.eve.alert.signature_id': 2610182 } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + bool: { + should: [ + { match: { 'suricata.eve.alert.signature_id': 2610183 } }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + bool: { + should: [ + { match: { 'suricata.eve.alert.signature_id': 2610184 } }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + bool: { + should: [ + { + match: { + 'suricata.eve.alert.signature_id': 2610185, + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + bool: { + should: [ + { + match: { + 'suricata.eve.alert.signature_id': 2610186, + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + match: { + 'suricata.eve.alert.signature_id': 2610187, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + ], + should: [], + must_not: [], + }, + }); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts index a30182c5378843..544250858a0832 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts @@ -24,6 +24,9 @@ export interface SignalsStatusParams { export interface SignalQueryParams { query: object | undefined | null; aggs: object | undefined | null; + _source: string[] | undefined | null; + size: number | undefined | null; + track_total_hits: boolean | undefined | null; } export type SignalsStatusRestParams = Omit & { diff --git a/x-pack/legacy/plugins/siem/server/lib/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/framework/kibana_framework_adapter.ts index 38fec7fc05a3c1..39f75e6ea36c3a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/framework/kibana_framework_adapter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/framework/kibana_framework_adapter.ts @@ -14,8 +14,10 @@ import { KibanaResponseFactory, RequestHandlerContext, PluginInitializerContext, -} from 'src/core/server'; + KibanaRequest, +} from '../../../../../../../src/core/server'; import { IndexPatternsFetcher } from '../../../../../../../src/plugins/data/server'; +import { AuthenticatedUser } from '../../../../../../plugins/security/common/model'; import { RequestFacade } from '../../types'; import { @@ -25,16 +27,19 @@ import { internalFrameworkRequest, WrappableRequest, } from './types'; +import { SiemPluginSecurity, PluginsSetup } from '../../plugin'; export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { public version: string; private isProductionMode: boolean; private router: IRouter; + private security: SiemPluginSecurity; - constructor(core: CoreSetup, env: PluginInitializerContext['env']) { + constructor(core: CoreSetup, plugins: PluginsSetup, env: PluginInitializerContext['env']) { this.version = env.packageInfo.version; this.isProductionMode = env.mode.prod; this.router = core.http.createRouter(); + this.security = plugins.security; } public async callWithRequest( @@ -76,10 +81,11 @@ export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { }, async (context, request, response) => { try { + const user = await this.getCurrentUserInfo(request); const gqlResponse = await runHttpQuery([request], { method: 'POST', options: (req: RequestFacade) => ({ - context: { req: wrapRequest(req, context) }, + context: { req: wrapRequest(req, context, user) }, schema, }), query: request.body, @@ -108,11 +114,12 @@ export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { }, async (context, request, response) => { try { + const user = await this.getCurrentUserInfo(request); const { query } = request; const gqlResponse = await runHttpQuery([request], { method: 'GET', options: (req: RequestFacade) => ({ - context: { req: wrapRequest(req, context) }, + context: { req: wrapRequest(req, context, user) }, schema, }), query, @@ -159,6 +166,15 @@ export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { } } + private async getCurrentUserInfo(request: KibanaRequest): Promise { + try { + const user = await this.security.authc.getCurrentUser(request); + return user; + } catch { + return null; + } + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any private handleError(error: any, response: KibanaResponseFactory) { if (error.name !== 'HttpQueryError') { @@ -194,7 +210,8 @@ export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { export function wrapRequest( req: InternalRequest, - context: RequestHandlerContext + context: RequestHandlerContext, + user: AuthenticatedUser | null ): FrameworkRequest { const { auth, params, payload, query } = req; @@ -205,5 +222,6 @@ export function wrapRequest( params, payload, query, + user, }; } diff --git a/x-pack/legacy/plugins/siem/server/lib/framework/types.ts b/x-pack/legacy/plugins/siem/server/lib/framework/types.ts index dd31ed9fcaf5fa..27254284b577d8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/framework/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/framework/types.ts @@ -8,7 +8,8 @@ import { IndicesGetMappingParams } from 'elasticsearch'; import { GraphQLSchema } from 'graphql'; import { RequestAuth } from 'hapi'; -import { RequestHandlerContext } from 'src/core/server'; +import { RequestHandlerContext } from '../../../../../../../src/core/server'; +import { AuthenticatedUser } from '../../../../../../plugins/security/common/model'; import { ESQuery } from '../../../common/typed_json'; import { PaginationInput, @@ -51,6 +52,7 @@ export interface FrameworkRequest { if (noteId == null) { savedNote.created = new Date().valueOf(); - savedNote.createdBy = getOr(null, 'credentials.username', userInfo); + savedNote.createdBy = userInfo?.username ?? UNAUTHENTICATED_USER; savedNote.updated = new Date().valueOf(); - savedNote.updatedBy = getOr(null, 'credentials.username', userInfo); + savedNote.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER; } else if (noteId != null) { savedNote.updated = new Date().valueOf(); - savedNote.updatedBy = getOr(null, 'credentials.username', userInfo); + savedNote.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER; } return savedNote; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/pinned_event/saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/pinned_event/saved_object.ts index 177eb05d8539d7..afa3595a09e1c6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/pinned_event/saved_object.ts +++ b/x-pack/legacy/plugins/siem/server/lib/pinned_event/saved_object.ts @@ -5,15 +5,15 @@ */ import { failure } from 'io-ts/lib/PathReporter'; -import { RequestAuth } from 'hapi'; import { getOr } from 'lodash/fp'; - -import { SavedObjectsFindOptions } from 'src/core/server'; - import { pipe } from 'fp-ts/lib/pipeable'; import { map, fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { FrameworkRequest, internalFrameworkRequest } from '../framework'; + +import { SavedObjectsFindOptions } from '../../../../../../../src/core/server'; +import { AuthenticatedUser } from '../../../../../../plugins/security/common/model'; +import { UNAUTHENTICATED_USER } from '../../../common/constants'; +import { FrameworkRequest } from '../framework'; import { PinnedEventSavedObject, PinnedEventSavedObjectRuntimeType, @@ -103,7 +103,7 @@ export class PinnedEvent { const timelineResult = convertSavedObjectToSavedTimeline( await savedObjectsClient.create( timelineSavedObjectType, - pickSavedTimeline(null, {}, request[internalFrameworkRequest].auth || null) + pickSavedTimeline(null, {}, request.user || null) ) ); timelineId = timelineResult.savedObjectId; // eslint-disable-line no-param-reassign @@ -125,11 +125,7 @@ export class PinnedEvent { return convertSavedObjectToSavedPinnedEvent( await savedObjectsClient.create( pinnedEventSavedObjectType, - pickSavedPinnedEvent( - pinnedEventId, - savedPinnedEvent, - request[internalFrameworkRequest].auth || null - ) + pickSavedPinnedEvent(pinnedEventId, savedPinnedEvent, request.user || null) ), timelineVersionSavedObject != null ? timelineVersionSavedObject : undefined ); @@ -211,17 +207,18 @@ const convertSavedObjectToSavedPinnedEvent = ( const pickSavedPinnedEvent = ( pinnedEventId: string | null, savedPinnedEvent: SavedPinnedEvent, - userInfo: RequestAuth + userInfo: AuthenticatedUser | null // eslint-disable-next-line @typescript-eslint/no-explicit-any ): any => { + const dateNow = new Date().valueOf(); if (pinnedEventId == null) { - savedPinnedEvent.created = new Date().valueOf(); - savedPinnedEvent.createdBy = getOr(null, 'credentials.username', userInfo); - savedPinnedEvent.updated = new Date().valueOf(); - savedPinnedEvent.updatedBy = getOr(null, 'credentials.username', userInfo); + savedPinnedEvent.created = dateNow; + savedPinnedEvent.createdBy = userInfo?.username ?? UNAUTHENTICATED_USER; + savedPinnedEvent.updated = dateNow; + savedPinnedEvent.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER; } else if (pinnedEventId != null) { - savedPinnedEvent.updated = new Date().valueOf(); - savedPinnedEvent.updatedBy = getOr(null, 'credentials.username', userInfo); + savedPinnedEvent.updated = dateNow; + savedPinnedEvent.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER; } return savedPinnedEvent; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/pick_saved_timeline.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/pick_saved_timeline.ts index 9e4916e201d449..5b60086ae81b65 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/pick_saved_timeline.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/pick_saved_timeline.ts @@ -4,24 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestAuth } from 'hapi'; -import { getOr } from 'lodash/fp'; +import { AuthenticatedUser } from '../../../../../../plugins/security/common/model'; +import { UNAUTHENTICATED_USER } from '../../../common/constants'; import { SavedTimeline } from './types'; export const pickSavedTimeline = ( timelineId: string | null, savedTimeline: SavedTimeline, - userInfo: RequestAuth + userInfo: AuthenticatedUser | null // eslint-disable-next-line @typescript-eslint/no-explicit-any ): any => { + const dateNow = new Date().valueOf(); if (timelineId == null) { - savedTimeline.created = new Date().valueOf(); - savedTimeline.createdBy = getOr(null, 'credentials.username', userInfo); - savedTimeline.updated = new Date().valueOf(); - savedTimeline.updatedBy = getOr(null, 'credentials.username', userInfo); + savedTimeline.created = dateNow; + savedTimeline.createdBy = userInfo?.username ?? UNAUTHENTICATED_USER; + savedTimeline.updated = dateNow; + savedTimeline.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER; } else if (timelineId != null) { - savedTimeline.updated = new Date().valueOf(); - savedTimeline.updatedBy = getOr(null, 'credentials.username', userInfo); + savedTimeline.updated = dateNow; + savedTimeline.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER; } return savedTimeline; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object.ts index 04c1584f1204d5..4b78a7bd3d06d1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object.ts @@ -6,8 +6,8 @@ import { getOr } from 'lodash/fp'; -import { SavedObjectsFindOptions } from 'src/core/server'; - +import { SavedObjectsFindOptions } from '../../../../../../../src/core/server'; +import { UNAUTHENTICATED_USER } from '../../../common/constants'; import { ResponseTimeline, PageInfoTimeline, @@ -15,16 +15,15 @@ import { ResponseFavoriteTimeline, TimelineResult, } from '../../graphql/types'; -import { FrameworkRequest, internalFrameworkRequest } from '../framework'; +import { FrameworkRequest } from '../framework'; +import { Note } from '../note/saved_object'; import { NoteSavedObject } from '../note/types'; import { PinnedEventSavedObject } from '../pinned_event/types'; - -import { SavedTimeline, TimelineSavedObject } from './types'; -import { Note } from '../note/saved_object'; import { PinnedEvent } from '../pinned_event/saved_object'; -import { timelineSavedObjectType } from './saved_object_mappings'; -import { pickSavedTimeline } from './pick_saved_timeline'; import { convertSavedObjectToSavedTimeline } from './convert_saved_object_to_savedtimeline'; +import { pickSavedTimeline } from './pick_saved_timeline'; +import { timelineSavedObjectType } from './saved_object_mappings'; +import { SavedTimeline, TimelineSavedObject } from './types'; interface ResponseTimelines { timeline: TimelineSavedObject[]; @@ -68,8 +67,8 @@ export class Timeline { request: FrameworkRequest, timelineId: string | null ): Promise { - const userName = getOr(null, 'credentials.username', request[internalFrameworkRequest].auth); - const fullName = getOr(null, 'credentials.fullname', request[internalFrameworkRequest].auth); + const userName = request.user?.username ?? UNAUTHENTICATED_USER; + const fullName = request.user?.full_name ?? ''; try { let timeline: SavedTimeline = {}; if (timelineId != null) { @@ -149,11 +148,7 @@ export class Timeline { timeline: convertSavedObjectToSavedTimeline( await savedObjectsClient.create( timelineSavedObjectType, - pickSavedTimeline( - timelineId, - timeline, - request[internalFrameworkRequest].auth || null - ) + pickSavedTimeline(timelineId, timeline, request.user) ) ), }; @@ -162,7 +157,7 @@ export class Timeline { await savedObjectsClient.update( timelineSavedObjectType, timelineId, - pickSavedTimeline(timelineId, timeline, request[internalFrameworkRequest].auth || null), + pickSavedTimeline(timelineId, timeline, request.user), { version: version || undefined, } @@ -217,7 +212,7 @@ export class Timeline { } private async getSavedTimeline(request: FrameworkRequest, timelineId: string) { - const userName = getOr(null, 'credentials.username', request[internalFrameworkRequest].auth); + const userName = request.user?.username ?? UNAUTHENTICATED_USER; const savedObjectsClient = request.context.core.savedObjects.client; const savedObject = await savedObjectsClient.get(timelineSavedObjectType, timelineId); @@ -234,7 +229,7 @@ export class Timeline { } private async getAllSavedTimeline(request: FrameworkRequest, options: SavedObjectsFindOptions) { - const userName = getOr(null, 'credentials.username', request[internalFrameworkRequest].auth); + const userName = request.user?.username ?? UNAUTHENTICATED_USER; const savedObjectsClient = request.context.core.savedObjects.client; if (options.searchFields != null && options.searchFields.includes('favorite.keySearch')) { options.search = `${options.search != null ? options.search : ''} ${ diff --git a/x-pack/legacy/plugins/siem/server/plugin.ts b/x-pack/legacy/plugins/siem/server/plugin.ts index 25fa3bd6cde4cb..8a47aa2a27082b 100644 --- a/x-pack/legacy/plugins/siem/server/plugin.ts +++ b/x-pack/legacy/plugins/siem/server/plugin.ts @@ -6,6 +6,7 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, PluginInitializerContext, Logger } from 'src/core/server'; +import { PluginSetupContract as SecurityPlugin } from '../../../../plugins/security/server'; import { PluginSetupContract as FeaturesSetupContract } from '../../../../plugins/features/server'; import { initServer } from './init_server'; import { compose } from './lib/compose/kibana'; @@ -15,8 +16,11 @@ import { timelineSavedObjectType, } from './saved_objects'; +export type SiemPluginSecurity = Pick; + export interface PluginsSetup { features: FeaturesSetupContract; + security: SiemPluginSecurity; } export class Plugin { @@ -33,7 +37,6 @@ export class Plugin { public setup(core: CoreSetup, plugins: PluginsSetup) { this.logger.debug('Shim plugin setup'); - plugins.features.registerFeature({ id: this.name, name: i18n.translate('xpack.siem.featureRegistry.linkSiemTitle', { @@ -75,7 +78,7 @@ export class Plugin { }, }); - const libs = compose(core, this.context.env); + const libs = compose(core, plugins, this.context.env); initServer(libs); } } diff --git a/x-pack/plugins/reporting/index.d.ts b/x-pack/plugins/reporting/index.d.ts index 65cdfa09fc4c6d..9559de4a5bb033 100644 --- a/x-pack/plugins/reporting/index.d.ts +++ b/x-pack/plugins/reporting/index.d.ts @@ -7,7 +7,7 @@ import { CoreSetup, CoreStart, - HttpServiceBase, + HttpSetup, Plugin, PluginInitializerContext, NotificationsStart, @@ -16,7 +16,7 @@ import { export type JobId = string; export type JobStatus = 'completed' | 'pending' | 'processing' | 'failed'; -export type HttpService = HttpServiceBase; +export type HttpService = HttpSetup; export type NotificationsService = NotificationsStart; export interface SourceJob { diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts index 1c669b7899ec39..aeba2ca5406b81 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts @@ -5,7 +5,7 @@ */ import sinon, { stub } from 'sinon'; -import { HttpServiceBase, NotificationsStart } from '../../../../../src/core/public'; +import { HttpSetup, NotificationsStart } from '../../../../../src/core/public'; import { SourceJob, JobSummary, HttpService } from '../../index.d'; import { JobQueue } from './job_queue'; import { ReportingNotifierStreamHandler } from './stream_handler'; @@ -57,7 +57,7 @@ const httpMock: HttpService = ({ basePath: { prepend: stub(), }, -} as unknown) as HttpServiceBase; +} as unknown) as HttpSetup; const mockShowDanger = stub(); const mockShowSuccess = stub(); diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 6749c11c77036a..61e7ab3fe46392 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -77,6 +77,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`), `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`, `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'actions')}`, + `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'task_manager')}`, `--server.xsrf.whitelist=${JSON.stringify(getAllExternalServiceSimulatorPaths())}`, ...(ssl ? [ diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/task_manager/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/task_manager/index.ts new file mode 100644 index 00000000000000..3bfad59b71166a --- /dev/null +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/task_manager/index.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +const taskManagerQuery = (...filters: any[]) => ({ + bool: { + filter: { + bool: { + must: filters, + }, + }, + }, +}); + +const tasksForAlerting = { + term: { + 'task.scope': 'alerting', + }, +}; +const taskByIdQuery = (id: string) => ({ + ids: { + values: [`task:${id}`], + }, +}); + +// eslint-disable-next-line import/no-default-export +export default function(kibana: any) { + return new kibana.Plugin({ + name: 'taskManagerHelpers', + require: ['elasticsearch', 'task_manager'], + + config(Joi: any) { + return Joi.object({ + enabled: Joi.boolean().default(true), + }).default(); + }, + + init(server: any) { + const taskManager = server.plugins.task_manager; + + server.route({ + path: '/api/alerting_tasks/{taskId}', + method: 'GET', + async handler(request: any) { + try { + return taskManager.fetch({ + query: taskManagerQuery(tasksForAlerting, taskByIdQuery(request.params.taskId)), + }); + } catch (err) { + return err; + } + }, + }); + }, + }); +} diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/task_manager/package.json b/x-pack/test/alerting_api_integration/common/fixtures/plugins/task_manager/package.json new file mode 100644 index 00000000000000..532b597829561c --- /dev/null +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/task_manager/package.json @@ -0,0 +1,12 @@ +{ + "name": "alerting_task_plugin", + "version": "1.0.0", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "dependencies": { + "joi": "^13.5.2" + } +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts index 89c5b35bc0904b..8cb01b5467388a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; +import { Response as SupertestResponse } from 'supertest'; import { UserAtSpaceScenarios } from '../../scenarios'; import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; @@ -13,6 +14,14 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function createUpdateTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); + const retry = getService('retry'); + + function getAlertingTaskById(taskId: string) { + return supertest + .get(`/api/alerting_tasks/${taskId}`) + .expect(200) + .then((response: SupertestResponse) => response.body); + } describe('update', () => { const objectRemover = new ObjectRemover(supertest); @@ -318,7 +327,75 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); + + it('should handle updates to an alert schedule by rescheduling the underlying task', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + schedule: { interval: '30m' }, + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert'); + + await retry.try(async () => { + const alertTask = (await getAlertingTaskById(createdAlert.scheduledTaskId)).docs[0]; + expect(alertTask.status).to.eql('idle'); + // ensure the alert inital run has completed and it's been rescheduled to half an hour from now + ensureDatetimeIsWithinRange(Date.parse(alertTask.runAt), 30 * 60 * 1000); + }); + + const updatedData = { + name: 'bcd', + tags: ['bar'], + params: { + foo: true, + }, + schedule: { interval: '1m' }, + actions: [], + throttle: '2m', + }; + const response = await supertestWithoutAuth + .put(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .send(updatedData); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'global_read at space1': + expect(response.statusCode).to.eql(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + expect(response.statusCode).to.eql(200); + await retry.try(async () => { + const alertTask = (await getAlertingTaskById(createdAlert.scheduledTaskId)).docs[0]; + expect(alertTask.status).to.eql('idle'); + // ensure the alert is rescheduled to a minute from now + ensureDatetimeIsWithinRange(Date.parse(alertTask.runAt), 60 * 1000); + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); }); } }); } + +function ensureDatetimeIsWithinRange(scheduledRunTime: number, expectedDiff: number) { + const buffer = 10000; + const diff = scheduledRunTime - Date.now(); + expect(diff).to.be.greaterThan(expectedDiff - buffer); + expect(diff).to.be.lessThan(expectedDiff + buffer); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts index 35e23a95ca11a8..c61c94bd603fbf 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts @@ -75,6 +75,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { schedule: { interval: '1m' }, scheduledTaskId: response.body.scheduledTaskId, updatedBy: null, + apiKeyOwner: null, throttle: '1m', muteAll: false, mutedInstanceIds: [], diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts index 6864c30134db36..1ee814aace797f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts @@ -48,6 +48,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { actions: [], params: {}, createdBy: null, + apiKeyOwner: null, scheduledTaskId: match.scheduledTaskId, updatedBy: null, throttle: '1m', diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts index 8dabc7eeb101d0..328b0a01d5cbdc 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts @@ -44,6 +44,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { createdBy: null, scheduledTaskId: response.body.scheduledTaskId, updatedBy: null, + apiKeyOwner: null, throttle: '1m', muteAll: false, mutedInstanceIds: [], diff --git a/x-pack/test/functional/apps/infra/link_to.ts b/x-pack/test/functional/apps/infra/link_to.ts index efe60b41badccf..a68637600be8bd 100644 --- a/x-pack/test/functional/apps/infra/link_to.ts +++ b/x-pack/test/functional/apps/infra/link_to.ts @@ -22,7 +22,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { state: undefined, }; const expectedSearchString = - "logFilter=(expression:'trace.id:433b4651687e18be2c6c8e3b11f53d09',kind:kuery)&logPosition=(position:(tiebreaker:0,time:1565707203194))&sourceId=default&_g=()"; + "logFilter=(expression:'trace.id:433b4651687e18be2c6c8e3b11f53d09',kind:kuery)&logPosition=(position:(tiebreaker:0,time:1565707203194))&sourceId=default"; const expectedRedirect = `/logs/stream?${expectedSearchString}`; await pageObjects.common.navigateToActualUrl( diff --git a/x-pack/test/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/joins.js index cbe7c98cd881d0..89a6c6ea82e533 100644 --- a/x-pack/test/functional/apps/maps/joins.js +++ b/x-pack/test/functional/apps/maps/joins.js @@ -18,6 +18,9 @@ const EXPECTED_JOIN_VALUES = { }; const VECTOR_SOURCE_ID = 'n1t6f'; +const CIRCLE_STYLE_LAYER_INDEX = 0; +const FILL_STYLE_LAYER_INDEX = 2; +const LINE_STYLE_LAYER_INDEX = 3; export default function({ getPageObjects, getService }) { const PageObjects = getPageObjects(['maps']); @@ -82,19 +85,21 @@ export default function({ getPageObjects, getService }) { const layersForVectorSource = mapboxStyle.layers.filter(mbLayer => { return mbLayer.id.startsWith(VECTOR_SOURCE_ID); }); + // Color is dynamically obtained from eui source lib - const dynamicColor = layersForVectorSource[0].paint['circle-stroke-color']; + const dynamicColor = + layersForVectorSource[CIRCLE_STYLE_LAYER_INDEX].paint['circle-stroke-color']; //circle layer for points - expect(layersForVectorSource[0]).to.eql( + expect(layersForVectorSource[CIRCLE_STYLE_LAYER_INDEX]).to.eql( _.set(MAPBOX_STYLES.POINT_LAYER, 'paint.circle-stroke-color', dynamicColor) ); //fill layer - expect(layersForVectorSource[1]).to.eql(MAPBOX_STYLES.FILL_LAYER); + expect(layersForVectorSource[FILL_STYLE_LAYER_INDEX]).to.eql(MAPBOX_STYLES.FILL_LAYER); //line layer for borders - expect(layersForVectorSource[2]).to.eql( + expect(layersForVectorSource[LINE_STYLE_LAYER_INDEX]).to.eql( _.set(MAPBOX_STYLES.LINE_LAYER, 'paint.line-color', dynamicColor) ); });