diff --git a/.ci/Jenkinsfile_flaky b/.ci/Jenkinsfile_flaky index 425a5e71798b1..2f496329dfd8e 100644 --- a/.ci/Jenkinsfile_flaky +++ b/.ci/Jenkinsfile_flaky @@ -70,6 +70,8 @@ def getWorkerFromParams(isXpack, job, ciGroup) { "run `node scripts/mocha`" ) }) + } else if (job == 'accessibility') { + return kibanaPipeline.functionalTestProcess('kibana-accessibility', './test/scripts/jenkins_accessibility.sh') } else if (job == 'firefoxSmoke') { return kibanaPipeline.functionalTestProcess('firefoxSmoke', './test/scripts/jenkins_firefox_smoke.sh') } else if(job == 'visualRegression') { @@ -79,7 +81,9 @@ def getWorkerFromParams(isXpack, job, ciGroup) { } } - if (job == 'firefoxSmoke') { + if (job == 'accessibility') { + return kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh') + } else if (job == 'firefoxSmoke') { return kibanaPipeline.functionalTestProcess('xpack-firefoxSmoke', './test/scripts/jenkins_xpack_firefox_smoke.sh') } else if(job == 'visualRegression') { return kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh') diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6df4136ef74af..42bf7662ff2e1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -158,7 +158,6 @@ /x-pack/legacy/plugins/security/ @elastic/kibana-security /x-pack/legacy/plugins/spaces/ @elastic/kibana-security /x-pack/plugins/spaces/ @elastic/kibana-security -/x-pack/legacy/plugins/encrypted_saved_objects/ @elastic/kibana-security /x-pack/plugins/encrypted_saved_objects/ @elastic/kibana-security /x-pack/plugins/security/ @elastic/kibana-security /x-pack/test/api_integration/apis/security/ @elastic/kibana-security diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md index a62cee7b654fe..1d3cfa9305c18 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md @@ -31,6 +31,7 @@ export declare class Field implements IFieldType | [indexPattern](./kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md) | | IndexPattern | | | [lang](./kibana-plugin-plugins-data-public.indexpatternfield.lang.md) | | string | | | [name](./kibana-plugin-plugins-data-public.indexpatternfield.name.md) | | string | | +| [readFromDocValues](./kibana-plugin-plugins-data-public.indexpatternfield.readfromdocvalues.md) | | boolean | | | [script](./kibana-plugin-plugins-data-public.indexpatternfield.script.md) | | string | | | [scripted](./kibana-plugin-plugins-data-public.indexpatternfield.scripted.md) | | boolean | | | [searchable](./kibana-plugin-plugins-data-public.indexpatternfield.searchable.md) | | boolean | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.readfromdocvalues.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.readfromdocvalues.md new file mode 100644 index 0000000000000..4b012c26a8620 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.readfromdocvalues.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [readFromDocValues](./kibana-plugin-plugins-data-public.indexpatternfield.readfromdocvalues.md) + +## IndexPatternField.readFromDocValues property + +Signature: + +```typescript +readFromDocValues?: boolean; +``` diff --git a/docs/images/lens_viz_types.png b/docs/images/lens_viz_types.png new file mode 100644 index 0000000000000..fb3961ad8bb28 Binary files /dev/null and b/docs/images/lens_viz_types.png differ diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc index df11f5f03a7de..6f691f2715bc8 100644 --- a/docs/user/alerting/index.asciidoc +++ b/docs/user/alerting/index.asciidoc @@ -160,7 +160,7 @@ If you are using an *on-premises* Elastic Stack deployment: If you are using an *on-premises* Elastic Stack deployment with <>: -* Transport Layer Security (TLS) must be configured for communication <>. {kib} alerting uses <> to secure background alert checks and actions, and API keys require {ref}/configuring-tls.html#tls-http[TLS on the HTTP interface]. +* You must enable Transport Layer Security (TLS) for communication <>. {kib} alerting uses <> to secure background alert checks and actions, and API keys require {ref}/configuring-tls.html#tls-http[TLS on the HTTP interface]. A proxy will not suffice. [float] [[alerting-security]] diff --git a/docs/user/dashboard.asciidoc b/docs/user/dashboard.asciidoc index 301efb2dfe2c0..1614f00f37ac7 100644 --- a/docs/user/dashboard.asciidoc +++ b/docs/user/dashboard.asciidoc @@ -160,7 +160,7 @@ When you're finished adding and arranging the panels, save the dashboard. . Enter the dashboard *Title* and optional *Description*, then *Save* the dashboard. [[sharing-dashboards]] -=== Share the dashboard +== Share the dashboard [[embedding-dashboards]] Share your dashboard outside of {kib}. diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index bcaede01b7a86..1704a80847652 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -1,123 +1,171 @@ [[management]] -= Management += Stack Management [partintro] -- -*Management* is home to UIs for managing all things Elastic Stack— +*Stack Management* is home to UIs for managing all things Elastic Stack— indices, clusters, licenses, UI settings, index patterns, spaces, and more. [float] -[[manage-Elasticsearch]] -== Manage {es} +[[manage-ingest]] +== Ingest [cols="50, 50"] |=== -a| <> - -Replicate indices on a remote cluster and copy them to a follower index on a local cluster. -This is important for -disaster recovery. It also keeps data local for faster queries. - -| <> - -Create a policy for defining the lifecycle of an index as it ages -through the hot, warm, cold, and delete phases. -Such policies help you control operation costs -because you can put data in different resource tiers. +| <> +| Create and manage {es} +pipelines that enable you to perform common transformations and +enrichments on your data. -a| <> +| {logstash-ref}/logstash-centralized-pipeline-management.html[Logstash Pipelines] +| Create, edit, and delete your Logstash pipeline configurations. -View index settings, mappings, and statistics and perform operations, such as refreshing, -flushing, and clearing the cache. Practicing good index management ensures -that your data is stored cost effectively. +| <> +| Manage your Beats configurations in a central location and +quickly deploy configuration changes to all Beats running across your enterprise. -a| <> -Create and manage {es} -pipelines that enable you to perform common transformations and -enrichments on your data. +|=== -| <> +[float] +[[manage-data]] +== Data -View the status of your license, start a trial, or install a new license. For -the full list of features that are included in your license, -see the https://www.elastic.co/subscriptions[subscription page]. +[cols="50, 50"] +|=== -| <> +a| <> +| View index settings, mappings, and statistics and perform operations, such as refreshing, +flushing, and clearing the cache. Practicing good index management ensures +that your data is stored cost effectively. -Manage your remote clusters for use with cross-cluster search and cross-cluster replication. -You can add and remove remote clusters, and check their connectivity. +| <> +|Create a policy for defining the lifecycle of an index as it ages +through the hot, warm, cold, and delete phases. +Such policies help you control operation costs +because you can put data in different resource tiers. -| <> +| <> +|Define a policy that creates, schedules, and automatically deletes snapshots to ensure that you +have backups of your cluster in case something goes wrong. -Create a job that periodically aggregates data from one or more indices, and then +| <> +|Create a job that periodically aggregates data from one or more indices, and then rolls it into a new, compact index. Rollup indices are a good way to store months or years of historical data in combination with your raw data. -| <> +| {ref}/transforms.html[Transforms] +|Use transforms to pivot existing {es} indices into summarized or entity-centric indices. -Define a policy that creates, schedules, and automatically deletes snapshots to ensure that you -have backups of your cluster in case something goes wrong. +| <> +|Replicate indices on a remote cluster and copy them to a follower index on a local cluster. +This is important for +disaster recovery. It also keeps data local for faster queries. -| {ref}/transforms.html[*Transforms*] +| <> +|Manage your remote clusters for use with cross-cluster search and cross-cluster replication. +You can add and remove remote clusters, and check their connectivity. +|=== -Use transforms to pivot existing {es} indices into summarized or entity-centric indices. +[float] +[[manage-alerts-insights]] +== Alerts and Insights -| <> +[cols="50, 50"] +|=== -Identify the issues that you need to address before upgrading to the -next major version of {es}, and then reindex, if needed. +| <> +| Centrally manage your alerts across {kib}. Create and manage reusable +connectors for triggering actions. -| <> +| <> +| Monitor the generation of reports—PDF, PNG, and CSV—and download reports that you previously generated. +A report can contain a dashboard, visualization, saved search, or Canvas workpad. + +| {ml-docs}/ml-jobs.html[Machine Learning Jobs] +| View your {anomaly-jobs} and {dfanalytics-jobs}. Open the Single Metric +Viewer or Anomaly Explorer to see your {ml} results. -Detect changes in your data by creating, managing, and monitoring alerts. -For example, create an alert when the maximum total CPU usage on a machine goes +| <> +| Detect changes in your data by creating, managing, and monitoring alerts. +For example, you might create an alert when the maximum total CPU usage on a machine goes above a certain percentage. |=== [float] -[[manage-kibana]] -== Manage {kib} +[[manage-security]] +== Security [cols="50, 50"] |=== -a| <> - -Customize {kib} to suit your needs. Change the format for displaying dates, turn on dark mode, -set the timespan for notification messages, and much more. +a| <> +|View the users that have been defined on your cluster. +Add or delete users and assign roles that give users +specific privileges. -| <> +| <> +|View the roles that exist on your cluster. Customize +the actions that a user with the role can perform, on a cluster, index, and space level. -Centrally manage your alerts across {kib}. Create and manage reusable -connectors for triggering actions. +| <> +| Create secondary credentials so that you can send requests on behalf of the user. +Secondary credentials have the same or lower access rights. -| <> +| <> +| Assign roles to your users using a set of rules. Role mappings are required +when authenticating via an external identity provider, such as Active Directory, +Kerberos, PKI, OIDC, and SAML. -Create and manage the index patterns that help you retrieve your data from {es}. +|=== -| <> +[float] +[[manage-kibana]] +== {kib} -Monitor the generation of reports—PDF, PNG, and CSV—and download reports that you previously generated. -A report can contain a dashboard, visualization, saved search, or Canvas workpad. +[cols="50, 50"] +|=== -| <> +a| <> +|Create and manage the index patterns that retrieve your data from {es}. -Copy, edit, delete, import, and export your saved objects. +| <> +| Copy, edit, delete, import, and export your saved objects. These include dashboards, visualizations, maps, index patterns, Canvas workpads, and more. -| <> - -Create spaces to organize your dashboards and other saved objects into categories. +| <> +| Create spaces to organize your dashboards and other saved objects into categories. A space is isolated from all other spaces, so you can tailor it to your needs without impacting others. -|   +a| <> +| Customize {kib} to suit your needs. Change the format for displaying dates, turn on dark mode, +set the timespan for notification messages, and much more. + +|=== + +[float] +[[manage-stack]] +== Stack + +[cols="50, 50"] +|=== + +| <> +| View the status of your license, start a trial, or install a new license. For +the full list of features that are included in your license, +see the https://www.elastic.co/subscriptions[subscription page]. + +| <> +| Identify the issues that you need to address before upgrading to the +next major version of {es}, and then reindex, if needed. |=== + + -- include::{kib-repo-dir}/management/advanced-options.asciidoc[] diff --git a/docs/user/security/authorization/index.asciidoc b/docs/user/security/authorization/index.asciidoc index 853c735418cea..4b91812660c78 100644 --- a/docs/user/security/authorization/index.asciidoc +++ b/docs/user/security/authorization/index.asciidoc @@ -2,16 +2,17 @@ [[xpack-security-authorization]] === Granting access to {kib} -The Elastic Stack comes with the `kibana_admin` {ref}/built-in-roles.html[built-in role], which you can use to grant access to all Kibana features in all spaces. To grant users access to a subset of spaces or features, you can create a custom role that grants the desired Kibana privileges. +The Elastic Stack comes with the `kibana_admin` {ref}/built-in-roles.html[built-in role], which you can use to grant access to all Kibana features in all spaces. To grant users access to a subset of spaces or features, you can create a custom role that grants the desired Kibana privileges. When you assign a user multiple roles, the user receives a union of the roles’ privileges. Therefore, assigning the `kibana_admin` role in addition to a custom role that grants Kibana privileges is ineffective because `kibana_admin` has access to all the features in all spaces. NOTE: When running multiple tenants of Kibana by changing the `kibana.index` in your `kibana.yml`, you cannot use `kibana_admin` to grant access. You must create custom roles that authorize the user for that specific tenant. Although multi-tenant installations are supported, the recommended approach to securing access to Kibana segments is to grant users access to specific spaces. [role="xpack"] +[[xpack-kibana-role-management]] === {kib} role management -To create a role that grants {kib} privileges, go to **Management -> Security -> Roles** and click **Create role**. +To create a role that grants {kib} privileges, go to **Management -> Security -> Roles** and click **Create role**. [[adding_kibana_privileges]] ==== Adding {kib} privileges @@ -63,7 +64,7 @@ Features are available to users when their roles grant access to the features, * Using the same role, it’s possible to assign different privileges to different spaces. After you’ve added space privileges, click **Add space privilege**. If you’ve already added privileges for either *** Global (all spaces)** or an individual space, you will not be able to select these in the **Spaces** selection control. -Additionally, if you’ve already assigned privileges at *** Global (all spaces)**, you are only able to assign additional privileges to individual spaces. Similar to the behavior of multiple roles granting the union of all privileges, space privileges are also a union. If you’ve already granted the user the **All** privilege at *** Global (all spaces)**, you’re not able to restrict the role to only the **Read** privilege at an individual space. +Additionally, if you’ve already assigned privileges at *** Global (all spaces)**, you are only able to assign additional privileges to individual spaces. Similar to the behavior of multiple roles granting the union of all privileges, space privileges are also a union. If you’ve already granted the user the **All** privilege at *** Global (all spaces)**, you’re not able to restrict the role to only the **Read** privilege at an individual space. ==== Privilege summary @@ -111,4 +112,3 @@ image::user/security/images/privilege-example-2.png[Privilege example 2] [role="screenshot"] image::user/security/images/privilege-example-3.png[Privilege example 3] - diff --git a/docs/visualize/lens.asciidoc b/docs/visualize/lens.asciidoc index 422afbb201183..38ccb7878a92b 100644 --- a/docs/visualize/lens.asciidoc +++ b/docs/visualize/lens.asciidoc @@ -4,60 +4,51 @@ beta[] -*Lens* provides you with a simple and fast way to create visualizations from your {es} data. With Lens, you can: +*Lens* is a simple and fast way to create visualizations of your {es} data. With *Lens*, +you drag and drop your data fields onto the visualization builder pane, and *Lens* automatically generates +a visualization that best displays your data. -* Quickly build visualizations by dragging and dropping data fields. +With Lens, you can: -* Understand your data with a summary view on each field. +* Explore your data in just a few clicks. -* Easily change the visualization type by selecting the automatically generated visualization suggestions. +* Create visualizations with multiple layers and indices. -* Save your visualization for use in a dashboard. +* Use the automatically generated visualization suggestions to change the visualization type. -[float] -[[drag-drop]] -=== Drag and drop - -The panel shows the data fields for the selected time period. When -you drag a field from the data panel, Lens highlights where you can drop that field. The first time you drag a data field, -you'll see two places highlighted in green: - -* The visualization builder pane +* Add your visualizations to dashboards and Canvas workpads. -* The *X-axis* or *Y-axis* fields - -You can incorporate many fields into your visualization, and Lens uses heuristics to decide how -to apply each one to the visualization. +To get started with *Lens*, click a field in the data panel, then drag and drop the field on a highlighted area. [role="screenshot"] image::images/lens_drag_drop.gif[] -TIP: Drag-and-drop capabilities are available only when Lens knows how to use the data. You can still customize -your visualization if Lens is unable to make a suggestion. +You can incorporate many fields into your visualization, and Lens uses heuristics to decide how to apply each one to the visualization. + +TIP: Drag-and-drop capabilities are available only when Lens knows how to use the data. If *Lens* is unable to automatically generate a visualization, +you can still configure the customization options for your visualization. [float] [[apply-lens-filters]] -==== Find the right data +==== Filter the data panel fields -Lens shows you fields based on the <> you have defined in -{kib}, and the current time range. When you change the index pattern or time filter, -the list of fields are updated. +The fields in the data panel based on your selected <>, and the <>. -To narrow the list of fields, you can: +To change the index pattern, click it, then select a new one. The fields in the data panel automatically update. -* Enter the field name in *Search field names*. +To filter the fields in the data panel: -* Click *Filter by type*, then select the filter. You can also select *Only show fields with data* -to show the full list of fields from the index pattern. +* Enter the name in *Search field names*. + +* Click *Filter by type*, then select the filter. To show all of the fields in the index pattern, deselect *Only show fields with data*. [float] [[view-data-summaries]] ==== Data summaries -To help you decide exactly the data you want to display, get a quick summary of each data field. -The summary shows the distribution of values in the time range. +To help you decide exactly the data you want to display, get a quick summary of each field. The summary shows the distribution of values in the time range. -To view the data information, navigate to a data field, then click *i*. +To view the field summary information, navigate to the field, then click *i*. [role="screenshot"] image::images/lens_data_info.png[] @@ -66,46 +57,40 @@ image::images/lens_data_info.png[] [[change-the-visualization-type]] ==== Change the visualization type -With Lens, you are no longer required to build each visualization from scratch. Lens allows -you to switch between any supported chart type at any time. Lens also provides -suggestions, which are shortcuts to alternate visualizations based on the data you have. +*Lens* enables you to switch between any supported visualization type at any time. -You can switch between suggestions without losing your previous state: +*Suggestions* are shortcuts to alternate visualizations that *Lens* generates for you. [role="screenshot"] image::images/lens_suggestions.gif[] -If you want to switch to a chart type that is not suggested, click the chart type, -then select a chart type. When there is an exclamation point (!) -next to a chart type, Lens is unable to transfer your current data, but +If you'd like to use a visualization type that is not suggested, click the visualization type, +then select a new one. + +[role="screenshot"] +image::images/lens_viz_types.png[] + +When there is an exclamation point (!) +next to a visualization type, Lens is unable to transfer your data, but still allows you to make the change. [float] [[customize-operation]] -==== Customize the data for your visualization +==== Change the aggregation and labels Lens allows some customizations of the data for each visualization. -. Click the index pattern name, then select the new index pattern. -+ -If there is a match, Lens displays the new data. All fields that do not match the index pattern are removed. - -. Change the data field options, such as the aggregation or label. - -.. Click *Drop a field here* or the field name in the column. +. Click *Drop a field here* or the field name in the column. -.. Change the options that appear depending on the type of field. +. Change the options that appear depending on the type of field. [float] [[layers]] -==== Layers in bar, line, and area charts +==== Add layers and indices -The bar, line, and area charts allow you to layer two different series. To add a layer, click *+*. +Bar, line, and area charts allow you to visualize multiple data layers and indices so that you can compare and analyze data from multiple sources. -To remove a layer, click the chart icon next to the index name: - -[role="screenshot"] -image::images/lens_remove_layer.png[] +To add a layer, click *+*, then drag and drop the fields for the new layer. To view a different index, click it, then select a new one. [float] [[lens-tutorial]] @@ -125,50 +110,48 @@ To start, you'll need to add the <>. Drag and drop your data onto the visualization builder pane. -. Open *Visualize*, then click *Create visualization*. +. From the menu, click *Visualize*, then click *Create visualization*. . On the *New Visualization* window, click *Lens*. -. Select the *kibana_sample_data_ecommerce* index. +. Select the *kibana_sample_data_ecommerce* index pattern. -. Click image:images/time-filter-calendar.png[], then click *Last 7 days*. The list of data fields are updated. +. Click image:images/time-filter-calendar.png[], then click *Last 7 days*. ++ +The fields in the data panel update. . Drag and drop the *taxful_total_price* data field to the visualization builder pane. + [role="screenshot"] image::images/lens_tutorial_1.png[Lens tutorial] -Lens has taken your intent to see *taxful_total_price* and added in the *order_date* field to show -average order prices over time. +To display the average order prices over time, *Lens* automatically added in *order_date* field. To break down your data, drag the *category.keyword* field to the visualization builder pane. Lens -understands that you want to show the top categories and compare them across the dates, -and creates a chart that compares the sales for each of the top 3 categories: +knows that you want to show the top categories and compare them across the dates, +and creates a chart that compares the sales for each of the top three categories: [role="screenshot"] image::images/lens_tutorial_2.png[Lens tutorial] [float] [[customize-lens-visualization]] -==== Further customization +==== Customize your visualization -Customize your visualization to look exactly how you want. +Make your visualization look exactly how you want with the customization options. . Click *Average of taxful_total_price*. -.. Change the *Label* to `Sales`, or a name that you prefer for the data. +.. Change the *Label* to `Sales`. . Click *Top values of category.keyword*. -.. Increase *Number of values* to `10`. The visualization updates in the background to show there are only +.. Change *Number of values* to `10`. The visualization updates to show there are only six available categories. ++ +Look at the *Suggestions*. An area chart is not an option, but for sales data, a stacked area chart might be the best option. -. Look at the suggestions. None of them show an area chart, but for sales data, a stacked area chart -might make sense. To switch the chart type: - -.. Click *Stacked bar chart* in the column. - -.. Click *Stacked area*. +. To switch the chart type, click *Stacked bar chart* in the column, then click *Stacked area* from the *Select a visualizations* window. + [role="screenshot"] image::images/lens_tutorial_3.png[Lens tutorial] @@ -177,6 +160,6 @@ image::images/lens_tutorial_3.png[Lens tutorial] [[lens-tutorial-next-steps]] ==== Next steps -Now that you've created your visualization in Lens, you can add it to a Dashboard. +Now that you've created your visualization, you can add it to a dashboard or Canvas workpad. -For more information, see <>. +For more information, refer to <> or <>. diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index df1ed3e100923..cadbecb2e9a3f 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -352,7 +352,6 @@ export class LegacyService implements CoreService { uiPlugins: setupDeps.uiPlugins, elasticsearch: setupDeps.core.elasticsearch, rendering: setupDeps.core.rendering, - uiSettings: setupDeps.core.uiSettings, savedObjectsClientProvider: startDeps.core.savedObjects.clientProvider, legacy: this.legacyInternals, }, diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 8226b4e3a57e0..b33528e2a3931 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -247,21 +247,21 @@ export class Server { coreId, 'core', async (context, req, res): Promise => { - const savedObjectsClient = this.coreStart!.savedObjects.getScopedClient(req); - const uiSettingsClient = coreSetup.uiSettings.asScopedToClient(savedObjectsClient); + const coreStart = this.coreStart!; + const savedObjectsClient = coreStart.savedObjects.getScopedClient(req); return { savedObjects: { client: savedObjectsClient, - typeRegistry: this.coreStart!.savedObjects.getTypeRegistry(), + typeRegistry: coreStart.savedObjects.getTypeRegistry(), }, elasticsearch: { legacy: { - client: coreSetup.elasticsearch.dataClient.asScoped(req), + client: coreStart.elasticsearch.legacy.client.asScoped(req), }, }, uiSettings: { - client: uiSettingsClient, + client: coreStart.uiSettings.asScopedToClient(savedObjectsClient), }, }; } diff --git a/src/core/server/ui_settings/types.ts b/src/core/server/ui_settings/types.ts index 076e1de4458d7..a29f16a90daeb 100644 --- a/src/core/server/ui_settings/types.ts +++ b/src/core/server/ui_settings/types.ts @@ -84,11 +84,6 @@ export interface InternalUiSettingsServiceSetup { * @param settings */ register(settings: Record): void; - /** - * Creates uiSettings client with provided *scoped* saved objects client {@link IUiSettingsClient} - * @param savedObjectsClient - */ - asScopedToClient(savedObjectsClient: SavedObjectsClientContract): IUiSettingsClient; } /** @public */ diff --git a/src/core/server/ui_settings/ui_settings_service.mock.ts b/src/core/server/ui_settings/ui_settings_service.mock.ts index cd781e9759b07..83cea6d7ab3e2 100644 --- a/src/core/server/ui_settings/ui_settings_service.mock.ts +++ b/src/core/server/ui_settings/ui_settings_service.mock.ts @@ -46,11 +46,8 @@ const createClientMock = () => { const createSetupMock = () => { const mocked: jest.Mocked = { register: jest.fn(), - asScopedToClient: jest.fn(), }; - mocked.asScopedToClient.mockReturnValue(createClientMock()); - return mocked; }; diff --git a/src/core/server/ui_settings/ui_settings_service.test.ts b/src/core/server/ui_settings/ui_settings_service.test.ts index 08400f56ad281..ebcb0cf1d762f 100644 --- a/src/core/server/ui_settings/ui_settings_service.test.ts +++ b/src/core/server/ui_settings/ui_settings_service.test.ts @@ -67,34 +67,6 @@ describe('uiSettings', () => { expect(setupDeps.savedObjects.registerType).toHaveBeenCalledWith(uiSettingsType); }); - describe('#asScopedToClient', () => { - it('passes saved object type "config" to UiSettingsClient', async () => { - const setup = await service.setup(setupDeps); - setup.asScopedToClient(savedObjectsClient); - expect(MockUiSettingsClientConstructor).toBeCalledTimes(1); - expect(MockUiSettingsClientConstructor.mock.calls[0][0].type).toBe('config'); - }); - - it('passes overrides to UiSettingsClient', async () => { - const setup = await service.setup(setupDeps); - setup.asScopedToClient(savedObjectsClient); - expect(MockUiSettingsClientConstructor).toBeCalledTimes(1); - expect(MockUiSettingsClientConstructor.mock.calls[0][0].overrides).toBe(overrides); - expect(MockUiSettingsClientConstructor.mock.calls[0][0].overrides).toEqual(overrides); - }); - - it('passes a copy of set defaults to UiSettingsClient', async () => { - const setup = await service.setup(setupDeps); - - setup.register(defaults); - setup.asScopedToClient(savedObjectsClient); - expect(MockUiSettingsClientConstructor).toBeCalledTimes(1); - - expect(MockUiSettingsClientConstructor.mock.calls[0][0].defaults).toEqual(defaults); - expect(MockUiSettingsClientConstructor.mock.calls[0][0].defaults).not.toBe(defaults); - }); - }); - describe('#register', () => { it('throws if registers the same key twice', async () => { const setup = await service.setup(setupDeps); diff --git a/src/core/server/ui_settings/ui_settings_service.ts b/src/core/server/ui_settings/ui_settings_service.ts index 83e66cf6dd06d..93593b29221da 100644 --- a/src/core/server/ui_settings/ui_settings_service.ts +++ b/src/core/server/ui_settings/ui_settings_service.ts @@ -65,7 +65,6 @@ export class UiSettingsService return { register: this.register.bind(this), - asScopedToClient: this.getScopedClientFactory(), }; } diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 02491ff872981..40996500bfbe0 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -113,7 +113,6 @@ export interface KibanaCore { legacy: ILegacyInternals; rendering: LegacyServiceSetupDeps['core']['rendering']; uiPlugins: UiPlugins; - uiSettings: LegacyServiceSetupDeps['core']['uiSettings']; savedObjectsClientProvider: LegacyServiceStartDeps['core']['savedObjects']['clientProvider']; }; env: { diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 52c43c426ed91..10847b9928528 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -247,9 +247,10 @@ export function uiRenderMixin(kbnServer, server, config) { rendering, legacy, savedObjectsClientProvider: savedObjects, - uiSettings: { asScopedToClient }, } = kbnServer.newPlatform.__internals; - const uiSettings = asScopedToClient(savedObjects.getClient(h.request)); + const uiSettings = kbnServer.newPlatform.start.core.uiSettings.asScopedToClient( + savedObjects.getClient(h.request) + ); const vars = await legacy.getVars(app.getId(), h.request, { apmConfig: getApmConfig(app), ...overrides, diff --git a/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts b/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts index bee0d69706ebb..84a64d3f46f11 100644 --- a/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts +++ b/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts @@ -59,6 +59,15 @@ describe('uiSettingsMixin()', () => { decorate: sinon.spy((type: keyof Decorators, name: string, value: any) => { decorations[type][name] = value; }), + newPlatform: { + setup: { + core: { + uiSettings: { + register: sinon.stub(), + }, + }, + }, + }, }; // "promise" returned from kbnServer.ready() @@ -70,13 +79,6 @@ describe('uiSettingsMixin()', () => { server, uiExports: { uiSettingDefaults }, ready: sinon.stub().returns(readyPromise), - newPlatform: { - __internals: { - uiSettings: { - register: sinon.stub(), - }, - }, - }, }; uiSettingsMixin(kbnServer, server); @@ -92,10 +94,10 @@ describe('uiSettingsMixin()', () => { afterEach(() => sandbox.restore()); it('passes uiSettingsDefaults to the new platform', () => { - const { kbnServer } = setup(); - sinon.assert.calledOnce(kbnServer.newPlatform.__internals.uiSettings.register); + const { server } = setup(); + sinon.assert.calledOnce(server.newPlatform.setup.core.uiSettings.register); sinon.assert.calledWithExactly( - kbnServer.newPlatform.__internals.uiSettings.register, + server.newPlatform.setup.core.uiSettings.register, uiSettingDefaults ); }); diff --git a/src/legacy/ui/ui_settings/ui_settings_mixin.js b/src/legacy/ui/ui_settings/ui_settings_mixin.js index accdc4d043d1a..8190b67732dac 100644 --- a/src/legacy/ui/ui_settings/ui_settings_mixin.js +++ b/src/legacy/ui/ui_settings/ui_settings_mixin.js @@ -37,7 +37,7 @@ export function uiSettingsMixin(kbnServer, server) { return acc; }, {}); - kbnServer.newPlatform.__internals.uiSettings.register(mergedUiSettingDefaults); + server.newPlatform.setup.core.uiSettings.register(mergedUiSettingDefaults); server.decorate('server', 'uiSettingsServiceFactory', (options = {}) => { return uiSettingsServiceFactory(server, options); diff --git a/src/legacy/ui/ui_settings/ui_settings_service_factory.ts b/src/legacy/ui/ui_settings/ui_settings_service_factory.ts index ab4eb75e4b703..6c3c50d175dc5 100644 --- a/src/legacy/ui/ui_settings/ui_settings_service_factory.ts +++ b/src/legacy/ui/ui_settings/ui_settings_service_factory.ts @@ -32,5 +32,5 @@ export function uiSettingsServiceFactory( server: Legacy.Server, options: UiSettingsServiceFactoryOptions ): IUiSettingsClient { - return server.newPlatform.__internals.uiSettings.asScopedToClient(options.savedObjectsClient); + return server.newPlatform.start.core.uiSettings.asScopedToClient(options.savedObjectsClient); } diff --git a/src/plugins/data/public/index_patterns/fields/__snapshots__/field.test.ts.snap b/src/plugins/data/public/index_patterns/fields/__snapshots__/field.test.ts.snap new file mode 100644 index 0000000000000..4593349a408a7 --- /dev/null +++ b/src/plugins/data/public/index_patterns/fields/__snapshots__/field.test.ts.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Field exports the property to JSON 1`] = ` +Object { + "aggregatable": true, + "conflictDescriptions": Object { + "a": Array [ + "b", + "c", + ], + "d": Array [ + "e", + ], + }, + "count": 1, + "esTypes": Array [ + "type", + ], + "lang": "lang", + "name": "name", + "readFromDocValues": false, + "script": "script", + "scripted": true, + "searchable": true, + "subType": Object { + "multi": Object { + "parent": "parent", + }, + "nested": Object { + "path": "path", + }, + }, + "type": "type", +} +`; diff --git a/src/plugins/data/public/index_patterns/fields/field.test.ts b/src/plugins/data/public/index_patterns/fields/field.test.ts new file mode 100644 index 0000000000000..18252b159d98d --- /dev/null +++ b/src/plugins/data/public/index_patterns/fields/field.test.ts @@ -0,0 +1,223 @@ +/* + * 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 { Field } from './field'; +import { IndexPattern } from '..'; +import { notificationServiceMock } from '../../../../../core/public/mocks'; +import { FieldFormatsStart } from '../../field_formats'; +import { KBN_FIELD_TYPES } from '../../../common'; + +describe('Field', function () { + function flatten(obj: Record) { + return JSON.parse(JSON.stringify(obj)); + } + + function getField(values = {}) { + return new Field( + fieldValues.indexPattern as IndexPattern, + { ...fieldValues, ...values }, + false, + { + fieldFormats: {} as FieldFormatsStart, + toastNotifications: notificationServiceMock.createStartContract().toasts, + } + ); + } + + const fieldValues = { + name: 'name', + type: 'type', + script: 'script', + lang: 'lang', + count: 1, + esTypes: ['type'], + aggregatable: true, + filterable: true, + searchable: true, + sortable: true, + readFromDocValues: false, + visualizable: true, + scripted: true, + subType: { multi: { parent: 'parent' }, nested: { path: 'path' } }, + displayName: 'displayName', + indexPattern: ({ + fieldFormatMap: { name: {}, _source: {}, _score: {}, _id: {} }, + } as unknown) as IndexPattern, + format: { name: 'formatName' }, + $$spec: {}, + conflictDescriptions: { a: ['b', 'c'], d: ['e'] }, + } as Field; + + it('the correct properties are writable', () => { + const field = getField(); + + expect(field.count).toEqual(1); + field.count = 2; + expect(field.count).toEqual(2); + + expect(field.script).toEqual(fieldValues.script); + field.script = '1'; + expect(field.script).toEqual('1'); + + expect(field.lang).toEqual(fieldValues.lang); + field.lang = 'painless'; + expect(field.lang).toEqual('painless'); + + expect(field.conflictDescriptions).toEqual(fieldValues.conflictDescriptions); + field.conflictDescriptions = {}; + expect(field.conflictDescriptions).toEqual({}); + }); + + it('the correct properties are not writable', () => { + const field = getField(); + + expect(field.name).toEqual(fieldValues.name); + field.name = 'newName'; + expect(field.name).toEqual(fieldValues.name); + + expect(field.type).toEqual(fieldValues.type); + field.type = 'newType'; + expect(field.type).toEqual(fieldValues.type); + + expect(field.esTypes).toEqual(fieldValues.esTypes); + field.esTypes = ['newType']; + expect(field.esTypes).toEqual(fieldValues.esTypes); + + expect(field.scripted).toEqual(fieldValues.scripted); + field.scripted = false; + expect(field.scripted).toEqual(fieldValues.scripted); + + expect(field.searchable).toEqual(fieldValues.searchable); + field.searchable = false; + expect(field.searchable).toEqual(fieldValues.searchable); + + expect(field.aggregatable).toEqual(fieldValues.aggregatable); + field.aggregatable = false; + expect(field.aggregatable).toEqual(fieldValues.aggregatable); + + expect(field.readFromDocValues).toEqual(fieldValues.readFromDocValues); + field.readFromDocValues = true; + expect(field.readFromDocValues).toEqual(fieldValues.readFromDocValues); + + expect(field.subType).toEqual(fieldValues.subType); + field.subType = {}; + expect(field.subType).toEqual(fieldValues.subType); + + // not writable, not serialized + expect(() => { + field.indexPattern = {} as IndexPattern; + }).toThrow(); + + // computed fields + expect(() => { + field.format = { name: 'newFormatName' }; + }).toThrow(); + + expect(() => { + field.sortable = false; + }).toThrow(); + + expect(() => { + field.filterable = false; + }).toThrow(); + + expect(() => { + field.visualizable = false; + }).toThrow(); + + expect(() => { + field.displayName = 'newDisplayName'; + }).toThrow(); + + expect(() => { + field.$$spec = { a: 'b' }; + }).toThrow(); + }); + + it('sets type field when _source field', () => { + const field = getField({ name: '_source' }); + expect(field.type).toEqual('_source'); + }); + + it('calculates searchable', () => { + const field = getField({ searchable: true, scripted: false }); + expect(field.searchable).toEqual(true); + + const fieldB = getField({ searchable: false, scripted: true }); + expect(fieldB.searchable).toEqual(true); + + const fieldC = getField({ searchable: false, scripted: false }); + expect(fieldC.searchable).toEqual(false); + }); + + it('calculates aggregatable', () => { + const field = getField({ aggregatable: true, scripted: false }); + expect(field.aggregatable).toEqual(true); + + const fieldB = getField({ aggregatable: false, scripted: true }); + expect(fieldB.aggregatable).toEqual(true); + + const fieldC = getField({ aggregatable: false, scripted: false }); + expect(fieldC.aggregatable).toEqual(false); + }); + + it('calculates readFromDocValues', () => { + const field = getField({ readFromDocValues: true, scripted: false }); + expect(field.readFromDocValues).toEqual(true); + + const fieldB = getField({ readFromDocValues: false, scripted: false }); + expect(fieldB.readFromDocValues).toEqual(false); + + const fieldC = getField({ readFromDocValues: true, scripted: true }); + expect(fieldC.readFromDocValues).toEqual(false); + }); + + it('calculates sortable', () => { + const field = getField({ name: '_score' }); + expect(field.sortable).toEqual(true); + + const fieldB = getField({ indexed: true, type: KBN_FIELD_TYPES.STRING }); + expect(fieldB.sortable).toEqual(true); + + const fieldC = getField({ indexed: false }); + expect(fieldC.sortable).toEqual(false); + }); + + it('calculates filterable', () => { + const field = getField({ name: '_id' }); + expect(field.filterable).toEqual(true); + + const fieldB = getField({ scripted: true }); + expect(fieldB.filterable).toEqual(true); + + const fieldC = getField({ indexed: true, type: KBN_FIELD_TYPES.STRING }); + expect(fieldC.filterable).toEqual(true); + + const fieldD = getField({ scripted: false, indexed: false }); + expect(fieldD.filterable).toEqual(false); + }); + + it('exports the property to JSON', () => { + const field = new Field({ fieldFormatMap: { name: {} } } as IndexPattern, fieldValues, false, { + fieldFormats: {} as FieldFormatsStart, + toastNotifications: notificationServiceMock.createStartContract().toasts, + }); + expect(flatten(field)).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/data/public/index_patterns/fields/field.ts b/src/plugins/data/public/index_patterns/fields/field.ts index 12db09bbb846f..625df17d62e0d 100644 --- a/src/plugins/data/public/index_patterns/fields/field.ts +++ b/src/plugins/data/public/index_patterns/fields/field.ts @@ -56,6 +56,7 @@ export class Field implements IFieldType { subType?: IFieldSubType; displayName?: string; indexPattern?: IndexPattern; + readFromDocValues?: boolean; format: any; $$spec: FieldSpec; conflictDescriptions?: Record; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index fd40153e12c06..32abfd2694f16 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -991,6 +991,8 @@ export class IndexPatternField implements IFieldType { // (undocumented) name: string; // (undocumented) + readFromDocValues?: boolean; + // (undocumented) script?: string; // (undocumented) scripted?: boolean; diff --git a/src/plugins/data/public/search/aggs/buckets/filters.test.ts b/src/plugins/data/public/search/aggs/buckets/filters.test.ts new file mode 100644 index 0000000000000..295e740a2a780 --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/filters.test.ts @@ -0,0 +1,253 @@ +/* + * 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 { Query } from '../../../../common'; +import { coreMock, notificationServiceMock } from '../../../../../../../src/core/public/mocks'; +import { AggConfigs } from '../agg_configs'; +import { mockAggTypesRegistry } from '../test_helpers'; +import { BUCKET_TYPES } from './bucket_agg_types'; +import { getFiltersBucketAgg, FiltersBucketAggDependencies } from './filters'; +import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; +import { InternalStartServices } from '../../../types'; + +describe('Filters Agg', () => { + let aggTypesDependencies: FiltersBucketAggDependencies; + + beforeEach(() => { + jest.resetAllMocks(); + const { uiSettings } = coreMock.createSetup(); + + aggTypesDependencies = { + uiSettings, + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), + }; + }); + + describe('order agg editor UI', () => { + const getAggConfigs = (params: Record = {}) => { + const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: { + getByName: () => field, + filter: () => [field], + find: () => field, + }, + } as any; + + const field = { + name: 'field', + indexPattern, + }; + + return new AggConfigs( + indexPattern, + [ + { + id: 'test', + params, + type: BUCKET_TYPES.FILTERS, + }, + ], + { + typesRegistry: mockAggTypesRegistry([getFiltersBucketAgg(aggTypesDependencies)]), + fieldFormats: aggTypesDependencies.getInternalStartServices().fieldFormats, + } + ); + }; + + const generateFilter = (label: string, language: string, query: Query['query']) => ({ + label, + input: { + language, + query, + }, + }); + + describe('using Lucene', () => { + test('works with lucene filters', () => { + const aggConfigs = getAggConfigs({ + filters: [ + generateFilter('a', 'lucene', 'foo'), + generateFilter('b', 'lucene', 'status:200'), + generateFilter('c', 'lucene', 'status:[400 TO 499] AND (foo OR bar)'), + ], + }); + + const { [BUCKET_TYPES.FILTERS]: params } = aggConfigs.aggs[0].toDsl(); + expect(Object.values(params.filters).map((v: any) => v.bool.must)).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "query_string": Object { + "query": "foo", + }, + }, + ], + Array [ + Object { + "query_string": Object { + "query": "status:200", + }, + }, + ], + Array [ + Object { + "query_string": Object { + "query": "status:[400 TO 499] AND (foo OR bar)", + }, + }, + ], + ] + `); + }); + }); + + describe('using KQL', () => { + test('works with KQL filters', () => { + const aggConfigs = getAggConfigs({ + filters: [ + generateFilter('a', 'kuery', 'status:200'), + generateFilter('b', 'kuery', 'status > 500 and name:hello'), + ], + }); + + const { [BUCKET_TYPES.FILTERS]: params } = aggConfigs.aggs[0].toDsl(); + expect(Object.values(params.filters).map((v: any) => v.bool.filter)).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "field": 200, + }, + }, + ], + }, + }, + ], + Array [ + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "range": Object { + "field": Object { + "gt": 500, + }, + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "field": "hello", + }, + }, + ], + }, + }, + ], + }, + }, + ], + ] + `); + }); + + test('works with KQL wildcards', () => { + const aggConfigs = getAggConfigs({ + filters: [generateFilter('a', 'kuery', '*'), generateFilter('b', 'kuery', 'foo*')], + }); + + const { [BUCKET_TYPES.FILTERS]: params } = aggConfigs.aggs[0].toDsl(); + expect(Object.values(params.filters).map((v: any) => v.bool.filter)).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "query_string": Object { + "query": "*", + }, + }, + ], + Array [ + Object { + "query_string": Object { + "query": "foo*", + }, + }, + ], + ] + `); + }); + + test('throws with leading wildcards if not allowed', () => { + const aggConfigs = getAggConfigs({ + filters: [generateFilter('a', 'kuery', '*foo*')], + }); + + expect(() => { + aggConfigs.aggs[0].toDsl(); + }).toThrowErrorMatchingInlineSnapshot(` +"Leading wildcards are disabled. See query:allowLeadingWildcards in Advanced Settings. +*foo* +^" +`); + }); + + test('works with leading wildcards if allowed', () => { + aggTypesDependencies.uiSettings.get = (s: any) => + s === 'query:allowLeadingWildcards' ? true : s; + + const aggConfigs = getAggConfigs({ + filters: [generateFilter('a', 'kuery', '*foo*')], + }); + + const { [BUCKET_TYPES.FILTERS]: params } = aggConfigs.aggs[0].toDsl(); + expect(Object.values(params.filters).map((v: any) => v.bool.filter)).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "query_string": Object { + "query": "*foo*", + }, + }, + ], + ] + `); + }); + }); + }); +}); diff --git a/x-pack/index.js b/x-pack/index.js index 0975a82f16f6d..9cf63854d4093 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -12,7 +12,6 @@ import { dashboardMode } from './legacy/plugins/dashboard_mode'; import { beats } from './legacy/plugins/beats_management'; import { maps } from './legacy/plugins/maps'; import { spaces } from './legacy/plugins/spaces'; -import { encryptedSavedObjects } from './legacy/plugins/encrypted_saved_objects'; import { ingestManager } from './legacy/plugins/ingest_manager'; module.exports = function (kibana) { @@ -25,7 +24,6 @@ module.exports = function (kibana) { dashboardMode(kibana), beats(kibana), maps(kibana), - encryptedSavedObjects(kibana), ingestManager(kibana), ]; }; diff --git a/x-pack/legacy/plugins/encrypted_saved_objects/index.ts b/x-pack/legacy/plugins/encrypted_saved_objects/index.ts deleted file mode 100644 index ce343dba006cf..0000000000000 --- a/x-pack/legacy/plugins/encrypted_saved_objects/index.ts +++ /dev/null @@ -1,46 +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 { Root } from 'joi'; -import { Legacy } from 'kibana'; -import { EncryptedSavedObjectsPluginSetup } from '../../../plugins/encrypted_saved_objects/server'; -// @ts-ignore -import { AuditLogger } from '../../server/lib/audit_logger'; - -export const encryptedSavedObjects = (kibana: { - Plugin: new (options: Legacy.PluginSpecOptions & { configPrefix?: string }) => unknown; -}) => - new kibana.Plugin({ - id: 'encryptedSavedObjects', - configPrefix: 'xpack.encryptedSavedObjects', - require: ['xpack_main'], - - // Some legacy plugins still use `enabled` config key, so we keep it here, but the rest of the - // keys is handled by the New Platform plugin. - config: (Joi: Root) => - Joi.object({ - enabled: Joi.boolean().default(true), - }) - .unknown(true) - .default(), - - init(server: Legacy.Server) { - const encryptedSavedObjectsPlugin = (server.newPlatform.setup.plugins - .encryptedSavedObjects as unknown) as EncryptedSavedObjectsPluginSetup; - if (!encryptedSavedObjectsPlugin) { - throw new Error('New Platform XPack EncryptedSavedObjects plugin is not available.'); - } - - encryptedSavedObjectsPlugin.__legacyCompat.registerLegacyAPI({ - auditLogger: new AuditLogger( - server, - 'encryptedSavedObjects', - server.config(), - server.plugins.xpack_main.info - ), - }); - }, - }); diff --git a/x-pack/legacy/plugins/maps/public/angular/map_controller.js b/x-pack/legacy/plugins/maps/public/angular/map_controller.js index 91b54d2698c1d..70d5195feef42 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map_controller.js +++ b/x-pack/legacy/plugins/maps/public/angular/map_controller.js @@ -38,7 +38,6 @@ import { setGotoWithCenter, replaceLayerList, setQuery, - clearTransientLayerStateAndCloseFlyout, setMapSettings, enableFullScreen, updateFlyout, @@ -535,7 +534,6 @@ app.controller( addHelpMenuToAppChrome(); async function doSave(saveOptions) { - await store.dispatch(clearTransientLayerStateAndCloseFlyout()); savedMap.syncWithStore(store.getState()); let id; diff --git a/x-pack/legacy/plugins/security/index.ts b/x-pack/legacy/plugins/security/index.ts index 577b23f3418e8..41371fcbc4c65 100644 --- a/x-pack/legacy/plugins/security/index.ts +++ b/x-pack/legacy/plugins/security/index.ts @@ -9,8 +9,6 @@ import { resolve } from 'path'; import { Server } from 'src/legacy/server/kbn_server'; import { KibanaRequest, LegacyRequest } from '../../../../src/core/server'; // @ts-ignore -import { AuditLogger } from '../../server/lib/audit_logger'; -// @ts-ignore import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status_and_license_to_initialize'; import { AuthenticatedUser, SecurityPluginSetup } from '../../../plugins/security/server'; @@ -33,28 +31,24 @@ function getSecurityPluginSetup(server: Server) { export const security = (kibana: Record) => new kibana.Plugin({ id: 'security', - configPrefix: 'xpack.security', publicDir: resolve(__dirname, 'public'), - require: ['kibana', 'elasticsearch', 'xpack_main'], + require: ['kibana', 'xpack_main'], + configPrefix: 'xpack.security', + uiExports: { + hacks: ['plugins/security/hacks/legacy'], + injectDefaultVars: (server: Server) => { + return { enableSpaceAwarePrivileges: server.config().get('xpack.spaces.enabled') }; + }, + }, - // This config is only used by `AuditLogger` and should be removed as soon as `AuditLogger` - // is migrated to Kibana Platform. config(Joi: Root) { return Joi.object({ enabled: Joi.boolean().default(true), - audit: Joi.object({ enabled: Joi.boolean().default(false) }).default(), }) .unknown() .default(); }, - uiExports: { - hacks: ['plugins/security/hacks/legacy'], - injectDefaultVars: (server: Server) => { - return { enableSpaceAwarePrivileges: server.config().get('xpack.spaces.enabled') }; - }, - }, - async postInit(server: Server) { watchStatusAndLicenseToInitialize(server.plugins.xpack_main, this, async () => { const xpackInfo = server.plugins.xpack_main.info; @@ -67,11 +61,6 @@ export const security = (kibana: Record) => async init(server: Server) { const securityPlugin = getSecurityPluginSetup(server); - const xpackInfo = server.plugins.xpack_main.info; - securityPlugin.__legacyCompat.registerLegacyAPI({ - auditLogger: new AuditLogger(server, 'security', server.config(), xpackInfo), - }); - server.expose({ getUser: async (request: LegacyRequest) => securityPlugin.authc.getCurrentUser(KibanaRequest.from(request)), diff --git a/x-pack/legacy/plugins/spaces/index.ts b/x-pack/legacy/plugins/spaces/index.ts index 723164480b3b8..2f3e5e0a86d21 100644 --- a/x-pack/legacy/plugins/spaces/index.ts +++ b/x-pack/legacy/plugins/spaces/index.ts @@ -8,20 +8,9 @@ import { resolve } from 'path'; import KbnServer, { Server } from 'src/legacy/server/kbn_server'; import { Legacy } from 'kibana'; import { KibanaRequest } from '../../../../src/core/server'; -import { SpacesServiceSetup } from '../../../plugins/spaces/server'; import { SpacesPluginSetup } from '../../../plugins/spaces/server'; -// @ts-ignore -import { AuditLogger } from '../../server/lib/audit_logger'; import { wrapError } from './server/lib/errors'; -export interface LegacySpacesPlugin { - getSpaceId: (request: Legacy.Request) => ReturnType; - getActiveSpace: (request: Legacy.Request) => ReturnType; - spaceIdToNamespace: SpacesServiceSetup['spaceIdToNamespace']; - namespaceToSpaceId: SpacesServiceSetup['namespaceToSpaceId']; - getBasePath: SpacesServiceSetup['getBasePath']; -} - export const spaces = (kibana: Record) => new kibana.Plugin({ id: 'spaces', @@ -79,15 +68,6 @@ export const spaces = (kibana: Record) => throw new Error('New Platform XPack Spaces plugin is not available.'); } - const { registerLegacyAPI } = spacesPlugin.__legacyCompat; - - registerLegacyAPI({ - auditLogger: { - create: (pluginId: string) => - new AuditLogger(server, pluginId, server.config(), server.plugins.xpack_main.info), - }, - }); - server.expose('getSpaceId', (request: Legacy.Request) => spacesPlugin.spacesService.getSpaceId(request) ); diff --git a/x-pack/legacy/server/lib/audit_logger.js b/x-pack/legacy/server/lib/audit_logger.js deleted file mode 100644 index 7d3467b323b3f..0000000000000 --- a/x-pack/legacy/server/lib/audit_logger.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { checkLicense } from './check_license'; -import { LICENSE_TYPE_STANDARD, LICENSE_STATUS_VALID } from '../../common/constants'; - -const FEATURE = { - ID: 'audit_logging', -}; - -export class AuditLogger { - constructor(server, pluginId, config, xPackInfo) { - this._server = server; - this._pluginId = pluginId; - this._enabled = - config.get('xpack.security.enabled') && config.get('xpack.security.audit.enabled'); - this._licensed = false; - this._checkLicense = (xPackInfo) => { - this._licensed = - checkLicense(FEATURE.ID, LICENSE_TYPE_STANDARD, xPackInfo).status === LICENSE_STATUS_VALID; - }; - xPackInfo - .feature(`${FEATURE.ID}-${pluginId}`) - .registerLicenseCheckResultsGenerator(this._checkLicense); - this._checkLicense(xPackInfo); - } - - log(eventType, message, data = {}) { - if (!this._licensed || !this._enabled) { - return; - } - - this._server.logWithMetadata(['info', 'audit', this._pluginId, eventType], message, { - ...data, - eventType, - }); - } -} diff --git a/x-pack/legacy/server/lib/audit_logger.test.js b/x-pack/legacy/server/lib/audit_logger.test.js deleted file mode 100644 index 51a239801caac..0000000000000 --- a/x-pack/legacy/server/lib/audit_logger.test.js +++ /dev/null @@ -1,195 +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 { AuditLogger } from './audit_logger'; -import { - LICENSE_TYPE_STANDARD, - LICENSE_TYPE_BASIC, - LICENSE_TYPE_GOLD, -} from '../../common/constants'; - -const createMockConfig = (settings) => { - const mockConfig = { - get: jest.fn(), - }; - - mockConfig.get.mockImplementation((key) => { - return settings[key]; - }); - - return mockConfig; -}; - -const mockLicenseInfo = { - isAvailable: () => true, - feature: () => { - return { - registerLicenseCheckResultsGenerator: () => { - return; - }, - }; - }, - license: { - isActive: () => true, - isOneOf: () => true, - getType: () => LICENSE_TYPE_STANDARD, - }, -}; - -const mockConfig = createMockConfig({ - 'xpack.security.enabled': true, - 'xpack.security.audit.enabled': true, -}); - -test(`calls server.log with 'info', audit', pluginId and eventType as tags`, () => { - const mockServer = { - logWithMetadata: jest.fn(), - }; - - const pluginId = 'foo'; - const auditLogger = new AuditLogger(mockServer, pluginId, mockConfig, mockLicenseInfo); - - const eventType = 'bar'; - auditLogger.log(eventType, ''); - expect(mockServer.logWithMetadata).toHaveBeenCalledTimes(1); - expect(mockServer.logWithMetadata).toHaveBeenCalledWith( - ['info', 'audit', pluginId, eventType], - expect.anything(), - expect.anything() - ); -}); - -test(`calls server.log with message`, () => { - const mockServer = { - logWithMetadata: jest.fn(), - }; - const auditLogger = new AuditLogger(mockServer, 'foo', mockConfig, mockLicenseInfo); - - const message = 'summary of what happened'; - auditLogger.log('bar', message); - expect(mockServer.logWithMetadata).toHaveBeenCalledTimes(1); - expect(mockServer.logWithMetadata).toHaveBeenCalledWith( - expect.anything(), - message, - expect.anything() - ); -}); - -test(`calls server.log with metadata `, () => { - const mockServer = { - logWithMetadata: jest.fn(), - }; - - const auditLogger = new AuditLogger(mockServer, 'foo', mockConfig, mockLicenseInfo); - - const data = { - foo: 'yup', - baz: 'nah', - }; - - auditLogger.log('bar', 'summary of what happened', data); - expect(mockServer.logWithMetadata).toHaveBeenCalledTimes(1); - expect(mockServer.logWithMetadata).toHaveBeenCalledWith(expect.anything(), expect.anything(), { - eventType: 'bar', - foo: data.foo, - baz: data.baz, - }); -}); - -test(`does not call server.log for license level < Standard`, () => { - const mockServer = { - logWithMetadata: jest.fn(), - }; - const mockLicenseInfo = { - isAvailable: () => true, - feature: () => { - return { - registerLicenseCheckResultsGenerator: () => { - return; - }, - }; - }, - license: { - isActive: () => true, - isOneOf: () => false, - getType: () => LICENSE_TYPE_BASIC, - }, - }; - - const auditLogger = new AuditLogger(mockServer, 'foo', mockConfig, mockLicenseInfo); - auditLogger.log('bar', 'what happened'); - expect(mockServer.logWithMetadata).toHaveBeenCalledTimes(0); -}); - -test(`does not call server.log if security is not enabled`, () => { - const mockServer = { - logWithMetadata: jest.fn(), - }; - - const mockConfig = createMockConfig({ - 'xpack.security.enabled': false, - 'xpack.security.audit.enabled': true, - }); - - const auditLogger = new AuditLogger(mockServer, 'foo', mockConfig, mockLicenseInfo); - auditLogger.log('bar', 'what happened'); - expect(mockServer.logWithMetadata).toHaveBeenCalledTimes(0); -}); - -test(`does not call server.log if security audit logging is not enabled`, () => { - const mockServer = { - logWithMetadata: jest.fn(), - }; - - const mockConfig = createMockConfig({ - 'xpack.security.enabled': true, - }); - - const auditLogger = new AuditLogger(mockServer, 'foo', mockConfig, mockLicenseInfo); - auditLogger.log('bar', 'what happened'); - expect(mockServer.logWithMetadata).toHaveBeenCalledTimes(0); -}); - -test(`calls server.log after basic -> gold upgrade`, () => { - const mockServer = { - logWithMetadata: jest.fn(), - }; - - const endLicenseInfo = { - isAvailable: () => true, - license: { - isActive: () => true, - isOneOf: () => true, - getType: () => LICENSE_TYPE_GOLD, - }, - }; - - let licenseCheckResultsGenerator; - - const startLicenseInfo = { - isAvailable: () => true, - feature: () => { - return { - registerLicenseCheckResultsGenerator: (fn) => { - licenseCheckResultsGenerator = fn; - }, - }; - }, - license: { - isActive: () => true, - isOneOf: () => false, - getType: () => LICENSE_TYPE_BASIC, - }, - }; - - const auditLogger = new AuditLogger(mockServer, 'foo', mockConfig, startLicenseInfo); - auditLogger.log('bar', 'what happened'); - expect(mockServer.logWithMetadata).toHaveBeenCalledTimes(0); - - //change basic to gold - licenseCheckResultsGenerator(endLicenseInfo); - auditLogger.log('bar', 'what happened'); - expect(mockServer.logWithMetadata).toHaveBeenCalledTimes(1); -}); diff --git a/x-pack/plugins/canvas/public/application.tsx b/x-pack/plugins/canvas/public/application.tsx index 426b34cf73b9a..8751d8102ad37 100644 --- a/x-pack/plugins/canvas/public/application.tsx +++ b/x-pack/plugins/canvas/public/application.tsx @@ -18,7 +18,6 @@ import { CanvasStartDeps, CanvasSetupDeps } from './plugin'; // @ts-ignore Untyped local import { App } from './components/app'; import { KibanaContextProvider } from '../../../../src/plugins/kibana_react/public'; -import { initInterpreter } from './lib/run_interpreter'; import { registerLanguage } from './lib/monaco_language_def'; import { SetupRegistries } from './plugin_api'; import { initRegistries, populateRegistries, destroyRegistries } from './registries'; @@ -37,6 +36,7 @@ import { startServices, services } from './services'; import { destroyHistory } from './lib/history_provider'; // @ts-ignore Untyped local import { stopRouter } from './lib/router_provider'; +import { initFunctions } from './functions'; // @ts-ignore Untyped local import { appUnload } from './state/actions/app'; @@ -82,15 +82,25 @@ export const initializeCanvas = async ( registries: SetupRegistries, appUpdater: BehaviorSubject ) => { - startServices(coreSetup, coreStart, setupPlugins, startPlugins, appUpdater); + await startServices(coreSetup, coreStart, setupPlugins, startPlugins, appUpdater); + + // Adding these functions here instead of in plugin.ts. + // Some of these functions have deep dependencies into Canvas, which was bulking up the size + // of our bundle entry point. Moving them here pushes that load to when canvas is actually loaded. + const canvasFunctions = initFunctions({ + timefilter: setupPlugins.data.query.timefilter.timefilter, + prependBasePath: coreSetup.http.basePath.prepend, + typesRegistry: setupPlugins.expressions.__LEGACY.types, + }); + + for (const fn of canvasFunctions) { + services.expressions.getService().registerFunction(fn); + } // Create Store const canvasStore = await createStore(coreSetup, setupPlugins); - // Init Interpreter - initInterpreter(startPlugins.expressions, setupPlugins.expressions).then(() => { - registerLanguage(Object.values(startPlugins.expressions.getFunctions())); - }); + registerLanguage(Object.values(services.expressions.getService().getFunctions())); // Init Registries initRegistries(); diff --git a/x-pack/plugins/canvas/public/components/fullscreen/fullscreen.scss b/x-pack/plugins/canvas/public/components/fullscreen/fullscreen.scss index c198884ee7131..7110a22408fe2 100644 --- a/x-pack/plugins/canvas/public/components/fullscreen/fullscreen.scss +++ b/x-pack/plugins/canvas/public/components/fullscreen/fullscreen.scss @@ -1,5 +1,5 @@ body.canvas-isFullscreen { // sass-lint:disable-line no-qualifying-elements - // following two rules are for overriding the header bar padding + // following two rules are for overriding the header bar padding &.euiBody--headerIsFixed { padding-top: 0; } @@ -8,6 +8,12 @@ body.canvas-isFullscreen { // sass-lint:disable-line no-qualifying-elements min-height: 100vh; } + // following rule is for docked navigation + &.euiBody--collapsibleNavIsDocked { + padding-left: 0 !important; // sass-lint:disable-line no-important + } + + // hide global loading indicator .kbnLoadingIndicator { display: none; diff --git a/x-pack/plugins/canvas/public/lib/run_interpreter.ts b/x-pack/plugins/canvas/public/lib/run_interpreter.ts index bd00cad4fcbe7..07c0ca4b1ce15 100644 --- a/x-pack/plugins/canvas/public/lib/run_interpreter.ts +++ b/x-pack/plugins/canvas/public/lib/run_interpreter.ts @@ -6,32 +6,7 @@ import { fromExpression, getType } from '@kbn/interpreter/common'; import { ExpressionValue, ExpressionAstExpression } from 'src/plugins/expressions/public'; -import { notifyService } from '../services'; - -import { CanvasStartDeps, CanvasSetupDeps } from '../plugin'; - -let expressionsStarting: Promise | undefined; - -export const initInterpreter = function ( - expressionsStart: CanvasStartDeps['expressions'], - expressionsSetup: CanvasSetupDeps['expressions'] -) { - expressionsStarting = startExpressions(expressionsStart, expressionsSetup); - - return expressionsStarting; -}; - -async function startExpressions( - expressionsStart: CanvasStartDeps['expressions'], - expressionsSetup: CanvasSetupDeps['expressions'] -) { - await expressionsSetup.__LEGACY.loadLegacyServerFunctionWrappers(); - return expressionsStart; -} - -export const resetInterpreter = function () { - expressionsStarting = undefined; -}; +import { notifyService, expressionsService } from '../services'; interface Options { castToRender?: boolean; @@ -41,12 +16,7 @@ interface Options { * Meant to be a replacement for plugins/interpreter/interpretAST */ export async function interpretAst(ast: ExpressionAstExpression): Promise { - if (!expressionsStarting) { - throw new Error('Interpreter has not been initialized'); - } - - const expressions = await expressionsStarting; - return await expressions.execute(ast).getData(); + return await expressionsService.getService().execute(ast).getData(); } /** @@ -63,14 +33,8 @@ export async function runInterpreter( input: ExpressionValue, options: Options = {} ): Promise { - if (!expressionsStarting) { - throw new Error('Interpreter has not been initialized'); - } - - const expressions = await expressionsStarting; - try { - const renderable = await expressions.execute(ast, input).getData(); + const renderable = await expressionsService.getService().execute(ast, input).getData(); if (getType(renderable) === 'render') { return renderable; diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index 1265bfbb69b70..9d2a6b3fdf4f4 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -28,7 +28,6 @@ import { Start as InspectorStart } from '../../../../src/plugins/inspector/publi import { argTypeSpecs } from './expression_types/arg_types'; import { transitions } from './transitions'; import { getPluginApi, CanvasApi } from './plugin_api'; -import { initFunctions } from './functions'; import { CanvasSrcPlugin } from '../canvas_plugin_src/plugin'; export { CoreStart, CoreSetup }; @@ -117,14 +116,6 @@ export class CanvasPlugin plugins.home.featureCatalogue.register(featureCatalogueEntry); - // Register core canvas stuff - canvasApi.addFunctions( - initFunctions({ - timefilter: plugins.data.query.timefilter.timefilter, - prependBasePath: core.http.basePath.prepend, - typesRegistry: plugins.expressions.__LEGACY.types, - }) - ); canvasApi.addArgumentUIs(argTypeSpecs); canvasApi.addTransitions(transitions); diff --git a/x-pack/plugins/canvas/public/services/expressions.ts b/x-pack/plugins/canvas/public/services/expressions.ts new file mode 100644 index 0000000000000..16f939a9c97fc --- /dev/null +++ b/x-pack/plugins/canvas/public/services/expressions.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 { CanvasServiceFactory } from '.'; +import { ExpressionsService } from '../../../../../src/plugins/expressions/common'; + +export const expressionsServiceFactory: CanvasServiceFactory = async ( + coreSetup, + coreStart, + setupPlugins, + startPlugins +) => { + await setupPlugins.expressions.__LEGACY.loadLegacyServerFunctionWrappers(); + + return setupPlugins.expressions.fork(); +}; diff --git a/x-pack/plugins/canvas/public/services/index.ts b/x-pack/plugins/canvas/public/services/index.ts index 42176f953c331..a929b4639d3e4 100644 --- a/x-pack/plugins/canvas/public/services/index.ts +++ b/x-pack/plugins/canvas/public/services/index.ts @@ -10,6 +10,7 @@ import { CanvasSetupDeps, CanvasStartDeps } from '../plugin'; import { notifyServiceFactory } from './notify'; import { platformServiceFactory } from './platform'; import { navLinkServiceFactory } from './nav_link'; +import { expressionsServiceFactory } from './expressions'; export type CanvasServiceFactory = ( coreSetup: CoreSetup, @@ -17,7 +18,7 @@ export type CanvasServiceFactory = ( canvasSetupPlugins: CanvasSetupDeps, canvasStartPlugins: CanvasStartDeps, appUpdater: BehaviorSubject -) => Service; +) => Service | Promise; class CanvasServiceProvider { private factory: CanvasServiceFactory; @@ -27,14 +28,14 @@ class CanvasServiceProvider { this.factory = factory; } - start( + async start( coreSetup: CoreSetup, coreStart: CoreStart, canvasSetupPlugins: CanvasSetupDeps, canvasStartPlugins: CanvasStartDeps, appUpdater: BehaviorSubject ) { - this.service = this.factory( + this.service = await this.factory( coreSetup, coreStart, canvasSetupPlugins, @@ -59,27 +60,31 @@ class CanvasServiceProvider { export type ServiceFromProvider

= P extends CanvasServiceProvider ? T : never; export const services = { + expressions: new CanvasServiceProvider(expressionsServiceFactory), notify: new CanvasServiceProvider(notifyServiceFactory), platform: new CanvasServiceProvider(platformServiceFactory), navLink: new CanvasServiceProvider(navLinkServiceFactory), }; export interface CanvasServices { + expressions: ServiceFromProvider; notify: ServiceFromProvider; platform: ServiceFromProvider; navLink: ServiceFromProvider; } -export const startServices = ( +export const startServices = async ( coreSetup: CoreSetup, coreStart: CoreStart, canvasSetupPlugins: CanvasSetupDeps, canvasStartPlugins: CanvasStartDeps, appUpdater: BehaviorSubject ) => { - Object.entries(services).forEach(([key, provider]) => + const startPromises = Object.values(services).map((provider) => provider.start(coreSetup, coreStart, canvasSetupPlugins, canvasStartPlugins, appUpdater) ); + + await Promise.all(startPromises); }; export const stopServices = () => { @@ -90,4 +95,5 @@ export const { notify: notifyService, platform: platformService, navLink: navLinkService, + expressions: expressionsService, } = services; diff --git a/x-pack/plugins/encrypted_saved_objects/server/audit/audit_logger.test.ts b/x-pack/plugins/encrypted_saved_objects/server/audit/audit_logger.test.ts index 760c4ef01b31c..1d1396fd520d1 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/audit/audit_logger.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/audit/audit_logger.test.ts @@ -9,7 +9,7 @@ import { mockAuthenticatedUser } from '../../../security/common/model/authentica it('properly logs audit events', () => { const mockInternalAuditLogger = { log: jest.fn() }; - const audit = new EncryptedSavedObjectsAuditLogger(() => mockInternalAuditLogger); + const audit = new EncryptedSavedObjectsAuditLogger(mockInternalAuditLogger); audit.encryptAttributesSuccess(['one', 'two'], { type: 'known-type', diff --git a/x-pack/plugins/encrypted_saved_objects/server/audit/audit_logger.ts b/x-pack/plugins/encrypted_saved_objects/server/audit/audit_logger.ts index 1a10dd343d43d..de14a79dd0ddb 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/audit/audit_logger.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/audit/audit_logger.ts @@ -4,22 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ +import { AuditLogger, AuthenticatedUser } from '../../../security/server'; import { SavedObjectDescriptor, descriptorToArray } from '../crypto'; -import { LegacyAPI } from '../plugin'; -import { AuthenticatedUser } from '../../../security/common/model'; /** * Represents all audit events the plugin can log. */ export class EncryptedSavedObjectsAuditLogger { - constructor(private readonly getAuditLogger: () => LegacyAPI['auditLogger']) {} + constructor(private readonly logger: AuditLogger = { log() {} }) {} public encryptAttributeFailure( attributeName: string, descriptor: SavedObjectDescriptor, user?: AuthenticatedUser ) { - this.getAuditLogger().log( + this.logger.log( 'encrypt_failure', `Failed to encrypt attribute "${attributeName}" for saved object "[${descriptorToArray( descriptor @@ -33,7 +32,7 @@ export class EncryptedSavedObjectsAuditLogger { descriptor: SavedObjectDescriptor, user?: AuthenticatedUser ) { - this.getAuditLogger().log( + this.logger.log( 'decrypt_failure', `Failed to decrypt attribute "${attributeName}" for saved object "[${descriptorToArray( descriptor @@ -47,7 +46,7 @@ export class EncryptedSavedObjectsAuditLogger { descriptor: SavedObjectDescriptor, user?: AuthenticatedUser ) { - this.getAuditLogger().log( + this.logger.log( 'encrypt_success', `Successfully encrypted attributes "[${attributesNames}]" for saved object "[${descriptorToArray( descriptor @@ -61,7 +60,7 @@ export class EncryptedSavedObjectsAuditLogger { descriptor: SavedObjectDescriptor, user?: AuthenticatedUser ) { - this.getAuditLogger().log( + this.logger.log( 'decrypt_success', `Successfully decrypted attributes "[${attributesNames}]" for saved object "[${descriptorToArray( descriptor diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts index e8568e9964c2f..4afd74488f9fe 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts @@ -16,9 +16,6 @@ describe('EncryptedSavedObjects Plugin', () => { await expect(plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() })) .resolves.toMatchInlineSnapshot(` Object { - "__legacyCompat": Object { - "registerLegacyAPI": [Function], - }, "registerType": [Function], "usingEphemeralEncryptionKey": true, } diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts index 83b412de5db7e..cdbdd18b9d696 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts @@ -22,7 +22,6 @@ export interface PluginsSetup { export interface EncryptedSavedObjectsPluginSetup { registerType: (typeRegistration: EncryptedSavedObjectTypeRegistration) => void; - __legacyCompat: { registerLegacyAPI: (legacyAPI: LegacyAPI) => void }; usingEphemeralEncryptionKey: boolean; } @@ -31,16 +30,6 @@ export interface EncryptedSavedObjectsPluginStart { getClient: ClientInstanciator; } -/** - * Describes a set of APIs that is available in the legacy platform only and required by this plugin - * to function properly. - */ -export interface LegacyAPI { - auditLogger: { - log: (eventType: string, message: string, data?: Record) => void; - }; -} - /** * Represents EncryptedSavedObjects Plugin instance that will be managed by the Kibana plugin system. */ @@ -48,14 +37,6 @@ export class Plugin { private readonly logger: Logger; private savedObjectsSetup!: ClientInstanciator; - private legacyAPI?: LegacyAPI; - private readonly getLegacyAPI = () => { - if (!this.legacyAPI) { - throw new Error('Legacy API is not registered!'); - } - return this.legacyAPI; - }; - constructor(private readonly initializerContext: PluginInitializerContext) { this.logger = this.initializerContext.logger.get(); } @@ -72,7 +53,9 @@ export class Plugin { new EncryptedSavedObjectsService( config.encryptionKey, this.logger, - new EncryptedSavedObjectsAuditLogger(() => this.getLegacyAPI().auditLogger) + new EncryptedSavedObjectsAuditLogger( + deps.security?.audit.getLogger('encryptedSavedObjects') + ) ) ); @@ -86,7 +69,6 @@ export class Plugin { return { registerType: (typeRegistration: EncryptedSavedObjectTypeRegistration) => service.registerType(typeRegistration), - __legacyCompat: { registerLegacyAPI: (legacyAPI: LegacyAPI) => (this.legacyAPI = legacyAPI) }, usingEphemeralEncryptionKey, }; } diff --git a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx index cc87167b10a96..d81d11e01d4a5 100644 --- a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx +++ b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx @@ -17,11 +17,7 @@ import { import { IErrorObject } from '../../../../../../triggers_actions_ui/public/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertsContextValue } from '../../../../../../triggers_actions_ui/public/application/context/alerts_context'; -import { - LogDocumentCountAlertParams, - Comparator, - TimeUnit, -} from '../../../../../common/alerting/logs/types'; +import { LogDocumentCountAlertParams, Comparator } from '../../../../../common/alerting/logs/types'; import { DocumentCount } from './document_count'; import { Criteria } from './criteria'; import { useSourceId } from '../../../../containers/source_id'; @@ -123,8 +119,6 @@ export const SourceStatusWrapper: React.FC = (props) => { export const Editor: React.FC = (props) => { const { setAlertParams, alertParams, errors } = props; - const [timeSize, setTimeSize] = useState(1); - const [timeUnit, setTimeUnit] = useState('m'); const [hasSetDefaults, setHasSetDefaults] = useState(false); const { sourceStatus } = useLogSourceContext(); @@ -165,15 +159,13 @@ export const Editor: React.FC = (props) => { const updateTimeSize = useCallback( (ts: number | undefined) => { - setTimeSize(ts || undefined); setAlertParams('timeSize', ts); }, - [setTimeSize, setAlertParams] + [setAlertParams] ); const updateTimeUnit = useCallback( (tu: string) => { - setTimeUnit(tu as TimeUnit); setAlertParams('timeUnit', tu); }, [setAlertParams] @@ -217,8 +209,8 @@ export const Editor: React.FC = (props) => { /> ; @@ -97,8 +98,38 @@ export const itemIdOrUndefined = t.union([item_id, t.undefined]); export type ItemIdOrUndefined = t.TypeOf; export const per_page = t.number; // TODO: Change this out for PositiveNumber from siem +export type PerPage = t.TypeOf; + +export const perPageOrUndefined = t.union([per_page, t.undefined]); +export type PerPageOrUndefined = t.TypeOf; + export const total = t.number; // TODO: Change this out for PositiveNumber from siem +export const totalUndefined = t.union([total, t.undefined]); +export type TotalOrUndefined = t.TypeOf; + export const page = t.number; // TODO: Change this out for PositiveNumber from siem +export type Page = t.TypeOf; + +export const pageOrUndefined = t.union([page, t.undefined]); +export type PageOrUndefined = t.TypeOf; + export const sort_field = t.string; +export const sortFieldOrUndefined = t.union([sort_field, t.undefined]); +export type SortFieldOrUndefined = t.TypeOf; + export const sort_order = t.keyof({ asc: null, desc: null }); +export const sortOrderOrUndefined = t.union([sort_order, t.undefined]); +export type SortOrderOrUndefined = t.TypeOf; + export const filter = t.string; +export type Filter = t.TypeOf; +export const filterOrUndefined = t.union([filter, t.undefined]); +export type FilterOrUndefined = t.TypeOf; + +export const cursor = t.string; +export type Cursor = t.TypeOf; +export const cursorOrUndefined = t.union([cursor, t.undefined]); +export type CursorOrUndefined = t.TypeOf; + +export const namespace_type = DefaultNamespace; +export type NamespaceType = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts index f899fd69110fa..c10d441d93aa5 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts @@ -10,6 +10,7 @@ import * as t from 'io-ts'; import { ItemId, + NamespaceType, Tags, _Tags, _tags, @@ -19,6 +20,7 @@ import { list_id, meta, name, + namespace_type, tags, } from '../common/schemas'; import { Identity, RequiredKeepUndefined } from '../../types'; @@ -41,6 +43,7 @@ export const createExceptionListItemSchema = t.intersection([ entries: DefaultEntryArray, // defaults to empty array if not set during decode item_id: DefaultUuid, // defaults to GUID (uuid v4) if not set during decode meta, // defaults to undefined if not set during decode + namespace_type, // defaults to 'single' if not set during decode tags, // defaults to empty array if not set during decode }) ), @@ -53,13 +56,16 @@ export type CreateExceptionListItemSchema = RequiredKeepUndefined< t.TypeOf >; -// This type is used after a decode since the arrays turn into defaults of empty arrays -// and if a item_id is not specified it turns into a default GUID +// This type is used after a decode since some things are defaults after a decode. export type CreateExceptionListItemSchemaDecoded = Identity< - Omit & { + Omit< + CreateExceptionListItemSchema, + '_tags' | 'tags' | 'item_id' | 'entries' | 'namespace_type' + > & { _tags: _Tags; tags: Tags; item_id: ItemId; entries: EntriesArray; + namespace_type: NamespaceType; } >; diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.mock.ts index d38d3cc038525..f0b98cb96f743 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.mock.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DESCRIPTION, LIST_ID, META, NAME, TYPE } from '../../constants.mock'; +import { DESCRIPTION, LIST_ID, META, NAME, NAMESPACE_TYPE, TYPE } from '../../constants.mock'; import { CreateExceptionListSchema } from './create_exception_list_schema'; @@ -14,6 +14,7 @@ export const getCreateExceptionListSchemaMock = (): CreateExceptionListSchema => list_id: LIST_ID, meta: META, name: NAME, + namespace_type: NAMESPACE_TYPE, tags: [], type: TYPE, }); diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts index 5ba3bf4e8f43b..3da8bfca126ae 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts @@ -10,6 +10,7 @@ import * as t from 'io-ts'; import { ListId, + NamespaceType, Tags, _Tags, _tags, @@ -17,6 +18,7 @@ import { exceptionListType, meta, name, + namespace_type, tags, } from '../common/schemas'; import { Identity, RequiredKeepUndefined } from '../../types'; @@ -35,6 +37,7 @@ export const createExceptionListSchema = t.intersection([ _tags, // defaults to empty array if not set during decode list_id: DefaultUuid, // defaults to a GUID (UUID v4) string if not set during decode meta, // defaults to undefined if not set during decode + namespace_type, // defaults to 'single' if not set during decode tags, // defaults to empty array if not set during decode }) ), @@ -45,11 +48,12 @@ export type CreateExceptionListSchema = RequiredKeepUndefined< t.TypeOf >; -// This type is used after a decode since the arrays turn into defaults of empty arrays. +// This type is used after a decode since some things are defaults after a decode. export type CreateExceptionListSchemaDecoded = Identity< - CreateExceptionListSchema & { + Omit & { _tags: _Tags; tags: Tags; list_id: ListId; + namespace_type: NamespaceType; } >; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts index 607e05ef8286f..4c5b70d9a4073 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts @@ -8,13 +8,22 @@ import * as t from 'io-ts'; -import { id, item_id } from '../common/schemas'; +import { NamespaceType, id, item_id, namespace_type } from '../common/schemas'; export const deleteExceptionListItemSchema = t.exact( t.partial({ id, item_id, + namespace_type, // defaults to 'single' if not set during decode }) ); export type DeleteExceptionListItemSchema = t.TypeOf; + +// This type is used after a decode since some things are defaults after a decode. +export type DeleteExceptionListItemSchemaDecoded = Omit< + DeleteExceptionListItemSchema, + 'namespace_type' +> & { + namespace_type: NamespaceType; +}; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts index 7a6086514f943..2577d867031f0 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts @@ -8,13 +8,19 @@ import * as t from 'io-ts'; -import { id, list_id } from '../common/schemas'; +import { NamespaceType, id, list_id, namespace_type } from '../common/schemas'; export const deleteExceptionListSchema = t.exact( t.partial({ id, list_id, + namespace_type, // defaults to 'single' if not set during decode }) ); export type DeleteExceptionListSchema = t.TypeOf; + +// This type is used after a decode since some things are defaults after a decode. +export type DeleteExceptionListSchemaDecoded = Omit & { + namespace_type: NamespaceType; +}; diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts index 3fc51dd20b0b3..31eb4925eb6d6 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts @@ -8,8 +8,16 @@ import * as t from 'io-ts'; -import { filter, list_id, page, per_page, sort_field, sort_order } from '../common/schemas'; +import { + NamespaceType, + filter, + list_id, + namespace_type, + sort_field, + sort_order, +} from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; +import { StringToPositiveNumber } from '../types/string_to_positive_number'; export const findExceptionListItemSchema = t.intersection([ t.exact( @@ -20,8 +28,9 @@ export const findExceptionListItemSchema = t.intersection([ t.exact( t.partial({ filter, // defaults to undefined if not set during decode - page, // defaults to undefined if not set during decode - per_page, // defaults to undefined if not set during decode + namespace_type, // defaults to 'single' if not set during decode + page: StringToPositiveNumber, // defaults to undefined if not set during decode + per_page: StringToPositiveNumber, // defaults to undefined if not set during decode sort_field, // defaults to undefined if not set during decode sort_order, // defaults to undefined if not set during decode }) @@ -30,6 +39,19 @@ export const findExceptionListItemSchema = t.intersection([ export type FindExceptionListItemSchemaPartial = t.TypeOf; +// This type is used after a decode since some things are defaults after a decode. +export type FindExceptionListItemSchemaPartialDecoded = Omit< + FindExceptionListItemSchemaPartial, + 'namespace_type' +> & { + namespace_type: NamespaceType; +}; + +// This type is used after a decode since some things are defaults after a decode. +export type FindExceptionListItemSchemaDecoded = RequiredKeepUndefined< + FindExceptionListItemSchemaPartialDecoded +>; + export type FindExceptionListItemSchema = RequiredKeepUndefined< t.TypeOf >; diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts index f795be9493fbf..fa00c5b0dafb1 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts @@ -8,14 +8,16 @@ import * as t from 'io-ts'; -import { filter, page, per_page, sort_field, sort_order } from '../common/schemas'; +import { NamespaceType, filter, namespace_type, sort_field, sort_order } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; +import { StringToPositiveNumber } from '../types/string_to_positive_number'; export const findExceptionListSchema = t.exact( t.partial({ filter, // defaults to undefined if not set during decode - page, // defaults to undefined if not set during decode - per_page, // defaults to undefined if not set during decode + namespace_type, // defaults to 'single' if not set during decode + page: StringToPositiveNumber, // defaults to undefined if not set during decode + per_page: StringToPositiveNumber, // defaults to undefined if not set during decode sort_field, // defaults to undefined if not set during decode sort_order, // defaults to undefined if not set during decode }) @@ -23,6 +25,19 @@ export const findExceptionListSchema = t.exact( export type FindExceptionListSchemaPartial = t.TypeOf; +// This type is used after a decode since some things are defaults after a decode. +export type FindExceptionListSchemaPartialDecoded = Omit< + FindExceptionListSchemaPartial, + 'namespace_type' +> & { + namespace_type: NamespaceType; +}; + +// This type is used after a decode since some things are defaults after a decode. +export type FindExceptionListSchemaDecoded = RequiredKeepUndefined< + FindExceptionListSchemaPartialDecoded +>; + export type FindExceptionListSchema = RequiredKeepUndefined< t.TypeOf >; diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts new file mode 100644 index 0000000000000..c9ece4224c4ce --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { cursor, filter, list_id, sort_field, sort_order } from '../common/schemas'; +import { Identity, RequiredKeepUndefined } from '../../types'; +import { StringToPositiveNumber } from '../types/string_to_positive_number'; + +export const findListItemSchema = t.intersection([ + t.exact(t.type({ list_id })), + t.exact( + t.partial({ + cursor, // defaults to undefined if not set during decode + filter, // defaults to undefined if not set during decode + page: StringToPositiveNumber, // defaults to undefined if not set during decode + per_page: StringToPositiveNumber, // defaults to undefined if not set during decode + sort_field, // defaults to undefined if not set during decode + sort_order, // defaults to undefined if not set during decode + }) + ), +]); + +export type FindListItemSchemaPartial = Identity>; + +export type FindListItemSchema = RequiredKeepUndefined>; diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts new file mode 100644 index 0000000000000..c29ab4f5360dd --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { cursor, filter, sort_field, sort_order } from '../common/schemas'; +import { RequiredKeepUndefined } from '../../types'; +import { StringToPositiveNumber } from '../types/string_to_positive_number'; + +export const findListSchema = t.exact( + t.partial({ + cursor, // defaults to undefined if not set during decode + filter, // defaults to undefined if not set during decode + page: StringToPositiveNumber, // defaults to undefined if not set during decode + per_page: StringToPositiveNumber, // defaults to undefined if not set during decode + sort_field, // defaults to undefined if not set during decode + sort_order, // defaults to undefined if not set during decode + }) +); + +export type FindListSchemaPartial = t.TypeOf; + +export type FindListSchema = RequiredKeepUndefined>; diff --git a/x-pack/plugins/lists/common/schemas/request/index.ts b/x-pack/plugins/lists/common/schemas/request/index.ts index 0dbd9297b773e..7ab3d943f14da 100644 --- a/x-pack/plugins/lists/common/schemas/request/index.ts +++ b/x-pack/plugins/lists/common/schemas/request/index.ts @@ -15,6 +15,8 @@ export * from './delete_list_schema'; export * from './export_list_item_query_schema'; export * from './find_exception_list_item_schema'; export * from './find_exception_list_schema'; +export * from './find_list_item_schema'; +export * from './find_list_schema'; export * from './import_list_item_schema'; export * from './patch_list_item_schema'; export * from './patch_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts index 095fcd2f63b48..fded35dfd1cc9 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts @@ -8,13 +8,28 @@ import * as t from 'io-ts'; -import { id, item_id } from '../common/schemas'; +import { NamespaceType, id, item_id, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; export const readExceptionListItemSchema = t.partial({ id, item_id, + namespace_type, // defaults to 'single' if not set during decode }); export type ReadExceptionListItemSchemaPartial = t.TypeOf; + +// This type is used after a decode since some things are defaults after a decode. +export type ReadExceptionListItemSchemaPartialDecoded = Omit< + ReadExceptionListItemSchemaPartial, + 'namespace_type' +> & { + namespace_type: NamespaceType; +}; + +// This type is used after a decode since some things are defaults after a decode. +export type ReadExceptionListItemSchemaDecoded = RequiredKeepUndefined< + ReadExceptionListItemSchemaPartialDecoded +>; + export type ReadExceptionListItemSchema = RequiredKeepUndefined; diff --git a/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts index 5593e640f71ac..6b623ea8c0b9b 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts @@ -8,13 +8,28 @@ import * as t from 'io-ts'; -import { id, list_id } from '../common/schemas'; +import { NamespaceType, id, list_id, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; export const readExceptionListSchema = t.partial({ id, list_id, + namespace_type, // defaults to 'single' if not set during decode }); export type ReadExceptionListSchemaPartial = t.TypeOf; + +// This type is used after a decode since some things are defaults after a decode. +export type ReadExceptionListSchemaPartialDecoded = Omit< + ReadExceptionListSchemaPartial, + 'namespace_type' +> & { + namespace_type: NamespaceType; +}; + +// This type is used after a decode since some things are defaults after a decode. +export type ReadExceptionListSchemaDecoded = RequiredKeepUndefined< + ReadExceptionListSchemaPartialDecoded +>; + export type ReadExceptionListSchema = RequiredKeepUndefined; diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts index 162406a6d6589..3d66dad959c25 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts @@ -9,6 +9,7 @@ import * as t from 'io-ts'; import { + NamespaceType, Tags, _Tags, _tags, @@ -18,6 +19,7 @@ import { id, meta, name, + namespace_type, tags, } from '../common/schemas'; import { Identity, RequiredKeepUndefined } from '../../types'; @@ -40,6 +42,7 @@ export const updateExceptionListItemSchema = t.intersection([ id, // defaults to undefined if not set during decode item_id: t.union([t.string, t.undefined]), meta, // defaults to undefined if not set during decode + namespace_type, // defaults to 'single' if not set during decode tags, // defaults to empty array if not set during decode }) ), @@ -52,12 +55,12 @@ export type UpdateExceptionListItemSchema = RequiredKeepUndefined< t.TypeOf >; -// This type is used after a decode since the arrays turn into defaults of empty arrays -// and if a item_id is not specified it turns into a default GUID +// This type is used after a decode since some things are defaults after a decode. export type UpdateExceptionListItemSchemaDecoded = Identity< - Omit & { + Omit & { _tags: _Tags; tags: Tags; entries: EntriesArray; + namespace_type: NamespaceType; } >; diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts index e8a0dcd4994a2..76160c3419449 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts @@ -9,6 +9,7 @@ import * as t from 'io-ts'; import { + NamespaceType, Tags, _Tags, _tags, @@ -16,6 +17,7 @@ import { exceptionListType, meta, name, + namespace_type, tags, } from '../common/schemas'; import { Identity, RequiredKeepUndefined } from '../../types'; @@ -34,6 +36,7 @@ export const updateExceptionListSchema = t.intersection([ id: t.union([t.string, t.undefined]), // defaults to undefined if not set during decode list_id: t.union([t.string, t.undefined]), // defaults to undefined if not set during decode meta, // defaults to undefined if not set during decode + namespace_type, // defaults to 'single' if not set during decode tags, // defaults to empty array if not set during decode }) ), @@ -46,8 +49,9 @@ export type UpdateExceptionListSchema = RequiredKeepUndefined< // This type is used after a decode since the arrays turn into defaults of empty arrays. export type UpdateExceptionListSchemaDecoded = Identity< - Omit & { + Omit & { _tags: _Tags; tags: Tags; + namespace_type: NamespaceType; } >; diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts index 15e1c92c06d13..ab405c21d9c77 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts @@ -20,6 +20,7 @@ import { list_id, metaOrUndefined, name, + namespace_type, tags, tie_breaker_id, updated_at, @@ -41,6 +42,7 @@ export const exceptionListItemSchema = t.exact( list_id, meta: metaOrUndefined, name, + namespace_type, tags, tie_breaker_id, type: exceptionListItemType, diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts index 1940d94597dec..120ed31f87d0d 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts @@ -18,6 +18,7 @@ import { list_id, metaOrUndefined, name, + namespace_type, tags, tie_breaker_id, updated_at, @@ -35,6 +36,7 @@ export const exceptionListSchema = t.exact( list_id, meta: metaOrUndefined, name, + namespace_type, tags, tie_breaker_id, type: exceptionListType, diff --git a/x-pack/plugins/lists/common/schemas/response/found_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/response/found_list_item_schema.ts new file mode 100644 index 0000000000000..f792774cd0c12 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/found_list_item_schema.ts @@ -0,0 +1,25 @@ +/* + * 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 @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { cursor, page, per_page, total } from '../common/schemas'; + +import { listItemSchema } from './list_item_schema'; + +export const foundListItemSchema = t.exact( + t.type({ + cursor, + data: t.array(listItemSchema), + page, + per_page, + total, + }) +); + +export type FoundListItemSchema = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/response/found_list_schema.ts b/x-pack/plugins/lists/common/schemas/response/found_list_schema.ts new file mode 100644 index 0000000000000..aaf4a721d050d --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/found_list_schema.ts @@ -0,0 +1,25 @@ +/* + * 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 @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { cursor, page, per_page, total } from '../common/schemas'; + +import { listSchema } from './list_schema'; + +export const foundListSchema = t.exact( + t.type({ + cursor, + data: t.array(listSchema), + page, + per_page, + total, + }) +); + +export type FoundListSchema = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/response/index.ts b/x-pack/plugins/lists/common/schemas/response/index.ts index 213685d1183bd..fb6f17a896ddb 100644 --- a/x-pack/plugins/lists/common/schemas/response/index.ts +++ b/x-pack/plugins/lists/common/schemas/response/index.ts @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './list_item_schema'; -export * from './list_schema'; export * from './acknowledge_schema'; -export * from './list_item_index_exist_schema'; export * from './exception_list_schema'; +export * from './exception_list_item_schema'; export * from './found_exception_list_item_schema'; export * from './found_exception_list_schema'; -export * from './exception_list_item_schema'; +export * from './found_list_item_schema'; +export * from './found_list_schema'; +export * from './list_item_schema'; +export * from './list_schema'; +export * from './list_item_index_exist_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/list_schema.ts b/x-pack/plugins/lists/common/schemas/response/list_schema.ts index cad449766ceb4..4e664685db9c7 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_schema.ts @@ -37,3 +37,6 @@ export const listSchema = t.exact( ); export type ListSchema = t.TypeOf; + +export const listArraySchema = t.array(listSchema); +export type ListArraySchema = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/default_namespace.ts b/x-pack/plugins/lists/common/schemas/types/default_namespace.ts new file mode 100644 index 0000000000000..ebe2cd60cf6c8 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/default_namespace.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 * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +const namespaceType = t.keyof({ agnostic: null, single: null }); + +type NamespaceType = t.TypeOf; + +export type DefaultNamespaceC = t.Type; + +/** + * Types the DefaultNamespace as: + * - If null or undefined, then a default string/enumeration of "single" will be used. + */ +export const DefaultNamespace: DefaultNamespaceC = new t.Type< + NamespaceType, + NamespaceType, + unknown +>( + 'DefaultNamespace', + namespaceType.is, + (input): Either => + input == null ? t.success('single') : namespaceType.decode(input), + t.identity +); diff --git a/x-pack/plugins/lists/common/schemas/types/string_to_positive_number.ts b/x-pack/plugins/lists/common/schemas/types/string_to_positive_number.ts new file mode 100644 index 0000000000000..4b62d6c11d801 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/string_to_positive_number.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; +import { Either, either } from 'fp-ts/lib/Either'; + +export type StringToPositiveNumberC = t.Type; + +/** + * Types the StrongToPositiveNumber as: + * - If a string this converts the string into a number + * - Ensures it is a number (and not NaN) + * - Ensures it is positive number + */ +export const StringToPositiveNumber: StringToPositiveNumberC = new t.Type( + 'StringToPositiveNumber', + t.number.is, + (input, context): Either => { + return either.chain( + t.string.validate(input, context), + (numberAsString): Either => { + const stringAsNumber = +numberAsString; + if (numberAsString.trim().length === 0 || isNaN(stringAsNumber) || stringAsNumber <= 0) { + return t.failure(input, context); + } else { + return t.success(stringAsNumber); + } + } + ); + }, + String +); diff --git a/x-pack/plugins/lists/public/exceptions/api.test.ts b/x-pack/plugins/lists/public/exceptions/api.test.ts index cc172ee1e6109..3a61140e5621d 100644 --- a/x-pack/plugins/lists/public/exceptions/api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/api.test.ts @@ -68,7 +68,7 @@ describe('Exceptions Lists API', () => { }); expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', { body: - '{"_tags":["endpoint","process","malware","os:linux"],"created_at":"2020-04-23T00:19:13.289Z","created_by":"user_name","description":"This is a sample endpoint type exception","id":"1","list_id":"endpoint_list","meta":{},"name":"Sample Endpoint Exception List","tags":["user added string for a tag","malware"],"tie_breaker_id":"77fd1909-6786-428a-a671-30229a719c1f","type":"endpoint","updated_at":"2020-04-23T00:19:13.289Z","updated_by":"user_name"}', + '{"_tags":["endpoint","process","malware","os:linux"],"created_at":"2020-04-23T00:19:13.289Z","created_by":"user_name","description":"This is a sample endpoint type exception","id":"1","list_id":"endpoint_list","meta":{},"name":"Sample Endpoint Exception List","namespace_type":"single","tags":["user added string for a tag","malware"],"tie_breaker_id":"77fd1909-6786-428a-a671-30229a719c1f","type":"endpoint","updated_at":"2020-04-23T00:19:13.289Z","updated_by":"user_name"}', method: 'PUT', signal: abortCtrl.signal, }); @@ -112,7 +112,7 @@ describe('Exceptions Lists API', () => { }); expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', { body: - '{"_tags":["endpoint","process","malware","os:linux"],"comment":[],"created_at":"2020-04-23T00:19:13.289Z","created_by":"user_name","description":"This is a sample endpoint type exception","entries":[{"field":"actingProcess.file.signer","match":"Elastic, N.V.","operator":"included"},{"field":"event.category","match_any":["process","malware"],"operator":"included"}],"id":"1","item_id":"endpoint_list_item","list_id":"endpoint_list","meta":{},"name":"Sample Endpoint Exception List","tags":["user added string for a tag","malware"],"tie_breaker_id":"77fd1909-6786-428a-a671-30229a719c1f","type":"simple","updated_at":"2020-04-23T00:19:13.289Z","updated_by":"user_name"}', + '{"_tags":["endpoint","process","malware","os:linux"],"comment":[],"created_at":"2020-04-23T00:19:13.289Z","created_by":"user_name","description":"This is a sample endpoint type exception","entries":[{"field":"actingProcess.file.signer","match":"Elastic, N.V.","operator":"included"},{"field":"event.category","match_any":["process","malware"],"operator":"included"}],"id":"1","item_id":"endpoint_list_item","list_id":"endpoint_list","meta":{},"name":"Sample Endpoint Exception List","namespace_type":"single","tags":["user added string for a tag","malware"],"tie_breaker_id":"77fd1909-6786-428a-a671-30229a719c1f","type":"simple","updated_at":"2020-04-23T00:19:13.289Z","updated_by":"user_name"}', method: 'PUT', signal: abortCtrl.signal, }); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.tsx b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.tsx index a4390ac07a5a0..308d1cf4d1b17 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.tsx +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.tsx @@ -69,6 +69,7 @@ describe('useExceptionList', () => { list_id: 'endpoint_list', meta: {}, name: 'Sample Endpoint Exception List', + namespace_type: 'single', tags: ['user added string for a tag', 'malware'], tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f', type: 'simple', @@ -84,6 +85,7 @@ describe('useExceptionList', () => { list_id: 'endpoint_list', meta: {}, name: 'Sample Endpoint Exception List', + namespace_type: 'single', tags: ['user added string for a tag', 'malware'], tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f', type: 'endpoint', diff --git a/x-pack/plugins/lists/public/exceptions/mock.ts b/x-pack/plugins/lists/public/exceptions/mock.ts index 6980051238973..38a0e65992982 100644 --- a/x-pack/plugins/lists/public/exceptions/mock.ts +++ b/x-pack/plugins/lists/public/exceptions/mock.ts @@ -19,6 +19,7 @@ export const mockExceptionList: ExceptionListSchema = { list_id: 'endpoint_list', meta: {}, name: 'Sample Endpoint Exception List', + namespace_type: 'single', tags: ['user added string for a tag', 'malware'], tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f', type: 'endpoint', @@ -84,6 +85,7 @@ export const mockExceptionItem: ExceptionListItemSchema = { list_id: 'endpoint_list', meta: {}, name: 'Sample Endpoint Exception List', + namespace_type: 'single', tags: ['user added string for a tag', 'malware'], tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f', type: 'simple', diff --git a/x-pack/plugins/lists/server/index.ts b/x-pack/plugins/lists/server/index.ts index c1e577aa60195..33f58ba65d3c3 100644 --- a/x-pack/plugins/lists/server/index.ts +++ b/x-pack/plugins/lists/server/index.ts @@ -9,6 +9,10 @@ import { PluginInitializerContext } from '../../../../src/core/server'; import { ConfigSchema } from './config'; import { ListPlugin } from './plugin'; +// exporting these since its required at top level in siem plugin +export { ListClient } from './services/lists/list_client'; +export { ListPluginSetup } from './types'; + export const config = { schema: ConfigSchema }; export const plugin = (initializerContext: PluginInitializerContext): ListPlugin => new ListPlugin(initializerContext); diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts index ddcae137a961a..e914d816b5e91 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts @@ -39,6 +39,7 @@ export const createExceptionListItemRoute = (router: IRouter): void => { const siemResponse = buildSiemResponse(response); try { const { + namespace_type: namespaceType, name, _tags, tags, @@ -54,8 +55,7 @@ export const createExceptionListItemRoute = (router: IRouter): void => { const exceptionList = await exceptionLists.getExceptionList({ id: undefined, listId, - // TODO: Expose the name space type - namespaceType: 'single', + namespaceType, }); if (exceptionList == null) { return siemResponse.error({ @@ -66,8 +66,7 @@ export const createExceptionListItemRoute = (router: IRouter): void => { const exceptionListItem = await exceptionLists.getExceptionListItem({ id: undefined, itemId, - // TODO: Expose the name space type - namespaceType: 'single', + namespaceType, }); if (exceptionListItem != null) { return siemResponse.error({ @@ -84,8 +83,7 @@ export const createExceptionListItemRoute = (router: IRouter): void => { listId, meta, name, - // TODO: Expose the name space type - namespaceType: 'single', + namespaceType, tags, type, }); diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_route.ts index c8a1b080c16f6..9be6b72dcd255 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_route.ts @@ -38,13 +38,21 @@ export const createExceptionListRoute = (router: IRouter): void => { async (context, request, response) => { const siemResponse = buildSiemResponse(response); try { - const { name, _tags, tags, meta, description, list_id: listId, type } = request.body; + const { + name, + _tags, + tags, + meta, + namespace_type: namespaceType, + description, + list_id: listId, + type, + } = request.body; const exceptionLists = getExceptionListClient(context); const exceptionList = await exceptionLists.getExceptionList({ id: undefined, listId, - // TODO: Expose the name space type - namespaceType: 'single', + namespaceType, }); if (exceptionList != null) { return siemResponse.error({ @@ -58,8 +66,7 @@ export const createExceptionListRoute = (router: IRouter): void => { listId, meta, name, - // TODO: Expose the name space type - namespaceType: 'single', + namespaceType, tags, type, }); diff --git a/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts index e10ffab5359b0..2c91fe3c28681 100644 --- a/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts @@ -13,7 +13,11 @@ import { transformError, validate, } from '../siem_server_deps'; -import { deleteExceptionListItemSchema, exceptionListItemSchema } from '../../common/schemas'; +import { + DeleteExceptionListItemSchemaDecoded, + deleteExceptionListItemSchema, + exceptionListItemSchema, +} from '../../common/schemas'; import { getErrorMessageExceptionListItem, getExceptionListClient } from './utils'; @@ -25,14 +29,17 @@ export const deleteExceptionListItemRoute = (router: IRouter): void => { }, path: EXCEPTION_LIST_ITEM_URL, validate: { - query: buildRouteValidation(deleteExceptionListItemSchema), + query: buildRouteValidation< + typeof deleteExceptionListItemSchema, + DeleteExceptionListItemSchemaDecoded + >(deleteExceptionListItemSchema), }, }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); try { const exceptionLists = getExceptionListClient(context); - const { item_id: itemId, id } = request.query; + const { item_id: itemId, id, namespace_type: namespaceType } = request.query; if (itemId == null && id == null) { return siemResponse.error({ body: 'Either "item_id" or "id" needs to be defined in the request', @@ -42,7 +49,7 @@ export const deleteExceptionListItemRoute = (router: IRouter): void => { const deleted = await exceptionLists.deleteExceptionListItem({ id, itemId, - namespaceType: 'single', // TODO: Bubble this up + namespaceType, }); if (deleted == null) { return siemResponse.error({ diff --git a/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts b/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts index ef30ab6ab64c5..b4c67c0ab1418 100644 --- a/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts @@ -13,7 +13,11 @@ import { transformError, validate, } from '../siem_server_deps'; -import { deleteExceptionListSchema, exceptionListSchema } from '../../common/schemas'; +import { + DeleteExceptionListSchemaDecoded, + deleteExceptionListSchema, + exceptionListSchema, +} from '../../common/schemas'; import { getErrorMessageExceptionList, getExceptionListClient } from './utils'; @@ -25,25 +29,27 @@ export const deleteExceptionListRoute = (router: IRouter): void => { }, path: EXCEPTION_LIST_URL, validate: { - query: buildRouteValidation(deleteExceptionListSchema), + query: buildRouteValidation< + typeof deleteExceptionListSchema, + DeleteExceptionListSchemaDecoded + >(deleteExceptionListSchema), }, }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); try { const exceptionLists = getExceptionListClient(context); - const { list_id: listId, id } = request.query; + const { list_id: listId, id, namespace_type: namespaceType } = request.query; if (listId == null && id == null) { return siemResponse.error({ body: 'Either "list_id" or "id" needs to be defined in the request', statusCode: 400, }); } else { - // TODO: At the moment this will delete the list but we need to delete all the list items before deleting the list const deleted = await exceptionLists.deleteExceptionList({ id, listId, - namespaceType: 'single', + namespaceType, }); if (deleted == null) { return siemResponse.error({ diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts index 3b5503ffb9833..1820ffdeadb88 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts @@ -13,7 +13,11 @@ import { transformError, validate, } from '../siem_server_deps'; -import { findExceptionListItemSchema, foundExceptionListItemSchema } from '../../common/schemas'; +import { + FindExceptionListItemSchemaDecoded, + findExceptionListItemSchema, + foundExceptionListItemSchema, +} from '../../common/schemas'; import { getExceptionListClient } from './utils'; @@ -25,7 +29,10 @@ export const findExceptionListItemRoute = (router: IRouter): void => { }, path: `${EXCEPTION_LIST_ITEM_URL}/_find`, validate: { - query: buildRouteValidation(findExceptionListItemSchema), + query: buildRouteValidation< + typeof findExceptionListItemSchema, + FindExceptionListItemSchemaDecoded + >(findExceptionListItemSchema), }, }, async (context, request, response) => { @@ -35,6 +42,7 @@ export const findExceptionListItemRoute = (router: IRouter): void => { const { filter, list_id: listId, + namespace_type: namespaceType, page, per_page: perPage, sort_field: sortField, @@ -43,7 +51,7 @@ export const findExceptionListItemRoute = (router: IRouter): void => { const exceptionListItems = await exceptionLists.findExceptionListItem({ filter, listId, - namespaceType: 'single', // TODO: Bubble this up + namespaceType, page, perPage, sortField, diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_route.ts index 41c0c0760e03b..3181deda8b91d 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_route.ts @@ -13,7 +13,11 @@ import { transformError, validate, } from '../siem_server_deps'; -import { findExceptionListSchema, foundExceptionListSchema } from '../../common/schemas'; +import { + FindExceptionListSchemaDecoded, + findExceptionListSchema, + foundExceptionListSchema, +} from '../../common/schemas'; import { getExceptionListClient } from './utils'; @@ -25,7 +29,9 @@ export const findExceptionListRoute = (router: IRouter): void => { }, path: `${EXCEPTION_LIST_URL}/_find`, validate: { - query: buildRouteValidation(findExceptionListSchema), + query: buildRouteValidation( + findExceptionListSchema + ), }, }, async (context, request, response) => { @@ -35,13 +41,14 @@ export const findExceptionListRoute = (router: IRouter): void => { const { filter, page, + namespace_type: namespaceType, per_page: perPage, sort_field: sortField, sort_order: sortOrder, } = request.query; const exceptionListItems = await exceptionLists.findExceptionList({ filter, - namespaceType: 'single', // TODO: Bubble this up + namespaceType, page, perPage, sortField, diff --git a/x-pack/plugins/lists/server/routes/find_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_list_item_route.ts new file mode 100644 index 0000000000000..37b5fe44b919c --- /dev/null +++ b/x-pack/plugins/lists/server/routes/find_list_item_route.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { LIST_ITEM_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { findListItemSchema, foundListItemSchema } from '../../common/schemas'; +import { decodeCursor } from '../services/utils'; + +import { getListClient } from './utils'; + +export const findListItemRoute = (router: IRouter): void => { + router.get( + { + options: { + tags: ['access:lists'], + }, + path: `${LIST_ITEM_URL}/_find`, + validate: { + query: buildRouteValidation(findListItemSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const lists = getListClient(context); + const { + cursor, + filter: filterOrUndefined, + list_id: listId, + page: pageOrUndefined, + per_page: perPageOrUndefined, + sort_field: sortField, + sort_order: sortOrder, + } = request.query; + + const page = pageOrUndefined ?? 1; + const perPage = perPageOrUndefined ?? 20; + const filter = filterOrUndefined ?? ''; + const { + isValid, + errorMessage, + cursor: [currentIndexPosition, searchAfter], + } = decodeCursor({ + cursor, + page, + perPage, + sortField, + }); + + if (!isValid) { + return siemResponse.error({ + body: errorMessage, + statusCode: 400, + }); + } else { + const exceptionList = await lists.findListItem({ + currentIndexPosition, + filter, + listId, + page, + perPage, + searchAfter, + sortField, + sortOrder, + }); + if (exceptionList == null) { + return siemResponse.error({ + body: `list id: "${listId}" does not exist`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(exceptionList, foundListItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/find_list_route.ts b/x-pack/plugins/lists/server/routes/find_list_route.ts new file mode 100644 index 0000000000000..04b33e3d67075 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/find_list_route.ts @@ -0,0 +1,89 @@ +/* + * 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 { IRouter } from 'kibana/server'; + +import { LIST_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { findListSchema, foundListSchema } from '../../common/schemas'; +import { decodeCursor } from '../services/utils'; + +import { getListClient } from './utils'; + +export const findListRoute = (router: IRouter): void => { + router.get( + { + options: { + tags: ['access:lists'], + }, + path: `${LIST_URL}/_find`, + validate: { + query: buildRouteValidation(findListSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const lists = getListClient(context); + const { + cursor, + filter: filterOrUndefined, + page: pageOrUndefined, + per_page: perPageOrUndefined, + sort_field: sortField, + sort_order: sortOrder, + } = request.query; + + const page = pageOrUndefined ?? 1; + const perPage = perPageOrUndefined ?? 20; + const filter = filterOrUndefined ?? ''; + const { + isValid, + errorMessage, + cursor: [currentIndexPosition, searchAfter], + } = decodeCursor({ + cursor, + page, + perPage, + sortField, + }); + if (!isValid) { + return siemResponse.error({ + body: errorMessage, + statusCode: 400, + }); + } else { + const exceptionList = await lists.findList({ + currentIndexPosition, + filter, + page, + perPage, + searchAfter, + sortField, + sortOrder, + }); + const [validated, errors] = validate(exceptionList, foundListSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/index.ts b/x-pack/plugins/lists/server/routes/index.ts index 97f497bca7183..72117c46213fe 100644 --- a/x-pack/plugins/lists/server/routes/index.ts +++ b/x-pack/plugins/lists/server/routes/index.ts @@ -17,6 +17,8 @@ export * from './delete_list_route'; export * from './export_list_item_route'; export * from './find_exception_list_item_route'; export * from './find_exception_list_route'; +export * from './find_list_item_route'; +export * from './find_list_route'; export * from './import_list_item_route'; export * from './init_routes'; export * from './patch_list_item_route'; diff --git a/x-pack/plugins/lists/server/routes/init_routes.ts b/x-pack/plugins/lists/server/routes/init_routes.ts index 16f96d99505d8..e74fa471734b0 100644 --- a/x-pack/plugins/lists/server/routes/init_routes.ts +++ b/x-pack/plugins/lists/server/routes/init_routes.ts @@ -20,6 +20,8 @@ import { exportListItemRoute, findExceptionListItemRoute, findExceptionListRoute, + findListItemRoute, + findListRoute, importListItemRoute, patchListItemRoute, patchListRoute, @@ -41,6 +43,7 @@ export const initRoutes = (router: IRouter): void => { updateListRoute(router); deleteListRoute(router); patchListRoute(router); + findListRoute(router); // list items createListItemRoute(router); @@ -50,6 +53,7 @@ export const initRoutes = (router: IRouter): void => { patchListItemRoute(router); exportListItemRoute(router); importListItemRoute(router); + findListItemRoute(router); // indexes of lists createListIndexRoute(router); diff --git a/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts index 77d37373549c7..083d4d7a0d479 100644 --- a/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts @@ -13,7 +13,11 @@ import { transformError, validate, } from '../siem_server_deps'; -import { exceptionListItemSchema, readExceptionListItemSchema } from '../../common/schemas'; +import { + ReadExceptionListItemSchemaDecoded, + exceptionListItemSchema, + readExceptionListItemSchema, +} from '../../common/schemas'; import { getErrorMessageExceptionListItem, getExceptionListClient } from './utils'; @@ -25,20 +29,22 @@ export const readExceptionListItemRoute = (router: IRouter): void => { }, path: EXCEPTION_LIST_ITEM_URL, validate: { - query: buildRouteValidation(readExceptionListItemSchema), + query: buildRouteValidation< + typeof readExceptionListItemSchema, + ReadExceptionListItemSchemaDecoded + >(readExceptionListItemSchema), }, }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); try { - const { id, item_id: itemId } = request.query; + const { id, item_id: itemId, namespace_type: namespaceType } = request.query; const exceptionLists = getExceptionListClient(context); if (id != null || itemId != null) { const exceptionListItem = await exceptionLists.getExceptionListItem({ id, itemId, - // TODO: Bubble this up - namespaceType: 'single', + namespaceType, }); if (exceptionListItem == null) { return siemResponse.error({ diff --git a/x-pack/plugins/lists/server/routes/read_exception_list_route.ts b/x-pack/plugins/lists/server/routes/read_exception_list_route.ts index 1668124acdfce..c295f045b38c2 100644 --- a/x-pack/plugins/lists/server/routes/read_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/read_exception_list_route.ts @@ -13,7 +13,11 @@ import { transformError, validate, } from '../siem_server_deps'; -import { exceptionListSchema, readExceptionListSchema } from '../../common/schemas'; +import { + ReadExceptionListSchemaDecoded, + exceptionListSchema, + readExceptionListSchema, +} from '../../common/schemas'; import { getErrorMessageExceptionList, getExceptionListClient } from './utils'; @@ -25,20 +29,21 @@ export const readExceptionListRoute = (router: IRouter): void => { }, path: EXCEPTION_LIST_URL, validate: { - query: buildRouteValidation(readExceptionListSchema), + query: buildRouteValidation( + readExceptionListSchema + ), }, }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); try { - const { id, list_id: listId } = request.query; + const { id, list_id: listId, namespace_type: namespaceType } = request.query; const exceptionLists = getExceptionListClient(context); if (id != null || listId != null) { const exceptionList = await exceptionLists.getExceptionList({ id, listId, - // TODO: Bubble this up - namespaceType: 'single', + namespaceType, }); if (exceptionList == null) { return siemResponse.error({ diff --git a/x-pack/plugins/lists/server/routes/read_list_index_route.ts b/x-pack/plugins/lists/server/routes/read_list_index_route.ts index 248fc72666d70..21f539d97fc74 100644 --- a/x-pack/plugins/lists/server/routes/read_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_index_route.ts @@ -31,7 +31,7 @@ export const readListIndexRoute = (router: IRouter): void => { if (listIndexExists || listItemIndexExists) { const [validated, errors] = validate( - { list_index: listIndexExists, lists_item_index: listItemIndexExists }, + { list_index: listIndexExists, list_item_index: listItemIndexExists }, listItemIndexExistSchema ); if (errors != null) { diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts index 478225ee35eb8..14b97bbe15206 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts @@ -48,6 +48,7 @@ export const updateExceptionListItemRoute = (router: IRouter): void => { comment, entries, item_id: itemId, + namespace_type: namespaceType, tags, } = request.body; const exceptionLists = getExceptionListClient(context); @@ -60,7 +61,7 @@ export const updateExceptionListItemRoute = (router: IRouter): void => { itemId, meta, name, - namespaceType: 'single', // TODO: Bubble this up + namespaceType, tags, type, }); diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts index a112c7422b952..fe45d403c040f 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts @@ -38,7 +38,17 @@ export const updateExceptionListRoute = (router: IRouter): void => { async (context, request, response) => { const siemResponse = buildSiemResponse(response); try { - const { _tags, tags, name, description, id, list_id: listId, meta, type } = request.body; + const { + _tags, + tags, + name, + description, + id, + list_id: listId, + meta, + namespace_type: namespaceType, + type, + } = request.body; const exceptionLists = getExceptionListClient(context); if (id == null && listId == null) { return siemResponse.error({ @@ -53,7 +63,7 @@ export const updateExceptionListRoute = (router: IRouter): void => { listId, meta, name, - namespaceType: 'single', // TODO: Bubble this up + namespaceType, tags, type, }); diff --git a/x-pack/plugins/lists/server/scripts/delete_all_lists.sh b/x-pack/plugins/lists/server/scripts/delete_all_lists.sh deleted file mode 100755 index 5b65bb14414c7..0000000000000 --- a/x-pack/plugins/lists/server/scripts/delete_all_lists.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/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 - -# Example: ./delete_all_lists.sh -# https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete-by-query.html - - -# Delete all the main lists that have children items -curl -s -k \ - -H "Content-Type: application/json" \ - -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X POST ${ELASTICSEARCH_URL}/${KIBANA_INDEX}*/_delete_by_query \ - --data '{ - "query": { - "exists": { "field": "siem_list" } - } - }' \ - | jq . - -# Delete all the list children items as well -curl -s -k \ - -H "Content-Type: application/json" \ - -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X POST ${ELASTICSEARCH_URL}/${KIBANA_INDEX}*/_delete_by_query \ - --data '{ - "query": { - "exists": { "field": "siem_list_item" } - } - }' \ - | jq . diff --git a/x-pack/plugins/lists/server/scripts/delete_exception_list.sh b/x-pack/plugins/lists/server/scripts/delete_exception_list.sh index fe2ca501b4416..efdb6d03db60b 100755 --- a/x-pack/plugins/lists/server/scripts/delete_exception_list.sh +++ b/x-pack/plugins/lists/server/scripts/delete_exception_list.sh @@ -9,8 +9,12 @@ set -e ./check_env_variables.sh +NAMESPACE_TYPE=${2-single} + # Example: ./delete_exception_list.sh ${list_id} +# Example: ./delete_exception_list.sh ${list_id} single +# Example: ./delete_exception_list.sh ${list_id} agnostic curl -s -k \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X DELETE ${KIBANA_URL}${SPACE_URL}/api/exception_lists?list_id="$1" | jq . + -X DELETE "${KIBANA_URL}${SPACE_URL}/api/exception_lists?list_id=$1&namespace_type=${NAMESPACE_TYPE}" | jq . diff --git a/x-pack/plugins/lists/server/scripts/delete_exception_list_by_id.sh b/x-pack/plugins/lists/server/scripts/delete_exception_list_by_id.sh index a87881b385328..2eb4f93d93015 100755 --- a/x-pack/plugins/lists/server/scripts/delete_exception_list_by_id.sh +++ b/x-pack/plugins/lists/server/scripts/delete_exception_list_by_id.sh @@ -9,8 +9,12 @@ set -e ./check_env_variables.sh +NAMESPACE_TYPE=${2-single} + # Example: ./delete_exception_list_by_id.sh ${list_id} +# Example: ./delete_exception_list_by_id.sh ${list_id} single +# Example: ./delete_exception_list_by_id.sh ${list_id} agnostic curl -s -k \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X DELETE ${KIBANA_URL}${SPACE_URL}/api/exception_lists?id="$1" | jq . + -X DELETE "${KIBANA_URL}${SPACE_URL}/api/exception_lists?id=$1&namespace_type=${NAMESPACE_TYPE}" | jq . diff --git a/x-pack/plugins/lists/server/scripts/delete_exception_list_item.sh b/x-pack/plugins/lists/server/scripts/delete_exception_list_item.sh index 7e09452a23e11..7617b4c47b1bc 100755 --- a/x-pack/plugins/lists/server/scripts/delete_exception_list_item.sh +++ b/x-pack/plugins/lists/server/scripts/delete_exception_list_item.sh @@ -9,8 +9,12 @@ set -e ./check_env_variables.sh +NAMESPACE_TYPE=${2-single} + # Example: ./delete_exception_list_item.sh ${item_id} +# Example: ./delete_exception_list_item.sh ${item_id} single +# Example: ./delete_exception_list_item.sh ${item_id} agnostic curl -s -k \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X DELETE ${KIBANA_URL}${SPACE_URL}/api/exception_lists/items?item_id="$1" | jq . + -X DELETE "${KIBANA_URL}${SPACE_URL}/api/exception_lists/items?item_id=$1&namespace_type=${NAMESPACE_TYPE}" | jq . diff --git a/x-pack/plugins/lists/server/scripts/delete_exception_list_item_by_id.sh b/x-pack/plugins/lists/server/scripts/delete_exception_list_item_by_id.sh index bbfbc3135ddb8..0e18004909222 100755 --- a/x-pack/plugins/lists/server/scripts/delete_exception_list_item_by_id.sh +++ b/x-pack/plugins/lists/server/scripts/delete_exception_list_item_by_id.sh @@ -9,8 +9,12 @@ set -e ./check_env_variables.sh +NAMESPACE_TYPE=${2-single} + # Example: ./delete_exception_list_item_by_id.sh ${list_id} +# Example: ./delete_exception_list_item_by_id.sh ${list_id} single +# Example: ./delete_exception_list_item_by_id.sh ${list_id} agnostic curl -s -k \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X DELETE ${KIBANA_URL}${SPACE_URL}/api/exception_lists/items?id="$1" | jq . + -X DELETE "${KIBANA_URL}${SPACE_URL}/api/exception_lists/items?id=$1&namespace_type=${NAMESPACE_TYPE}" | jq . diff --git a/x-pack/plugins/lists/server/scripts/delete_list.sh b/x-pack/plugins/lists/server/scripts/delete_list.sh index ce9fdd6aa21d4..95aa8eddbdf8d 100755 --- a/x-pack/plugins/lists/server/scripts/delete_list.sh +++ b/x-pack/plugins/lists/server/scripts/delete_list.sh @@ -13,4 +13,4 @@ set -e curl -s -k \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X DELETE ${KIBANA_URL}${SPACE_URL}/api/lists?id="$1" | jq . + -X DELETE "${KIBANA_URL}${SPACE_URL}/api/lists?id=$1" | jq . diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_agnostic.json b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_agnostic.json new file mode 100644 index 0000000000000..4121b13880660 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_agnostic.json @@ -0,0 +1,9 @@ +{ + "list_id": "endpoint_list", + "_tags": ["endpoint", "process", "malware", "os:linux"], + "tags": ["user added string for a tag", "malware"], + "type": "endpoint", + "description": "This is a sample agnostic endpoint type exception", + "name": "Sample Endpoint Exception List", + "namespace_type": "agnostic" +} diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_agnostic.json b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_agnostic.json new file mode 100644 index 0000000000000..db0b11480b81a --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_agnostic.json @@ -0,0 +1,22 @@ +{ + "list_id": "endpoint_list", + "item_id": "endpoint_list_item", + "_tags": ["endpoint", "process", "malware", "os:linux"], + "tags": ["user added string for a tag", "malware"], + "type": "simple", + "description": "This is a sample agnostic endpoint type exception", + "name": "Sample Endpoint Exception List", + "namespace_type": "agnostic", + "entries": [ + { + "field": "actingProcess.file.signer", + "operator": "included", + "match": "Elastic, N.V." + }, + { + "field": "event.category", + "operator": "included", + "match_any": ["process", "malware"] + } + ] +} diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_agnostic.json b/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_agnostic.json new file mode 100644 index 0000000000000..72ddd15ebee47 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_agnostic.json @@ -0,0 +1,16 @@ +{ + "item_id": "endpoint_list_item", + "_tags": ["endpoint", "process", "malware", "os:windows"], + "tags": ["user added string for a tag", "malware"], + "type": "simple", + "description": "This is a sample agnostic change here this list", + "name": "Sample Endpoint Exception List update change", + "namespace_type": "agnostic", + "entries": [ + { + "field": "event.category", + "operator": "included", + "match_any": ["process", "malware"] + } + ] +} diff --git a/x-pack/plugins/lists/server/scripts/find_exception_list_items.sh b/x-pack/plugins/lists/server/scripts/find_exception_list_items.sh index 85c5b0e518fab..e3f21da56d1b7 100755 --- a/x-pack/plugins/lists/server/scripts/find_exception_list_items.sh +++ b/x-pack/plugins/lists/server/scripts/find_exception_list_items.sh @@ -10,7 +10,11 @@ set -e ./check_env_variables.sh LIST_ID=${1:-endpoint_list} +NAMESPACE_TYPE=${2-single} + # Example: ./find_exception_list_items.sh {list-id} +# Example: ./find_exception_list_items.sh {list-id} single +# Example: ./find_exception_list_items.sh {list-id} agnostic curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}${SPACE_URL}/api/exception_lists/items/_find?list_id=${LIST_ID} | jq . + -X GET "${KIBANA_URL}${SPACE_URL}/api/exception_lists/items/_find?list_id=${LIST_ID}&namespace_type=${NAMESPACE_TYPE}" | jq . diff --git a/x-pack/plugins/lists/server/scripts/find_exception_list_items_by_filter.sh b/x-pack/plugins/lists/server/scripts/find_exception_list_items_by_filter.sh new file mode 100755 index 0000000000000..57313275ccd0e --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/find_exception_list_items_by_filter.sh @@ -0,0 +1,29 @@ +#!/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 + +LIST_ID=${1:-endpoint_list} +FILTER=${2:-'exception-list.attributes.name:%20Sample%20Endpoint%20Exception%20List'} +NAMESPACE_TYPE=${3-single} + +# The %20 is just an encoded space that is typical of URL's. +# The %22 is just an encoded quote of " +# Table of them for testing if needed: https://www.w3schools.com/tags/ref_urlencode.asp + +# Example: ./find_exception_list_items_by_filter.sh endpoint_list exception-list.attributes.name:%20Sample%20Endpoint%20Exception%20List +# Example: ./find_exception_list_items_by_filter.sh endpoint_list exception-list.attributes.name:%20Sample%20Endpoint%20Exception%20List single +# Example: ./find_exception_list_items_by_filter.sh endpoint_list exception-list.attributes.name:%20Sample%20Endpoint%20Exception%20List agnostic +# +# Example: ./find_exception_list_items_by_filter.sh endpoint_list exception-list.attributes.entries.field:actingProcess.file.signer +# Example: ./find_exception_list_items_by_filter.sh endpoint_list "exception-list.attributes.entries.field:actingProcess.file.signe*" +# Example: ./find_exception_list_items_by_filter.sh endpoint_list "exception-list.attributes.entries.match:Elastic*%20AND%20exception-list.attributes.entries.field:actingProcess.file.signe*" +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/exception_lists/items/_find?list_id=${LIST_ID}&filter=${FILTER}&namespace_type=${NAMESPACE_TYPE}" | jq . diff --git a/x-pack/plugins/lists/server/scripts/find_exception_lists.sh b/x-pack/plugins/lists/server/scripts/find_exception_lists.sh index a1ee184b3e5bb..d3420e53343a3 100755 --- a/x-pack/plugins/lists/server/scripts/find_exception_lists.sh +++ b/x-pack/plugins/lists/server/scripts/find_exception_lists.sh @@ -9,7 +9,11 @@ set -e ./check_env_variables.sh +NAMESPACE_TYPE=${1-single} + # Example: ./find_exception_lists.sh {list-id} +# Example: ./find_exception_lists.sh {list-id} single +# Example: ./find_exception_lists.sh {list-id} agnostic curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}${SPACE_URL}/api/exception_lists/_find | jq . + -X GET "${KIBANA_URL}${SPACE_URL}/api/exception_lists/_find?namespace_type=${NAMESPACE_TYPE}" | jq . diff --git a/x-pack/plugins/lists/server/scripts/find_exception_lists_by_filter.sh b/x-pack/plugins/lists/server/scripts/find_exception_lists_by_filter.sh new file mode 100755 index 0000000000000..3f5600af76b83 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/find_exception_lists_by_filter.sh @@ -0,0 +1,26 @@ +#!/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 + +FILTER=${1:-'exception-list.attributes.name:%20Sample%20Endpoint%20Exception%20List'} +NAMESPACE_TYPE=${2-single} + +# The %20 is just an encoded space that is typical of URL's. +# The %22 is just an encoded quote of " +# Table of them for testing if needed: https://www.w3schools.com/tags/ref_urlencode.asp + +# Example get all lists by a particular name: +# ./find_exception_lists_by_filter.sh exception-list.attributes.name:%20Sample%20Endpoint%20Exception%20List +# ./find_exception_lists_by_filter.sh exception-list.attributes.tags:%20malware +# ./find_exception_lists_by_filter.sh exception-list.attributes.tags:%20malware single +# ./find_exception_lists_by_filter.sh exception-list.attributes.tags:%20malware agnostic +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/exception_lists/_find?filter=${FILTER}&namespace_type=${NAMESPACE_TYPE}" | jq . diff --git a/x-pack/plugins/lists/server/scripts/find_list_items.sh b/x-pack/plugins/lists/server/scripts/find_list_items.sh new file mode 100755 index 0000000000000..c4a610e313fa8 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/find_list_items.sh @@ -0,0 +1,19 @@ +#!/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 + +PAGE=${1-1} +PER_PAGE=${2-20} +LIST_ID=${3-list-ip} + +# Example: ./find_list_items.sh 1 20 list-ip +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/items/_find?list_id=${LIST_ID}&page=${PAGE}&per_page=${PER_PAGE}" | jq . diff --git a/x-pack/plugins/lists/server/scripts/find_list_items_with_cursor.sh b/x-pack/plugins/lists/server/scripts/find_list_items_with_cursor.sh new file mode 100755 index 0000000000000..3fd5178b2d9b1 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/find_list_items_with_cursor.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +PAGE=${1-1} +PER_PAGE=${2-20} +LIST_ID=${3-list-ip} +CURSOR=${4-invalid} + +# Example: +# ./find_list_items.sh 1 20 | jq .cursor +# Copy the cursor into the argument below like so +# ./find_list_items_with_cursor.sh 1 10 list-ip eyJwYWdlX2luZGV4IjoyMCwic2VhcmNoX2FmdGVyIjpbIjAyZDZlNGY3LWUzMzAtNGZkYi1iNTY0LTEzZjNiOTk1MjRiYSJdfQ== +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/items/_find?list_id=${LIST_ID}&page=${PAGE}&per_page=${PER_PAGE}&cursor=${CURSOR}" | jq . diff --git a/x-pack/plugins/lists/server/scripts/find_list_items_with_sort.sh b/x-pack/plugins/lists/server/scripts/find_list_items_with_sort.sh new file mode 100755 index 0000000000000..dcea698be231d --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/find_list_items_with_sort.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +PAGE=${1-1} +PER_PAGE=${2-20} +SORT_FIELD=${3-value} +SORT_ORDER=${4-asc} +LIST_ID=${5-list-ip} + +# Example: ./find_list_items_with_sort.sh 1 20 value asc list-ip +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/items/_find?list_id=${LIST_ID}&page=${PAGE}&per_page=${PER_PAGE}&sort_field=${SORT_FIELD}&sort_order=${SORT_ORDER}" | jq . diff --git a/x-pack/plugins/lists/server/scripts/find_list_items_with_sort_cursor.sh b/x-pack/plugins/lists/server/scripts/find_list_items_with_sort_cursor.sh new file mode 100755 index 0000000000000..07b67a9bd1c5f --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/find_list_items_with_sort_cursor.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 + +PAGE=${1-1} +PER_PAGE=${2-20} +SORT_FIELD=${3-value} +SORT_ORDER=${4-asc} +LIST_ID=${5-list-ip} +CURSOR=${6-invalid} + +# Example: ./find_list_items_with_sort_cursor.sh 1 20 value asc list-ip +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/items/_find?list_id=${LIST_ID}&page=${PAGE}&per_page=${PER_PAGE}&sort_field=${SORT_FIELD}&sort_order=${SORT_ORDER}&cursor=${CURSOR}" | jq . diff --git a/x-pack/plugins/lists/server/scripts/find_lists.sh b/x-pack/plugins/lists/server/scripts/find_lists.sh new file mode 100755 index 0000000000000..6ff673c91cad4 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/find_lists.sh @@ -0,0 +1,18 @@ +#!/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 + +PAGE=${1-1} +PER_PAGE=${2-20} + +# Example: ./find_lists.sh 1 20 +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/_find?page=${PAGE}&per_page=${PER_PAGE}" | jq . diff --git a/x-pack/plugins/lists/server/scripts/find_lists_with_cursor.sh b/x-pack/plugins/lists/server/scripts/find_lists_with_cursor.sh new file mode 100755 index 0000000000000..a3bff5c37d090 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/find_lists_with_cursor.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 + +PAGE=${1-1} +PER_PAGE=${2-20} +CURSOR=${3-invalid} + +# Example: +# ./find_lists.sh 1 20 | jq .cursor +# Copy the cursor into the argument below like so +# ./find_lists_with_cursor.sh 1 10 eyJwYWdlX2luZGV4IjoyMCwic2VhcmNoX2FmdGVyIjpbIjAyZDZlNGY3LWUzMzAtNGZkYi1iNTY0LTEzZjNiOTk1MjRiYSJdfQ== +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/_find?page=${PAGE}&per_page=${PER_PAGE}&cursor=${CURSOR}" | jq . diff --git a/x-pack/plugins/lists/server/scripts/find_lists_with_filter.sh b/x-pack/plugins/lists/server/scripts/find_lists_with_filter.sh new file mode 100755 index 0000000000000..1919d13fdf793 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/find_lists_with_filter.sh @@ -0,0 +1,18 @@ +#!/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 + +PAGE=${1-1} +PER_PAGE=${2-20} +FILTER=${3-type:ip} +# Example: ./find_lists_with_filter.sh 1 20 type:ip +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/_find?page=${PAGE}&per_page=${PER_PAGE}&filter=${FILTER}" | jq . diff --git a/x-pack/plugins/lists/server/scripts/find_lists_with_sort.sh b/x-pack/plugins/lists/server/scripts/find_lists_with_sort.sh new file mode 100755 index 0000000000000..411f3a396cdb3 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/find_lists_with_sort.sh @@ -0,0 +1,20 @@ +#!/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 + +PAGE=${1-1} +PER_PAGE=${2-20} +SORT_FIELD=${3-name} +SORT_ORDER=${4-asc} + +# Example: ./find_lists_with_sort.sh 1 20 name asc +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/_find?page=${PAGE}&per_page=${PER_PAGE}&sort_field=${SORT_FIELD}&sort_order=${SORT_ORDER}" | jq . diff --git a/x-pack/plugins/lists/server/scripts/find_lists_with_sort_cursor.sh b/x-pack/plugins/lists/server/scripts/find_lists_with_sort_cursor.sh new file mode 100755 index 0000000000000..c706eb68869ef --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/find_lists_with_sort_cursor.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +PAGE=${1-1} +PER_PAGE=${2-20} +SORT_FIELD=${3-name} +SORT_ORDER=${4-asc} +CURSOR=${5-invalid} + +# Example: ./find_lists_with_sort_cursor.sh 1 20 name asc +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/_find?page=${PAGE}&per_page=${PER_PAGE}&sort_field=${SORT_FIELD}&sort_order=${SORT_ORDER}&cursor=${CURSOR}" | jq . diff --git a/x-pack/plugins/lists/server/scripts/get_exception_list.sh b/x-pack/plugins/lists/server/scripts/get_exception_list.sh index 34e6de2576879..9aa15a08dec14 100755 --- a/x-pack/plugins/lists/server/scripts/get_exception_list.sh +++ b/x-pack/plugins/lists/server/scripts/get_exception_list.sh @@ -9,7 +9,10 @@ set -e ./check_env_variables.sh -# Example: ./get_exception_list.sh {id} +NAMESPACE_TYPE=${2-single} + +# Example: ./get_exception_list.sh {id} single +# Example: ./get_exception_list.sh {id} agnostic curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}${SPACE_URL}/api/exception_lists?list_id="$1" | jq . + -X GET "${KIBANA_URL}${SPACE_URL}/api/exception_lists?list_id=$1&namespace_type=${NAMESPACE_TYPE}" | jq . diff --git a/x-pack/plugins/lists/server/scripts/get_exception_list_by_id.sh b/x-pack/plugins/lists/server/scripts/get_exception_list_by_id.sh index 0420a1f702328..bcd6721b6fd00 100755 --- a/x-pack/plugins/lists/server/scripts/get_exception_list_by_id.sh +++ b/x-pack/plugins/lists/server/scripts/get_exception_list_by_id.sh @@ -9,7 +9,9 @@ set -e ./check_env_variables.sh +NAMESPACE_TYPE=${2-single} + # Example: ./get_exception_list_by_id.sh {id} curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}${SPACE_URL}/api/exception_lists?id="$1" | jq . + -X GET "${KIBANA_URL}${SPACE_URL}/api/exception_lists?id=$1&namespace_type=${NAMESPACE_TYPE}" | jq . diff --git a/x-pack/plugins/lists/server/scripts/get_exception_list_item.sh b/x-pack/plugins/lists/server/scripts/get_exception_list_item.sh index ac8337aab8368..141bbe60f193f 100755 --- a/x-pack/plugins/lists/server/scripts/get_exception_list_item.sh +++ b/x-pack/plugins/lists/server/scripts/get_exception_list_item.sh @@ -9,7 +9,11 @@ set -e ./check_env_variables.sh +NAMESPACE_TYPE=${2-single} + # Example: ./get_exception_list_item.sh {id} +# Example: ./get_exception_list_item.sh {id} single +# Example: ./get_exception_list_item.sh {id} agnostic curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}${SPACE_URL}/api/exception_lists/items?item_id="$1" | jq . + -X GET "${KIBANA_URL}${SPACE_URL}/api/exception_lists/items?item_id=$1&namespace_type=${NAMESPACE_TYPE}" | jq . diff --git a/x-pack/plugins/lists/server/scripts/get_exception_list_item_by_id.sh b/x-pack/plugins/lists/server/scripts/get_exception_list_item_by_id.sh index 575a529c69906..97a90c28daebd 100755 --- a/x-pack/plugins/lists/server/scripts/get_exception_list_item_by_id.sh +++ b/x-pack/plugins/lists/server/scripts/get_exception_list_item_by_id.sh @@ -9,7 +9,11 @@ set -e ./check_env_variables.sh +NAMESPACE_TYPE=${2-single} + # Example: ./get_exception_list_item_by_id.sh {id} +# Example: ./get_exception_list_item_by_id.sh {id} single +# Example: ./get_exception_list_item_by_id.sh {id} agnostic curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}${SPACE_URL}/api/exception_lists/items?id="$1" | jq . + -X GET "${KIBANA_URL}${SPACE_URL}/api/exception_lists/items?id=$1&namespace_type=${NAMESPACE_TYPE}" | jq . diff --git a/x-pack/plugins/lists/server/scripts/lists/new/list_auto_id.json b/x-pack/plugins/lists/server/scripts/lists/new/list_auto_id.json new file mode 100644 index 0000000000000..ef48ba8f67009 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/new/list_auto_id.json @@ -0,0 +1,5 @@ +{ + "name": "Simple list with a type of ip and an auto created id", + "description": "list with an auto created id", + "type": "ip" +} diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts index 7ba832e72bb8e..c6d4bc006ef0b 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts @@ -15,12 +15,12 @@ import { ListId, MetaOrUndefined, Name, + NamespaceType, Tags, _Tags, } from '../../../common/schemas'; import { getSavedObjectType, transformSavedObjectToExceptionList } from './utils'; -import { NamespaceType } from './types'; interface CreateExceptionListOptions { _tags: _Tags; @@ -68,5 +68,5 @@ export const createExceptionList = async ({ type, updated_by: user, }); - return transformSavedObjectToExceptionList({ savedObject }); + return transformSavedObjectToExceptionList({ namespaceType, savedObject }); }; diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts index 4a6dc1da97854..44e87ab06f52b 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts @@ -18,12 +18,12 @@ import { ListId, MetaOrUndefined, Name, + NamespaceType, Tags, _Tags, } from '../../../common/schemas'; import { getSavedObjectType, transformSavedObjectToExceptionListItem } from './utils'; -import { NamespaceType } from './types'; interface CreateExceptionListItemOptions { _tags: _Tags; @@ -77,5 +77,5 @@ export const createExceptionListItem = async ({ type, updated_by: user, }); - return transformSavedObjectToExceptionListItem({ savedObject }); + return transformSavedObjectToExceptionListItem({ namespaceType, savedObject }); }; diff --git a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts index 6904438c8d275..afeed6b5e2cde 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts @@ -6,10 +6,14 @@ import { SavedObjectsClientContract } from 'kibana/server'; -import { ExceptionListSchema, IdOrUndefined, ListIdOrUndefined } from '../../../common/schemas'; +import { + ExceptionListSchema, + IdOrUndefined, + ListIdOrUndefined, + NamespaceType, +} from '../../../common/schemas'; import { getSavedObjectType } from './utils'; -import { NamespaceType } from './types'; import { getExceptionList } from './get_exception_list'; import { deleteExceptionListItemByList } from './delete_exception_list_items_by_list'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts index 3b2d991281cd6..8dce1f1f79e35 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts @@ -6,10 +6,14 @@ import { SavedObjectsClientContract } from 'kibana/server'; -import { ExceptionListItemSchema, IdOrUndefined, ItemIdOrUndefined } from '../../../common/schemas'; +import { + ExceptionListItemSchema, + IdOrUndefined, + ItemIdOrUndefined, + NamespaceType, +} from '../../../common/schemas'; import { getSavedObjectType } from './utils'; -import { NamespaceType } from './types'; import { getExceptionListItem } from './get_exception_list_item'; interface DeleteExceptionListItemOptions { diff --git a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts index 31bf1ffacbbb2..e835ffae02c9e 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts @@ -5,10 +5,9 @@ */ import { SavedObjectsClientContract } from '../../../../../../src/core/server/'; -import { ListId } from '../../../common/schemas'; +import { ListId, NamespaceType } from '../../../common/schemas'; import { findExceptionListItem } from './find_exception_list_item'; -import { NamespaceType } from './types'; import { getSavedObjectType } from './utils'; const PER_PAGE = 100; diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts index 6e71ed1b3e59d..efd117a3c38f4 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts @@ -7,6 +7,7 @@ import { SavedObjectsClientContract } from 'kibana/server'; import { + ExceptionListItemSchema, ExceptionListSchema, FoundExceptionListItemSchema, FoundExceptionListSchema, @@ -59,7 +60,7 @@ export class ExceptionListClient { itemId, id, namespaceType, - }: GetExceptionListItemOptions): Promise => { + }: GetExceptionListItemOptions): Promise => { const { savedObjectsClient } = this; return getExceptionListItem({ id, itemId, namespaceType, savedObjectsClient }); }; @@ -142,7 +143,7 @@ export class ExceptionListClient { namespaceType, tags, type, - }: CreateExceptionListItemOptions): Promise => { + }: CreateExceptionListItemOptions): Promise => { const { savedObjectsClient, user } = this; return createExceptionListItem({ _tags, @@ -173,7 +174,7 @@ export class ExceptionListClient { namespaceType, tags, type, - }: UpdateExceptionListItemOptions): Promise => { + }: UpdateExceptionListItemOptions): Promise => { const { savedObjectsClient, user } = this; return updateExceptionListItem({ _tags, @@ -196,7 +197,7 @@ export class ExceptionListClient { id, itemId, namespaceType, - }: DeleteExceptionListItemOptions): Promise => { + }: DeleteExceptionListItemOptions): Promise => { const { savedObjectsClient } = this; return deleteExceptionListItem({ id, diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts index cecd6bf3397a7..0ac543afee9f9 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts @@ -14,6 +14,7 @@ import { EntriesArrayOrUndefined, ExceptionListType, ExceptionListTypeOrUndefined, + FilterOrUndefined, IdOrUndefined, ItemId, ItemIdOrUndefined, @@ -22,14 +23,17 @@ import { MetaOrUndefined, Name, NameOrUndefined, + NamespaceType, + PageOrUndefined, + PerPageOrUndefined, + SortFieldOrUndefined, + SortOrderOrUndefined, Tags, TagsOrUndefined, _Tags, _TagsOrUndefined, } from '../../../common/schemas'; -import { NamespaceType } from './types'; - export interface ConstructorOptions { user: string; savedObjectsClient: SavedObjectsClientContract; @@ -113,18 +117,18 @@ export interface UpdateExceptionListItemOptions { export interface FindExceptionListItemOptions { listId: ListId; namespaceType: NamespaceType; - filter: string | undefined; - perPage: number | undefined; - page: number | undefined; - sortField: string | undefined; - sortOrder: string | undefined; + filter: FilterOrUndefined; + perPage: PerPageOrUndefined; + page: PageOrUndefined; + sortField: SortFieldOrUndefined; + sortOrder: SortOrderOrUndefined; } export interface FindExceptionListOptions { namespaceType: NamespaceType; - filter: string | undefined; - perPage: number | undefined; - page: number | undefined; - sortField: string | undefined; - sortOrder: string | undefined; + filter: FilterOrUndefined; + perPage: PerPageOrUndefined; + page: PageOrUndefined; + sortField: SortFieldOrUndefined; + sortOrder: SortOrderOrUndefined; } diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts index 539dda673208b..6a8fbf3306971 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts @@ -6,20 +6,28 @@ import { SavedObjectsClientContract } from 'kibana/server'; -import { ExceptionListSoSchema, FoundExceptionListSchema } from '../../../common/schemas'; +import { + ExceptionListSoSchema, + FilterOrUndefined, + FoundExceptionListSchema, + NamespaceType, + PageOrUndefined, + PerPageOrUndefined, + SortFieldOrUndefined, + SortOrderOrUndefined, +} from '../../../common/schemas'; import { SavedObjectType } from '../../saved_objects'; import { getSavedObjectType, transformSavedObjectsToFounExceptionList } from './utils'; -import { NamespaceType } from './types'; interface FindExceptionListOptions { namespaceType: NamespaceType; savedObjectsClient: SavedObjectsClientContract; - filter: string | undefined; - perPage: number | undefined; - page: number | undefined; - sortField: string | undefined; - sortOrder: string | undefined; + filter: FilterOrUndefined; + perPage: PerPageOrUndefined; + page: PageOrUndefined; + sortField: SortFieldOrUndefined; + sortOrder: SortOrderOrUndefined; } export const findExceptionList = async ({ @@ -40,14 +48,14 @@ export const findExceptionList = async ({ sortOrder, type: savedObjectType, }); - return transformSavedObjectsToFounExceptionList({ savedObjectsFindResponse }); + return transformSavedObjectsToFounExceptionList({ namespaceType, savedObjectsFindResponse }); }; export const getExceptionListFilter = ({ filter, savedObjectType, }: { - filter: string | undefined; + filter: FilterOrUndefined; savedObjectType: SavedObjectType; }): string => { if (filter == null) { diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts index d635cafbd3b1b..c3b09a5f44b15 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts @@ -8,24 +8,29 @@ import { SavedObjectsClientContract } from 'kibana/server'; import { ExceptionListSoSchema, + FilterOrUndefined, FoundExceptionListItemSchema, ListId, + NamespaceType, + PageOrUndefined, + PerPageOrUndefined, + SortFieldOrUndefined, + SortOrderOrUndefined, } from '../../../common/schemas'; import { SavedObjectType } from '../../saved_objects'; import { getSavedObjectType, transformSavedObjectsToFounExceptionListItem } from './utils'; -import { NamespaceType } from './types'; import { getExceptionList } from './get_exception_list'; interface FindExceptionListItemOptions { listId: ListId; namespaceType: NamespaceType; savedObjectsClient: SavedObjectsClientContract; - filter: string | undefined; - perPage: number | undefined; - page: number | undefined; - sortField: string | undefined; - sortOrder: string | undefined; + filter: FilterOrUndefined; + perPage: PerPageOrUndefined; + page: PageOrUndefined; + sortField: SortFieldOrUndefined; + sortOrder: SortOrderOrUndefined; } export const findExceptionListItem = async ({ @@ -56,7 +61,10 @@ export const findExceptionListItem = async ({ sortOrder, type: savedObjectType, }); - return transformSavedObjectsToFounExceptionListItem({ savedObjectsFindResponse }); + return transformSavedObjectsToFounExceptionListItem({ + namespaceType, + savedObjectsFindResponse, + }); } }; @@ -66,7 +74,7 @@ export const getExceptionListItemFilter = ({ savedObjectType, }: { listId: ListId; - filter: string | undefined; + filter: FilterOrUndefined; savedObjectType: SavedObjectType; }): string => { if (filter == null) { diff --git a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts index 8b28443b4e30c..8f511d140b0ff 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts @@ -13,10 +13,10 @@ import { ExceptionListSoSchema, IdOrUndefined, ListIdOrUndefined, + NamespaceType, } from '../../../common/schemas'; import { getSavedObjectType, transformSavedObjectToExceptionList } from './utils'; -import { NamespaceType } from './types'; interface GetExceptionListOptions { id: IdOrUndefined; @@ -35,7 +35,7 @@ export const getExceptionList = async ({ if (id != null) { try { const savedObject = await savedObjectsClient.get(savedObjectType, id); - return transformSavedObjectToExceptionList({ savedObject }); + return transformSavedObjectToExceptionList({ namespaceType, savedObject }); } catch (err) { if (SavedObjectsErrorHelpers.isNotFoundError(err)) { return null; @@ -54,7 +54,10 @@ export const getExceptionList = async ({ type: savedObjectType, }); if (savedObject.saved_objects[0] != null) { - return transformSavedObjectToExceptionList({ savedObject: savedObject.saved_objects[0] }); + return transformSavedObjectToExceptionList({ + namespaceType, + savedObject: savedObject.saved_objects[0], + }); } else { return null; } diff --git a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts index 7ef3e4af3d604..d7efdc054c48c 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts @@ -13,10 +13,10 @@ import { ExceptionListSoSchema, IdOrUndefined, ItemIdOrUndefined, + NamespaceType, } from '../../../common/schemas'; import { getSavedObjectType, transformSavedObjectToExceptionListItem } from './utils'; -import { NamespaceType } from './types'; interface GetExceptionListItemOptions { id: IdOrUndefined; @@ -35,7 +35,7 @@ export const getExceptionListItem = async ({ if (id != null) { try { const savedObject = await savedObjectsClient.get(savedObjectType, id); - return transformSavedObjectToExceptionListItem({ savedObject }); + return transformSavedObjectToExceptionListItem({ namespaceType, savedObject }); } catch (err) { if (SavedObjectsErrorHelpers.isNotFoundError(err)) { return null; @@ -55,6 +55,7 @@ export const getExceptionListItem = async ({ }); if (savedObject.saved_objects[0] != null) { return transformSavedObjectToExceptionListItem({ + namespaceType, savedObject: savedObject.saved_objects[0], }); } else { diff --git a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts index 6c5ccb5e1f2fd..e4d6718ddc29f 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts @@ -15,12 +15,12 @@ import { ListIdOrUndefined, MetaOrUndefined, NameOrUndefined, + NamespaceType, TagsOrUndefined, _TagsOrUndefined, } from '../../../common/schemas'; import { getSavedObjectType, transformSavedObjectUpdateToExceptionList } from './utils'; -import { NamespaceType } from './types'; import { getExceptionList } from './get_exception_list'; interface UpdateExceptionListOptions { @@ -69,6 +69,6 @@ export const updateExceptionList = async ({ updated_by: user, } ); - return transformSavedObjectUpdateToExceptionList({ exceptionList, savedObject }); + return transformSavedObjectUpdateToExceptionList({ exceptionList, namespaceType, savedObject }); } }; diff --git a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts index 4e955d4281c4d..39c319a944e38 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts @@ -17,12 +17,12 @@ import { ItemIdOrUndefined, MetaOrUndefined, NameOrUndefined, + NamespaceType, TagsOrUndefined, _TagsOrUndefined, } from '../../../common/schemas'; import { getSavedObjectType, transformSavedObjectUpdateToExceptionListItem } from './utils'; -import { NamespaceType } from './types'; import { getExceptionListItem } from './get_exception_list_item'; interface UpdateExceptionListItemOptions { @@ -82,6 +82,10 @@ export const updateExceptionListItem = async ({ updated_by: user, } ); - return transformSavedObjectUpdateToExceptionListItem({ exceptionListItem, savedObject }); + return transformSavedObjectUpdateToExceptionListItem({ + exceptionListItem, + namespaceType, + savedObject, + }); } }; diff --git a/x-pack/plugins/lists/server/services/exception_lists/utils.ts b/x-pack/plugins/lists/server/services/exception_lists/utils.ts index 28dfb9c1cddaf..82a98f4bdd3e2 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/utils.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/utils.ts @@ -12,6 +12,7 @@ import { ExceptionListSoSchema, FoundExceptionListItemSchema, FoundExceptionListSchema, + NamespaceType, } from '../../../common/schemas'; import { SavedObjectType, @@ -19,8 +20,6 @@ import { exceptionListSavedObjectType, } from '../../saved_objects'; -import { NamespaceType } from './types'; - export const getSavedObjectType = ({ namespaceType, }: { @@ -35,8 +34,10 @@ export const getSavedObjectType = ({ export const transformSavedObjectToExceptionList = ({ savedObject, + namespaceType, }: { savedObject: SavedObject; + namespaceType: NamespaceType; }): ExceptionListSchema => { const dateNow = new Date().toISOString(); const { @@ -68,6 +69,7 @@ export const transformSavedObjectToExceptionList = ({ list_id, meta, name, + namespace_type: namespaceType, tags, tie_breaker_id, type, @@ -79,9 +81,11 @@ export const transformSavedObjectToExceptionList = ({ export const transformSavedObjectUpdateToExceptionList = ({ exceptionList, savedObject, + namespaceType, }: { exceptionList: ExceptionListSchema; savedObject: SavedObjectsUpdateResponse; + namespaceType: NamespaceType; }): ExceptionListSchema => { const dateNow = new Date().toISOString(); const { @@ -101,6 +105,7 @@ export const transformSavedObjectUpdateToExceptionList = ({ list_id: exceptionList.list_id, meta: meta ?? exceptionList.meta, name: name ?? exceptionList.name, + namespace_type: namespaceType, tags: tags ?? exceptionList.tags, tie_breaker_id: exceptionList.tie_breaker_id, type: type ?? exceptionList.type, @@ -111,8 +116,10 @@ export const transformSavedObjectUpdateToExceptionList = ({ export const transformSavedObjectToExceptionListItem = ({ savedObject, + namespaceType, }: { savedObject: SavedObject; + namespaceType: NamespaceType; }): ExceptionListItemSchema => { const dateNow = new Date().toISOString(); const { @@ -150,6 +157,7 @@ export const transformSavedObjectToExceptionListItem = ({ list_id, meta, name, + namespace_type: namespaceType, tags, tie_breaker_id, type, @@ -161,9 +169,11 @@ export const transformSavedObjectToExceptionListItem = ({ export const transformSavedObjectUpdateToExceptionListItem = ({ exceptionListItem, savedObject, + namespaceType, }: { exceptionListItem: ExceptionListItemSchema; savedObject: SavedObjectsUpdateResponse; + namespaceType: NamespaceType; }): ExceptionListItemSchema => { const dateNow = new Date().toISOString(); const { @@ -196,6 +206,7 @@ export const transformSavedObjectUpdateToExceptionListItem = ({ list_id: exceptionListItem.list_id, meta: meta ?? exceptionListItem.meta, name: name ?? exceptionListItem.name, + namespace_type: namespaceType, tags: tags ?? exceptionListItem.tags, tie_breaker_id: exceptionListItem.tie_breaker_id, type: type ?? exceptionListItem.type, @@ -206,12 +217,14 @@ export const transformSavedObjectUpdateToExceptionListItem = ({ export const transformSavedObjectsToFounExceptionListItem = ({ savedObjectsFindResponse, + namespaceType, }: { savedObjectsFindResponse: SavedObjectsFindResponse; + namespaceType: NamespaceType; }): FoundExceptionListItemSchema => { return { data: savedObjectsFindResponse.saved_objects.map((savedObject) => - transformSavedObjectToExceptionListItem({ savedObject }) + transformSavedObjectToExceptionListItem({ namespaceType, savedObject }) ), page: savedObjectsFindResponse.page, per_page: savedObjectsFindResponse.per_page, @@ -221,12 +234,14 @@ export const transformSavedObjectsToFounExceptionListItem = ({ export const transformSavedObjectsToFounExceptionList = ({ savedObjectsFindResponse, + namespaceType, }: { savedObjectsFindResponse: SavedObjectsFindResponse; + namespaceType: NamespaceType; }): FoundExceptionListSchema => { return { data: savedObjectsFindResponse.saved_objects.map((savedObject) => - transformSavedObjectToExceptionList({ savedObject }) + transformSavedObjectToExceptionList({ namespaceType, savedObject }) ), page: savedObjectsFindResponse.page, per_page: savedObjectsFindResponse.per_page, diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.ts b/x-pack/plugins/lists/server/services/items/create_list_item.ts index 83a118b795192..d46b9b4703fcb 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_item.ts @@ -58,7 +58,7 @@ export const createListItem = async ({ ...transformListItemToElasticQuery({ type, value }), }; - const response: CreateDocumentResponse = await callCluster('index', { + const response = await callCluster('index', { body, id, index: listItemIndex, diff --git a/x-pack/plugins/lists/server/services/items/find_list_item.ts b/x-pack/plugins/lists/server/services/items/find_list_item.ts new file mode 100644 index 0000000000000..d10e6466d03d0 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/find_list_item.ts @@ -0,0 +1,116 @@ +/* + * 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 { APICaller } from 'kibana/server'; + +import { + Filter, + FoundListItemSchema, + ListId, + Page, + PerPage, + SearchEsListItemSchema, + SortFieldOrUndefined, + SortOrderOrUndefined, +} from '../../../common/schemas'; +import { getList } from '../lists'; +import { + encodeCursor, + getQueryFilter, + getSearchAfterWithTieBreaker, + getSortWithTieBreaker, + scrollToStartPage, + transformElasticToListItem, +} from '../utils'; + +interface FindListItemOptions { + listId: ListId; + filter: Filter; + currentIndexPosition: number; + searchAfter: string[] | undefined; + perPage: PerPage; + page: Page; + sortField: SortFieldOrUndefined; + sortOrder: SortOrderOrUndefined; + callCluster: APICaller; + listIndex: string; + listItemIndex: string; +} + +export const findListItem = async ({ + callCluster, + currentIndexPosition, + filter, + listId, + page, + perPage, + searchAfter, + sortField: sortFieldWithPossibleValue, + listIndex, + listItemIndex, + sortOrder, +}: FindListItemOptions): Promise => { + const query = getQueryFilter({ filter }); + const list = await getList({ callCluster, id: listId, listIndex }); + if (list == null) { + return null; + } else { + const sortField = + sortFieldWithPossibleValue === 'value' ? list.type : sortFieldWithPossibleValue; + const scroll = await scrollToStartPage({ + callCluster, + currentIndexPosition, + filter, + hopSize: 100, + index: listItemIndex, + page, + perPage, + searchAfter, + sortField, + sortOrder, + }); + + const { count } = await callCluster('count', { + body: { + query, + }, + ignoreUnavailable: true, + index: listItemIndex, + }); + + if (scroll.validSearchAfterFound) { + const response = await callCluster('search', { + body: { + query, + search_after: scroll.searchAfter, + sort: getSortWithTieBreaker({ sortField, sortOrder }), + }, + ignoreUnavailable: true, + index: listItemIndex, + size: perPage, + }); + return { + cursor: encodeCursor({ + page, + perPage, + searchAfter: getSearchAfterWithTieBreaker({ response, sortField }), + }), + data: transformElasticToListItem({ response, type: list.type }), + page, + per_page: perPage, + total: count, + }; + } else { + return { + cursor: encodeCursor({ page, perPage, searchAfter: undefined }), + data: [], + page, + per_page: perPage, + total: count, + }; + } + } +}; diff --git a/x-pack/plugins/lists/server/services/items/get_list_item.ts b/x-pack/plugins/lists/server/services/items/get_list_item.ts index 83b30d336ccd4..296d1e4e82184 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SearchResponse } from 'elasticsearch'; import { APICaller } from 'kibana/server'; import { Id, ListItemSchema, SearchEsListItemSchema } from '../../../common/schemas'; @@ -21,7 +20,7 @@ export const getListItem = async ({ callCluster, listItemIndex, }: GetListItemOptions): Promise => { - const listItemES: SearchResponse = await callCluster('search', { + const listItemES = await callCluster('search', { body: { query: { term: { diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts index 29b9b01754027..cf0ccf3f10aa6 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SearchResponse } from 'elasticsearch'; import { APICaller } from 'kibana/server'; import { ListItemArraySchema, SearchEsListItemSchema, Type } from '../../../common/schemas'; @@ -25,7 +24,7 @@ export const getListItemByValues = async ({ type, value, }: GetListItemByValuesOptions): Promise => { - const response: SearchResponse = await callCluster('search', { + const response = await callCluster('search', { body: { query: { bool: { diff --git a/x-pack/plugins/lists/server/services/items/index.ts b/x-pack/plugins/lists/server/services/items/index.ts index ee1d83fabca31..bc04ba88b943e 100644 --- a/x-pack/plugins/lists/server/services/items/index.ts +++ b/x-pack/plugins/lists/server/services/items/index.ts @@ -8,12 +8,13 @@ export * from './buffer_lines'; export * from './create_list_item'; export * from './create_list_items_bulk'; export * from './delete_list_item_by_value'; +export * from './delete_list_item'; +export * from './find_list_item'; export * from './get_list_item_by_value'; export * from './get_list_item'; export * from './get_list_item_by_values'; +export * from './get_list_item_template'; +export * from './get_list_item_index'; export * from './update_list_item'; export * from './write_lines_to_bulk_list_items'; export * from './write_list_items_to_stream'; -export * from './get_list_item_template'; -export * from './delete_list_item'; -export * from './get_list_item_index'; diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.ts b/x-pack/plugins/lists/server/services/items/update_list_item.ts index 6a71b2a0caf41..6a428b4be854d 100644 --- a/x-pack/plugins/lists/server/services/items/update_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/update_list_item.ts @@ -48,7 +48,7 @@ export const updateListItem = async ({ ...transformListItemToElasticQuery({ type: listItem.type, value: value ?? listItem.value }), }; - const response: CreateDocumentResponse = await callCluster('update', { + const response = await callCluster('update', { body: { doc, }, diff --git a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts index 10d8581ccdbc0..f485f557433c6 100644 --- a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts +++ b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts @@ -114,7 +114,7 @@ export const getResponse = async ({ listItemIndex, size = SIZE, }: GetResponseOptions): Promise> => { - return callCluster('search', { + return callCluster('search', { body: { query: { term: { diff --git a/x-pack/plugins/lists/server/services/lists/create_list.ts b/x-pack/plugins/lists/server/services/lists/create_list.ts index ddbc99c88a877..0d2ee606a066d 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list.ts @@ -55,7 +55,7 @@ export const createList = async ({ updated_at: createdAt, updated_by: user, }; - const response: CreateDocumentResponse = await callCluster('index', { + const response = await callCluster('index', { body, id, index: listIndex, diff --git a/x-pack/plugins/lists/server/services/lists/find_list.ts b/x-pack/plugins/lists/server/services/lists/find_list.ts new file mode 100644 index 0000000000000..41dcdfcd0f8db --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/find_list.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { APICaller } from 'kibana/server'; + +import { + Filter, + FoundListSchema, + Page, + PerPage, + SearchEsListSchema, + SortFieldOrUndefined, + SortOrderOrUndefined, +} from '../../../common/schemas'; +import { + encodeCursor, + getQueryFilter, + getSearchAfterWithTieBreaker, + getSortWithTieBreaker, + scrollToStartPage, + transformElasticToList, +} from '../utils'; + +interface FindListOptions { + filter: Filter; + currentIndexPosition: number; + searchAfter: string[] | undefined; + perPage: PerPage; + page: Page; + sortField: SortFieldOrUndefined; + sortOrder: SortOrderOrUndefined; + callCluster: APICaller; + listIndex: string; +} + +export const findList = async ({ + callCluster, + currentIndexPosition, + filter, + page, + perPage, + searchAfter, + sortField, + listIndex, + sortOrder, +}: FindListOptions): Promise => { + const query = getQueryFilter({ filter }); + + const scroll = await scrollToStartPage({ + callCluster, + currentIndexPosition, + filter, + hopSize: 100, + index: listIndex, + page, + perPage, + searchAfter, + sortField, + sortOrder, + }); + + const { count } = await callCluster('count', { + body: { + query, + }, + ignoreUnavailable: true, + index: listIndex, + }); + + if (scroll.validSearchAfterFound) { + const response = await callCluster('search', { + body: { + query, + search_after: scroll.searchAfter, + sort: getSortWithTieBreaker({ sortField, sortOrder }), + }, + ignoreUnavailable: true, + index: listIndex, + size: perPage, + }); + return { + cursor: encodeCursor({ + page, + perPage, + searchAfter: getSearchAfterWithTieBreaker({ response, sortField }), + }), + data: transformElasticToList({ response }), + page, + per_page: perPage, + total: count, + }; + } else { + return { + cursor: encodeCursor({ page, perPage, searchAfter: undefined }), + data: [], + page, + per_page: perPage, + total: count, + }; + } +}; diff --git a/x-pack/plugins/lists/server/services/lists/get_list.ts b/x-pack/plugins/lists/server/services/lists/get_list.ts index c04bd504ad8c0..386232bfeee1f 100644 --- a/x-pack/plugins/lists/server/services/lists/get_list.ts +++ b/x-pack/plugins/lists/server/services/lists/get_list.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SearchResponse } from 'elasticsearch'; import { APICaller } from 'kibana/server'; import { Id, ListSchema, SearchEsListSchema } from '../../../common/schemas'; +import { transformElasticToList } from '../utils/transform_elastic_to_list'; interface GetListOptions { id: Id; @@ -20,7 +20,7 @@ export const getList = async ({ callCluster, listIndex, }: GetListOptions): Promise => { - const result: SearchResponse = await callCluster('search', { + const response = await callCluster('search', { body: { query: { term: { @@ -31,12 +31,6 @@ export const getList = async ({ ignoreUnavailable: true, index: listIndex, }); - if (result.hits.hits.length) { - return { - id: result.hits.hits[0]._id, - ...result.hits.hits[0]._source, - }; - } else { - return null; - } + const list = transformElasticToList({ response }); + return list[0] ?? null; }; diff --git a/x-pack/plugins/lists/server/services/lists/index.ts b/x-pack/plugins/lists/server/services/lists/index.ts index f704ef0b05b82..bafeb929a8d53 100644 --- a/x-pack/plugins/lists/server/services/lists/index.ts +++ b/x-pack/plugins/lists/server/services/lists/index.ts @@ -6,6 +6,7 @@ export * from './create_list'; export * from './delete_list'; +export * from './find_list'; export * from './get_list'; export * from './get_list_template'; export * from './update_list'; diff --git a/x-pack/plugins/lists/server/services/lists/list_client.ts b/x-pack/plugins/lists/server/services/lists/list_client.ts index cba48115c746c..5a7d20c7d64d5 100644 --- a/x-pack/plugins/lists/server/services/lists/list_client.ts +++ b/x-pack/plugins/lists/server/services/lists/list_client.ts @@ -6,11 +6,18 @@ import { APICaller } from 'kibana/server'; -import { ListItemArraySchema, ListItemSchema, ListSchema } from '../../../common/schemas'; +import { + FoundListItemSchema, + FoundListSchema, + ListItemArraySchema, + ListItemSchema, + ListSchema, +} from '../../../common/schemas'; import { ConfigType } from '../../config'; import { createList, deleteList, + findList, getList, getListIndex, getListTemplate, @@ -21,6 +28,7 @@ import { deleteListItem, deleteListItemByValue, exportListItemsToStream, + findListItem, getListItem, getListItemByValue, getListItemByValues, @@ -52,6 +60,8 @@ import { DeleteListItemOptions, DeleteListOptions, ExportListItemsToStreamOptions, + FindListItemOptions, + FindListOptions, GetListItemByValueOptions, GetListItemOptions, GetListItemsByValueOptions, @@ -410,4 +420,56 @@ export class ListClient { value, }); }; + + public findList = async ({ + filter, + currentIndexPosition, + perPage, + page, + sortField, + sortOrder, + searchAfter, + }: FindListOptions): Promise => { + const { callCluster } = this; + const listIndex = this.getListIndex(); + return findList({ + callCluster, + currentIndexPosition, + filter, + listIndex, + page, + perPage, + searchAfter, + sortField, + sortOrder, + }); + }; + + public findListItem = async ({ + listId, + filter, + currentIndexPosition, + perPage, + page, + sortField, + sortOrder, + searchAfter, + }: FindListItemOptions): Promise => { + const { callCluster } = this; + const listIndex = this.getListIndex(); + const listItemIndex = this.getListItemIndex(); + return findListItem({ + callCluster, + currentIndexPosition, + filter, + listId, + listIndex, + listItemIndex, + page, + perPage, + searchAfter, + sortField, + sortOrder, + }); + }; } diff --git a/x-pack/plugins/lists/server/services/lists/list_client_types.ts b/x-pack/plugins/lists/server/services/lists/list_client_types.ts index d66575e7a30db..4171b6ee9f165 100644 --- a/x-pack/plugins/lists/server/services/lists/list_client_types.ts +++ b/x-pack/plugins/lists/server/services/lists/list_client_types.ts @@ -11,11 +11,17 @@ import { APICaller } from 'kibana/server'; import { Description, DescriptionOrUndefined, + Filter, Id, IdOrUndefined, + ListId, MetaOrUndefined, Name, NameOrUndefined, + Page, + PerPage, + SortFieldOrUndefined, + SortOrderOrUndefined, Type, } from '../../../common/schemas'; import { ConfigType } from '../../config'; @@ -110,3 +116,24 @@ export interface GetListItemsByValueOptions { listId: string; value: string[]; } + +export interface FindListOptions { + currentIndexPosition: number; + filter: Filter; + perPage: PerPage; + page: Page; + searchAfter: string[] | undefined; + sortField: SortFieldOrUndefined; + sortOrder: SortOrderOrUndefined; +} + +export interface FindListItemOptions { + currentIndexPosition: number; + filter: Filter; + listId: ListId; + perPage: PerPage; + page: Page; + searchAfter: string[] | undefined; + sortField: SortFieldOrUndefined; + sortOrder: SortOrderOrUndefined; +} diff --git a/x-pack/plugins/lists/server/services/exception_lists/types.ts b/x-pack/plugins/lists/server/services/lists/types.ts similarity index 72% rename from x-pack/plugins/lists/server/services/exception_lists/types.ts rename to x-pack/plugins/lists/server/services/lists/types.ts index dbb188bc2754a..2e0e4b7d038e7 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/types.ts +++ b/x-pack/plugins/lists/server/services/lists/types.ts @@ -3,4 +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. */ -export type NamespaceType = 'agnostic' | 'single'; + +interface Scroll { + searchAfter: string[] | undefined; + validSearchAfterFound: boolean; +} diff --git a/x-pack/plugins/lists/server/services/lists/update_list.ts b/x-pack/plugins/lists/server/services/lists/update_list.ts index 9859adf062485..28be50e9d6ac8 100644 --- a/x-pack/plugins/lists/server/services/lists/update_list.ts +++ b/x-pack/plugins/lists/server/services/lists/update_list.ts @@ -51,7 +51,7 @@ export const updateList = async ({ updated_at: updatedAt, updated_by: user, }; - const response: CreateDocumentResponse = await callCluster('update', { + const response = await callCluster('update', { body: { doc }, id, index: listIndex, diff --git a/x-pack/plugins/lists/server/services/utils/calculate_scroll_math.ts b/x-pack/plugins/lists/server/services/utils/calculate_scroll_math.ts new file mode 100644 index 0000000000000..6ec240d844f84 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/calculate_scroll_math.ts @@ -0,0 +1,41 @@ +/* + * 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 { Page, PerPage } from '../../../common/schemas'; + +interface CalculateScrollMathOptions { + perPage: PerPage; + page: Page; + hopSize: number; + currentIndexPosition: number; +} + +interface CalculateScrollMathReturn { + hops: number; + leftOverAfterHops: number; +} + +export const calculateScrollMath = ({ + currentIndexPosition, + page, + perPage, + hopSize, +}: CalculateScrollMathOptions): CalculateScrollMathReturn => { + const startPageIndex = (page - 1) * perPage - currentIndexPosition; + if (startPageIndex < 0) { + // This should never be hit but just in case I do a check. We do validate higher above this + // before the current index position gets to this point but to be safe we add this line. + throw new Error( + `page: ${page}, perPage ${perPage} and currentIndex ${currentIndexPosition} are less than zero` + ); + } + const hops = Math.floor(startPageIndex / hopSize); + const leftOverAfterHops = startPageIndex - hops * hopSize; + return { + hops, + leftOverAfterHops, + }; +}; diff --git a/x-pack/plugins/lists/server/services/utils/encode_decode_cursor.ts b/x-pack/plugins/lists/server/services/utils/encode_decode_cursor.ts new file mode 100644 index 0000000000000..205d61f204ba6 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/encode_decode_cursor.ts @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; +import { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { CursorOrUndefined, SortFieldOrUndefined } from '../../../common/schemas'; +import { exactCheck } from '../../../common/siem_common_deps'; + +/** + * Used only internally for this ad-hoc opaque cursor structure to keep track of the + * current page_index that the search_after is currently on. The format of an array + * is to be consistent with other compact forms of opaque nature such as a saved object versioning. + * + * The format is [index of item, search_after_array] + */ + +// TODO: Use PositiveInteger from siem once that type is outside of server and in common +export const contextCursor = t.tuple([t.number, t.union([t.array(t.string), t.undefined])]); + +export type ContextCursor = t.TypeOf; + +export interface EncodeCursorOptions { + searchAfter: string[] | undefined; + page: number; + perPage: number; +} + +export const encodeCursor = ({ searchAfter, page, perPage }: EncodeCursorOptions): string => { + const index = searchAfter != null ? page * perPage : 0; + const encodedCursor = searchAfter != null ? [index, searchAfter] : [index]; + const scrollStringed = JSON.stringify(encodedCursor); + return Buffer.from(scrollStringed).toString('base64'); +}; + +export interface DecodeCursorOptions { + cursor: CursorOrUndefined; + page: number; + perPage: number; + sortField: SortFieldOrUndefined; +} + +export interface DecodeCursor { + cursor: ContextCursor; + isValid: boolean; + errorMessage: string; +} + +export const decodeCursor = ({ + cursor, + page, + perPage, + sortField, +}: DecodeCursorOptions): DecodeCursor => { + if (cursor == null) { + return { + cursor: [0, undefined], + errorMessage: '', + isValid: true, + }; + } else { + const fromBuffer = Buffer.from(cursor, 'base64').toString(); + const parsed = parseOrUndefined(fromBuffer); + if (parsed == null) { + return { + cursor: [0, undefined], + errorMessage: 'Error parsing JSON from base64 encoded cursor', + isValid: false, + }; + } else { + const decodedCursor = contextCursor.decode(parsed); + const checked = exactCheck(parsed, decodedCursor); + + const onLeft = (): ContextCursor | undefined => undefined; + const onRight = (schema: ContextCursor): ContextCursor | undefined => schema; + const cursorOrUndefined = pipe(checked, fold(onLeft, onRight)); + + const startPageIndex = (page - 1) * perPage; + if (cursorOrUndefined == null) { + return { + cursor: [0, undefined], + errorMessage: 'Error decoding cursor structure', + isValid: false, + }; + } else { + const [index, searchAfter] = cursorOrUndefined; + if (index < 0) { + return { + cursor: [0, undefined], + errorMessage: 'index of cursor cannot be less 0', + isValid: false, + }; + } else if (index > startPageIndex) { + return { + cursor: [0, undefined], + errorMessage: `index: ${index} of cursor cannot be greater than the start page index: ${startPageIndex}`, + isValid: false, + }; + } else if (searchAfter != null && searchAfter.length > 1 && sortField == null) { + return { + cursor: [0, undefined], + errorMessage: '', + isValid: false, + }; + } else { + return { + cursor: [index, searchAfter != null ? searchAfter : undefined], + errorMessage: '', + isValid: true, + }; + } + } + } + } +}; + +export const parseOrUndefined = (input: string): ContextCursor | undefined => { + try { + return JSON.parse(input); + } catch (err) { + return undefined; + } +}; diff --git a/x-pack/plugins/lists/server/services/utils/get_query_filter.test.ts b/x-pack/plugins/lists/server/services/utils/get_query_filter.test.ts new file mode 100644 index 0000000000000..50c266eb5d573 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/get_query_filter.test.ts @@ -0,0 +1,34 @@ +/* + * 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 { getQueryFilter } from './get_query_filter'; + +describe('get_query_filter', () => { + test('it should work with a basic kuery', () => { + const esQuery = getQueryFilter({ filter: 'type: ip' }); + expect(esQuery).toEqual({ + bool: { + filter: [ + { + bool: { + minimum_should_match: 1, + should: [ + { + match: { + type: 'ip', + }, + }, + ], + }, + }, + ], + must: [], + must_not: [], + should: [], + }, + }); + }); +}); diff --git a/x-pack/plugins/lists/server/services/utils/get_query_filter.ts b/x-pack/plugins/lists/server/services/utils/get_query_filter.ts new file mode 100644 index 0000000000000..cf0dd5b6250e5 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/get_query_filter.ts @@ -0,0 +1,32 @@ +/* + * 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 { DslQuery, EsQueryConfig } from 'src/plugins/data/common'; + +import { Filter, Query, esQuery } from '../../../../../../src/plugins/data/server'; + +export interface GetQueryFilterOptions { + filter: string; +} + +export interface GetQueryFilterReturn { + bool: { must: DslQuery[]; filter: Filter[]; should: never[]; must_not: Filter[] }; +} + +export const getQueryFilter = ({ filter }: GetQueryFilterOptions): GetQueryFilterReturn => { + const kqlQuery: Query = { + language: 'kuery', + query: filter, + }; + const config: EsQueryConfig = { + allowLeadingWildcards: true, + dateFormatTZ: 'Zulu', + ignoreFilterIfFieldNotInIndex: false, + queryStringOptions: { analyze_wildcard: true }, + }; + + return esQuery.buildEsQuery(undefined, kqlQuery, [], config); +}; diff --git a/x-pack/plugins/lists/server/services/utils/get_search_after_scroll.ts b/x-pack/plugins/lists/server/services/utils/get_search_after_scroll.ts new file mode 100644 index 0000000000000..9721baefbe5ee --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/get_search_after_scroll.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { APICaller } from 'kibana/server'; + +import { Filter, SortFieldOrUndefined, SortOrderOrUndefined } from '../../../common/schemas'; + +import { getQueryFilter } from './get_query_filter'; +import { getSortWithTieBreaker } from './get_sort_with_tie_breaker'; +import { getSourceWithTieBreaker } from './get_source_with_tie_breaker'; +import { TieBreaker, getSearchAfterWithTieBreaker } from './get_search_after_with_tie_breaker'; + +interface GetSearchAfterOptions { + callCluster: APICaller; + filter: Filter; + hops: number; + hopSize: number; + searchAfter: string[] | undefined; + index: string; + sortField: SortFieldOrUndefined; + sortOrder: SortOrderOrUndefined; +} + +export const getSearchAfterScroll = async ({ + callCluster, + filter, + hopSize, + hops, + searchAfter, + sortField, + sortOrder, + index, +}: GetSearchAfterOptions): Promise => { + const query = getQueryFilter({ filter }); + let newSearchAfter = searchAfter; + for (let i = 0; i < hops; ++i) { + const response = await callCluster>('search', { + body: { + _source: getSourceWithTieBreaker({ sortField }), + query, + search_after: newSearchAfter, + sort: getSortWithTieBreaker({ sortField, sortOrder }), + }, + ignoreUnavailable: true, + index, + size: hopSize, + }); + if (response.hits.hits.length > 0) { + newSearchAfter = getSearchAfterWithTieBreaker({ response, sortField }); + } else { + return { + searchAfter: undefined, + validSearchAfterFound: false, + }; + } + } + return { + searchAfter: newSearchAfter, + validSearchAfterFound: true, + }; +}; diff --git a/x-pack/plugins/lists/server/services/utils/get_search_after_with_tie_breaker.ts b/x-pack/plugins/lists/server/services/utils/get_search_after_with_tie_breaker.ts new file mode 100644 index 0000000000000..b5d44fbc9fd84 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/get_search_after_with_tie_breaker.ts @@ -0,0 +1,41 @@ +/* + * 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 { SearchResponse } from 'elasticsearch'; + +import { SortFieldOrUndefined } from '../../../common/schemas'; + +export type TieBreaker = T & { + tie_breaker_id: string; +}; + +interface GetSearchAfterWithTieBreakerOptions { + response: SearchResponse>; + sortField: SortFieldOrUndefined; +} + +export const getSearchAfterWithTieBreaker = ({ + response, + sortField, +}: GetSearchAfterWithTieBreakerOptions): string[] | undefined => { + if (response.hits.hits.length === 0) { + return undefined; + } else { + const lastEsElement = response.hits.hits[response.hits.hits.length - 1]; + if (sortField == null) { + return [lastEsElement._source.tie_breaker_id]; + } else { + const [[, sortValue]] = Object.entries(lastEsElement._source).filter( + ([key]) => key === sortField + ); + if (typeof sortValue === 'string') { + return [sortValue, lastEsElement._source.tie_breaker_id]; + } else { + return [lastEsElement._source.tie_breaker_id]; + } + } + } +}; diff --git a/x-pack/plugins/lists/server/services/utils/get_sort_with_tie_breaker.ts b/x-pack/plugins/lists/server/services/utils/get_sort_with_tie_breaker.ts new file mode 100644 index 0000000000000..fee65cce580a0 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/get_sort_with_tie_breaker.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SortFieldOrUndefined, SortOrderOrUndefined } from '../../../common/schemas'; + +export interface SortWithTieBreakerReturn { + tie_breaker_id: 'asc'; + [key: string]: string; +} + +export const getSortWithTieBreaker = ({ + sortField, + sortOrder, +}: { + sortField: SortFieldOrUndefined; + sortOrder: SortOrderOrUndefined; +}): SortWithTieBreakerReturn[] | undefined => { + const ascOrDesc = sortOrder ?? 'asc'; + if (sortField != null) { + return [{ [sortField]: ascOrDesc, tie_breaker_id: 'asc' }]; + } else { + return [{ tie_breaker_id: 'asc' }]; + } +}; diff --git a/x-pack/plugins/lists/server/services/utils/get_source_with_tie_breaker.ts b/x-pack/plugins/lists/server/services/utils/get_source_with_tie_breaker.ts new file mode 100644 index 0000000000000..76cdd22f710e1 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/get_source_with_tie_breaker.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SortFieldOrUndefined } from '../../../common/schemas'; + +export const getSourceWithTieBreaker = ({ + sortField, +}: { + sortField: SortFieldOrUndefined; +}): string[] => { + return sortField != null ? ['tie_breaker_id', sortField] : ['tie_breaker_id']; +}; diff --git a/x-pack/plugins/lists/server/services/utils/index.ts b/x-pack/plugins/lists/server/services/utils/index.ts index e6365e689f761..28bb3cea29e61 100644 --- a/x-pack/plugins/lists/server/services/utils/index.ts +++ b/x-pack/plugins/lists/server/services/utils/index.ts @@ -5,6 +5,14 @@ */ export * from './derive_type_from_es_type'; +export * from './encode_decode_cursor'; export * from './get_query_filter_from_type_value'; +export * from './get_query_filter'; +export * from './get_search_after_scroll'; +export * from './get_search_after_with_tie_breaker'; +export * from './get_sort_with_tie_breaker'; +export * from './get_source_with_tie_breaker'; +export * from './scroll_to_start_page'; export * from './transform_elastic_to_list_item'; +export * from './transform_elastic_to_list'; export * from './transform_list_item_to_elastic_query'; diff --git a/x-pack/plugins/lists/server/services/utils/scroll_to_start_page.ts b/x-pack/plugins/lists/server/services/utils/scroll_to_start_page.ts new file mode 100644 index 0000000000000..16e07044dc0d4 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/scroll_to_start_page.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 { APICaller } from 'kibana/server'; + +import { Filter, SortFieldOrUndefined, SortOrderOrUndefined } from '../../../common/schemas'; + +import { calculateScrollMath } from './calculate_scroll_math'; +import { getSearchAfterScroll } from './get_search_after_scroll'; + +interface ScrollToStartPageOptions { + callCluster: APICaller; + filter: Filter; + sortField: SortFieldOrUndefined; + sortOrder: SortOrderOrUndefined; + page: number; + perPage: number; + hopSize: number; + index: string; + currentIndexPosition: number; + searchAfter: string[] | undefined; +} + +export const scrollToStartPage = async ({ + callCluster, + filter, + hopSize, + currentIndexPosition, + searchAfter, + page, + perPage, + sortOrder, + sortField, + index, +}: ScrollToStartPageOptions): Promise => { + const { hops, leftOverAfterHops } = calculateScrollMath({ + currentIndexPosition, + hopSize, + page, + perPage, + }); + + if (hops === 0 && leftOverAfterHops === 0 && currentIndexPosition === 0) { + // We want to use a valid searchAfter of undefined to start at the start of our list + return { + searchAfter: undefined, + validSearchAfterFound: true, + }; + } else if (hops === 0 && leftOverAfterHops === 0 && currentIndexPosition > 0) { + return { + searchAfter, + validSearchAfterFound: true, + }; + } else if (hops > 0) { + const scroll = await getSearchAfterScroll({ + callCluster, + filter, + hopSize, + hops, + index, + searchAfter, + sortField, + sortOrder, + }); + if (scroll.validSearchAfterFound && leftOverAfterHops > 0) { + return getSearchAfterScroll({ + callCluster, + filter, + hopSize: leftOverAfterHops, + hops: 1, + index, + searchAfter: scroll.searchAfter, + sortField, + sortOrder, + }); + } else { + return scroll; + } + } else { + return getSearchAfterScroll({ + callCluster, + filter, + hopSize: leftOverAfterHops, + hops: 1, + index, + searchAfter, + sortField, + sortOrder, + }); + } +}; diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list.ts new file mode 100644 index 0000000000000..bb1ae1d4b9ff3 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list.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. + */ + +import { SearchResponse } from 'elasticsearch'; + +import { ListArraySchema, SearchEsListSchema } from '../../../common/schemas'; + +export interface TransformElasticToListOptions { + response: SearchResponse; +} + +export const transformElasticToList = ({ + response, +}: TransformElasticToListOptions): ListArraySchema => { + return response.hits.hits.map((hit) => { + return { + id: hit._id, + ...hit._source, + }; + }); +}; diff --git a/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts index 4bdafcabaad06..b412375874f68 100644 --- a/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts +++ b/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts @@ -132,6 +132,7 @@ export type SourceDescriptor = export type LayerDescriptor = { __dataRequests?: DataRequestDescriptor[]; __isInErrorState?: boolean; + __isPreviewLayer?: boolean; __errorMessage?: string; __trackedLayerDescriptor?: LayerDescriptor; alpha?: number; diff --git a/x-pack/plugins/maps/public/actions/layer_actions.ts b/x-pack/plugins/maps/public/actions/layer_actions.ts index cac79093ce437..51e251a5d8e20 100644 --- a/x-pack/plugins/maps/public/actions/layer_actions.ts +++ b/x-pack/plugins/maps/public/actions/layer_actions.ts @@ -9,10 +9,10 @@ import { Query } from 'src/plugins/data/public'; import { MapStoreState } from '../reducers/store'; import { getLayerById, + getLayerList, getLayerListRaw, getSelectedLayerId, getMapReady, - getTransientLayerId, } from '../selectors/map_selectors'; import { FLYOUT_STATE } from '../reducers/ui'; import { cancelRequest } from '../reducers/non_serializable_instances'; @@ -27,7 +27,6 @@ import { SET_JOINS, SET_LAYER_VISIBILITY, SET_SELECTED_LAYER, - SET_TRANSIENT_LAYER, SET_WAITING_FOR_READY_HIDDEN_LAYERS, TRACK_CURRENT_LAYER_STATE, UPDATE_LAYER_ORDER, @@ -139,6 +138,41 @@ export function addLayerWithoutDataSync(layerDescriptor: LayerDescriptor) { }; } +export function addPreviewLayers(layerDescriptors: LayerDescriptor[]) { + return (dispatch: Dispatch) => { + dispatch(removePreviewLayers()); + + layerDescriptors.forEach((layerDescriptor) => { + dispatch(addLayer({ ...layerDescriptor, __isPreviewLayer: true })); + }); + }; +} + +export function removePreviewLayers() { + return (dispatch: Dispatch, getState: () => MapStoreState) => { + getLayerList(getState()).forEach((layer) => { + if (layer.isPreviewLayer()) { + dispatch(removeLayer(layer.getId())); + } + }); + }; +} + +export function promotePreviewLayers() { + return (dispatch: Dispatch, getState: () => MapStoreState) => { + getLayerList(getState()).forEach((layer) => { + if (layer.isPreviewLayer()) { + dispatch({ + type: UPDATE_LAYER_PROP, + id: layer.getId(), + propName: '__isPreviewLayer', + newValue: false, + }); + } + }); + }; +} + export function setLayerVisibility(layerId: string, makeVisible: boolean) { return async (dispatch: Dispatch, getState: () => MapStoreState) => { // if the current-state is invisible, we also want to sync data @@ -193,31 +227,17 @@ export function setSelectedLayer(layerId: string | null) { }; } -export function removeTransientLayer() { +export function setFirstPreviewLayerToSelectedLayer() { return async (dispatch: Dispatch, getState: () => MapStoreState) => { - const transientLayerId = getTransientLayerId(getState()); - if (transientLayerId) { - await dispatch(removeLayerFromLayerList(transientLayerId)); - await dispatch(setTransientLayer(null)); + const firstPreviewLayer = getLayerList(getState()).find((layer) => { + return layer.isPreviewLayer(); + }); + if (firstPreviewLayer) { + dispatch(setSelectedLayer(firstPreviewLayer.getId())); } }; } -export function setTransientLayer(layerId: string | null) { - return { - type: SET_TRANSIENT_LAYER, - transientLayerId: layerId, - }; -} - -export function clearTransientLayerStateAndCloseFlyout() { - return async (dispatch: Dispatch) => { - await dispatch(updateFlyout(FLYOUT_STATE.NONE)); - await dispatch(setSelectedLayer(null)); - await dispatch(removeTransientLayer()); - }; -} - export function updateLayerOrder(newLayerOrder: number[]) { return { type: UPDATE_LAYER_ORDER, diff --git a/x-pack/plugins/maps/public/actions/map_action_constants.ts b/x-pack/plugins/maps/public/actions/map_action_constants.ts index 0a32dba119429..25a86e4c50d07 100644 --- a/x-pack/plugins/maps/public/actions/map_action_constants.ts +++ b/x-pack/plugins/maps/public/actions/map_action_constants.ts @@ -5,7 +5,6 @@ */ export const SET_SELECTED_LAYER = 'SET_SELECTED_LAYER'; -export const SET_TRANSIENT_LAYER = 'SET_TRANSIENT_LAYER'; export const UPDATE_LAYER_ORDER = 'UPDATE_LAYER_ORDER'; export const ADD_LAYER = 'ADD_LAYER'; export const SET_LAYER_ERROR_STATUS = 'SET_LAYER_ERROR_STATUS'; diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index 263e9888cd059..5d54166e08fb7 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -80,6 +80,7 @@ export interface ILayer { getInFlightRequestTokens(): symbol[]; getPrevRequestToken(dataId: string): symbol | undefined; destroy: () => void; + isPreviewLayer: () => boolean; } export type Footnote = { icon: ReactElement; @@ -179,6 +180,10 @@ export class AbstractLayer implements ILayer { return this.getSource().isJoinable(); } + isPreviewLayer(): boolean { + return !!this._descriptor.__isPreviewLayer; + } + supportsElasticsearchFilters(): boolean { return this.getSource().isESSource(); } diff --git a/x-pack/plugins/maps/public/classes/layers/layer_wizard_registry.ts b/x-pack/plugins/maps/public/classes/layers/layer_wizard_registry.ts index 7698fb7c0947e..2bdeb6446cf28 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer_wizard_registry.ts +++ b/x-pack/plugins/maps/public/classes/layers/layer_wizard_registry.ts @@ -9,7 +9,7 @@ import { ReactElement } from 'react'; import { LayerDescriptor } from '../../../common/descriptor_types'; export type RenderWizardArguments = { - previewLayer: (layerDescriptor: LayerDescriptor | null, isIndexingSource?: boolean) => void; + previewLayers: (layerDescriptors: LayerDescriptor[], isIndexingSource?: boolean) => void; mapColors: string[]; // upload arguments isIndexingTriggered: boolean; diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/observability_layer_template.tsx b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/observability_layer_template.tsx index bfd78d5490059..3f3c556dcae1e 100644 --- a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/observability_layer_template.tsx +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/observability_layer_template.tsx @@ -53,13 +53,12 @@ export class ObservabilityLayerTemplate extends Component { function previewGeojsonFile(geojsonFile: unknown, name: string) { if (!geojsonFile) { - previewLayer(null); + previewLayers([]); return; } const sourceDescriptor = GeojsonFileSource.createDescriptor(geojsonFile, name); const layerDescriptor = VectorLayer.createDescriptor({ sourceDescriptor }, mapColors); // TODO figure out a better way to handle passing this information back to layer_addpanel - previewLayer(layerDescriptor, true); + previewLayers([layerDescriptor], true); } function viewIndexedData(indexResponses: { @@ -72,7 +72,7 @@ export const uploadLayerWizardConfig: LayerWizard = { ) ); if (!indexPatternId || !geoField) { - previewLayer(null); + previewLayers([]); } else { const esSearchSourceConfig = { indexPatternId, @@ -85,7 +85,7 @@ export const uploadLayerWizardConfig: LayerWizard = { ? SCALING_TYPES.CLUSTERS : SCALING_TYPES.LIMIT, }; - previewLayer(createDefaultLayerDescriptor(esSearchSourceConfig, mapColors)); + previewLayers([createDefaultLayerDescriptor(esSearchSourceConfig, mapColors)]); importSuccessHandler(indexResponses); } } diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx index 4f1edca75b308..7eec84ef5bb2e 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx @@ -22,11 +22,11 @@ export const emsBoundariesLayerWizardConfig: LayerWizard = { defaultMessage: 'Administrative boundaries from Elastic Maps Service', }), icon: 'emsApp', - renderWizard: ({ previewLayer, mapColors }: RenderWizardArguments) => { + renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: Partial) => { const sourceDescriptor = EMSFileSource.createDescriptor(sourceConfig); const layerDescriptor = VectorLayer.createDescriptor({ sourceDescriptor }, mapColors); - previewLayer(layerDescriptor); + previewLayers([layerDescriptor]); }; return ; }, diff --git a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx index 7a25609c6a5d1..60e67b1ae7053 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx @@ -22,12 +22,12 @@ export const emsBaseMapLayerWizardConfig: LayerWizard = { defaultMessage: 'Tile map service from Elastic Maps Service', }), icon: 'emsApp', - renderWizard: ({ previewLayer }: RenderWizardArguments) => { + renderWizard: ({ previewLayers }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: unknown) => { const layerDescriptor = VectorTileLayer.createDescriptor({ sourceDescriptor: EMSTMSSource.createDescriptor(sourceConfig), }); - previewLayer(layerDescriptor); + previewLayers([layerDescriptor]); }; return ; diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx index 4e75ae8823385..b9d5faa8e18f1 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx @@ -27,7 +27,6 @@ import { VECTOR_STYLES, STYLE_TYPE, } from '../../../../common/constants'; -// @ts-ignore import { COLOR_GRADIENTS } from '../../styles/color_utils'; export const clustersLayerWizardConfig: LayerWizard = { @@ -35,10 +34,10 @@ export const clustersLayerWizardConfig: LayerWizard = { defaultMessage: 'Geospatial data grouped in grids with metrics for each gridded cell', }), icon: 'logoElasticsearch', - renderWizard: ({ previewLayer }: RenderWizardArguments) => { + renderWizard: ({ previewLayers }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: Partial) => { if (!sourceConfig) { - previewLayer(null); + previewLayers([]); return; } @@ -94,7 +93,7 @@ export const clustersLayerWizardConfig: LayerWizard = { }, }), }); - previewLayer(layerDescriptor); + previewLayers([layerDescriptor]); }; return ( diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx index d0e45cb05ca06..79252c7febf8c 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx @@ -21,17 +21,17 @@ export const heatmapLayerWizardConfig: LayerWizard = { defaultMessage: 'Geospatial data grouped in grids to show density', }), icon: 'logoElasticsearch', - renderWizard: ({ previewLayer }: RenderWizardArguments) => { + renderWizard: ({ previewLayers }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: Partial) => { if (!sourceConfig) { - previewLayer(null); + previewLayers([]); return; } const layerDescriptor = HeatmapLayer.createDescriptor({ sourceDescriptor: ESGeoGridSource.createDescriptor(sourceConfig), }); - previewLayer(layerDescriptor); + previewLayers([layerDescriptor]); }; return ( diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx index bda1a6650c48a..5169af9bdddf2 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx @@ -17,7 +17,6 @@ import { VECTOR_STYLES, STYLE_TYPE, } from '../../../../common/constants'; -// @ts-ignore import { COLOR_GRADIENTS } from '../../styles/color_utils'; // @ts-ignore import { CreateSourceEditor } from './create_source_editor'; @@ -29,10 +28,10 @@ export const point2PointLayerWizardConfig: LayerWizard = { defaultMessage: 'Aggregated data paths between the source and destination', }), icon: 'logoElasticsearch', - renderWizard: ({ previewLayer }: RenderWizardArguments) => { + renderWizard: ({ previewLayers }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: unknown) => { if (!sourceConfig) { - previewLayer(null); + previewLayers([]); return; } @@ -65,7 +64,7 @@ export const point2PointLayerWizardConfig: LayerWizard = { }, }), }); - previewLayer(layerDescriptor); + previewLayers([layerDescriptor]); }; return ; diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx index 8898735427ccb..888de2e7297cb 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx @@ -28,14 +28,14 @@ export const esDocumentsLayerWizardConfig: LayerWizard = { defaultMessage: 'Vector data from a Kibana index pattern', }), icon: 'logoElasticsearch', - renderWizard: ({ previewLayer, mapColors }: RenderWizardArguments) => { + renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: unknown) => { if (!sourceConfig) { - previewLayer(null); + previewLayers([]); return; } - previewLayer(createDefaultLayerDescriptor(sourceConfig, mapColors)); + previewLayers([createDefaultLayerDescriptor(sourceConfig, mapColors)]); }; return ; }, diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx index 309cb3abd83b2..b778dc0076459 100644 --- a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx @@ -24,11 +24,11 @@ export const kibanaRegionMapLayerWizardConfig: LayerWizard = { defaultMessage: 'Vector data from hosted GeoJSON configured in kibana.yml', }), icon: 'logoKibana', - renderWizard: ({ previewLayer, mapColors }: RenderWizardArguments) => { + renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: unknown) => { const sourceDescriptor = KibanaRegionmapSource.createDescriptor(sourceConfig); const layerDescriptor = VectorLayer.createDescriptor({ sourceDescriptor }, mapColors); - previewLayer(layerDescriptor); + previewLayers([layerDescriptor]); }; return ; diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx index 46513985ed1ab..227c0182b98de 100644 --- a/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx @@ -24,12 +24,12 @@ export const kibanaBasemapLayerWizardConfig: LayerWizard = { defaultMessage: 'Tile map service configured in kibana.yml', }), icon: 'logoKibana', - renderWizard: ({ previewLayer }: RenderWizardArguments) => { + renderWizard: ({ previewLayers }: RenderWizardArguments) => { const onSourceConfigChange = () => { const layerDescriptor = TileLayer.createDescriptor({ sourceDescriptor: KibanaTilemapSource.createDescriptor(), }); - previewLayer(layerDescriptor); + previewLayers([layerDescriptor]); }; return ; }, diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx index 86f8108d5e23b..c29302a2058b2 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx @@ -19,11 +19,11 @@ export const mvtVectorSourceWizardConfig: LayerWizard = { defaultMessage: 'Vector source wizard', }), icon: 'grid', - renderWizard: ({ previewLayer, mapColors }: RenderWizardArguments) => { + renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: MVTSingleLayerVectorSourceConfig) => { const sourceDescriptor = MVTSingleLayerVectorSource.createDescriptor(sourceConfig); const layerDescriptor = TiledVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); - previewLayer(layerDescriptor); + previewLayers([layerDescriptor]); }; return ; diff --git a/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx index 9261b8866d115..62eeef234f414 100644 --- a/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx @@ -18,17 +18,17 @@ export const wmsLayerWizardConfig: LayerWizard = { defaultMessage: 'Maps from OGC Standard WMS', }), icon: 'grid', - renderWizard: ({ previewLayer }: RenderWizardArguments) => { + renderWizard: ({ previewLayers }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: unknown) => { if (!sourceConfig) { - previewLayer(null); + previewLayers([]); return; } const layerDescriptor = TileLayer.createDescriptor({ sourceDescriptor: WMSSource.createDescriptor(sourceConfig), }); - previewLayer(layerDescriptor); + previewLayers([layerDescriptor]); }; return ; }, diff --git a/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx index 574aaa262569f..b99b17c1d22d4 100644 --- a/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx @@ -16,12 +16,12 @@ export const tmsLayerWizardConfig: LayerWizard = { defaultMessage: 'Tile map service configured in interface', }), icon: 'grid', - renderWizard: ({ previewLayer }: RenderWizardArguments) => { + renderWizard: ({ previewLayers }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: XYZTMSSourceConfig) => { const layerDescriptor = TileLayer.createDescriptor({ sourceDescriptor: XYZTMSSource.createDescriptor(sourceConfig), }); - previewLayer(layerDescriptor); + previewLayers([layerDescriptor]); }; return ; }, diff --git a/x-pack/plugins/maps/public/classes/styles/color_utils.test.js b/x-pack/plugins/maps/public/classes/styles/color_utils.test.ts similarity index 100% rename from x-pack/plugins/maps/public/classes/styles/color_utils.test.js rename to x-pack/plugins/maps/public/classes/styles/color_utils.test.ts diff --git a/x-pack/plugins/maps/public/classes/styles/color_utils.js b/x-pack/plugins/maps/public/classes/styles/color_utils.tsx similarity index 53% rename from x-pack/plugins/maps/public/classes/styles/color_utils.js rename to x-pack/plugins/maps/public/classes/styles/color_utils.tsx index 9dc79c006dffe..116e03096b0f5 100644 --- a/x-pack/plugins/maps/public/classes/styles/color_utils.js +++ b/x-pack/plugins/maps/public/classes/styles/color_utils.tsx @@ -7,73 +7,85 @@ import React from 'react'; import tinycolor from 'tinycolor2'; import chroma from 'chroma-js'; +// @ts-ignore import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; import { ColorGradient } from './components/color_gradient'; -import { vislibColorMaps } from '../../../../../../src/plugins/charts/public'; +import { RawColorSchema, vislibColorMaps } from '../../../../../../src/plugins/charts/public'; export const GRADIENT_INTERVALS = 8; -export const DEFAULT_FILL_COLORS = euiPaletteColorBlind(); -export const DEFAULT_LINE_COLORS = [ - ...DEFAULT_FILL_COLORS.map((color) => tinycolor(color).darken().toHexString()), +export const DEFAULT_FILL_COLORS: string[] = euiPaletteColorBlind(); +export const DEFAULT_LINE_COLORS: string[] = [ + ...DEFAULT_FILL_COLORS.map((color: string) => tinycolor(color).darken().toHexString()), // Explicitly add black & white as border color options '#000', '#FFF', ]; -function getLegendColors(colorRamp, numLegendColors = 4) { +function getRGBColors(colorRamp: Array<[number, number[]]>, numLegendColors: number = 4): string[] { const colors = []; - colors[0] = getColor(colorRamp, 0); + colors[0] = getRGBColor(colorRamp, 0); for (let i = 1; i < numLegendColors - 1; i++) { - colors[i] = getColor(colorRamp, Math.floor((colorRamp.length * i) / numLegendColors)); + colors[i] = getRGBColor(colorRamp, Math.floor((colorRamp.length * i) / numLegendColors)); } - colors[numLegendColors - 1] = getColor(colorRamp, colorRamp.length - 1); + colors[numLegendColors - 1] = getRGBColor(colorRamp, colorRamp.length - 1); return colors; } -function getColor(colorRamp, i) { - const color = colorRamp[i][1]; - const red = Math.floor(color[0] * 255); - const green = Math.floor(color[1] * 255); - const blue = Math.floor(color[2] * 255); +function getRGBColor(colorRamp: Array<[number, number[]]>, i: number): string { + const rgbArray = colorRamp[i][1]; + const red = Math.floor(rgbArray[0] * 255); + const green = Math.floor(rgbArray[1] * 255); + const blue = Math.floor(rgbArray[2] * 255); return `rgb(${red},${green},${blue})`; } -function getColorRamp(colorRampName) { - const colorRamp = vislibColorMaps[colorRampName]; - if (!colorRamp) { +function getColorSchema(colorRampName: string): RawColorSchema { + const colorSchema = vislibColorMaps[colorRampName]; + if (!colorSchema) { throw new Error( `${colorRampName} not found. Expected one of following values: ${Object.keys( vislibColorMaps )}` ); } - return colorRamp; + return colorSchema; } -export function getRGBColorRangeStrings(colorRampName, numberColors = GRADIENT_INTERVALS) { - const colorRamp = getColorRamp(colorRampName); - return getLegendColors(colorRamp.value, numberColors); +export function getRGBColorRangeStrings( + colorRampName: string, + numberColors: number = GRADIENT_INTERVALS +): string[] { + const colorSchema = getColorSchema(colorRampName); + return getRGBColors(colorSchema.value, numberColors); } -export function getHexColorRangeStrings(colorRampName, numberColors = GRADIENT_INTERVALS) { +export function getHexColorRangeStrings( + colorRampName: string, + numberColors: number = GRADIENT_INTERVALS +): string[] { return getRGBColorRangeStrings(colorRampName, numberColors).map((rgbColor) => chroma(rgbColor).hex() ); } -export function getColorRampCenterColor(colorRampName) { +export function getColorRampCenterColor(colorRampName: string): string | null { if (!colorRampName) { return null; } - const colorRamp = getColorRamp(colorRampName); - const centerIndex = Math.floor(colorRamp.value.length / 2); - return getColor(colorRamp.value, centerIndex); + const colorSchema = getColorSchema(colorRampName); + const centerIndex = Math.floor(colorSchema.value.length / 2); + return getRGBColor(colorSchema.value, centerIndex); } // Returns an array of color stops // [ stop_input_1: number, stop_output_1: color, stop_input_n: number, stop_output_n: color ] -export function getOrdinalMbColorRampStops(colorRampName, min, max, numberColors) { +export function getOrdinalMbColorRampStops( + colorRampName: string, + min: number, + max: number, + numberColors: number +): Array | null { if (!colorRampName) { return null; } @@ -84,15 +96,18 @@ export function getOrdinalMbColorRampStops(colorRampName, min, max, numberColors const hexColors = getHexColorRangeStrings(colorRampName, numberColors); if (max === min) { - //just return single stop value + // just return single stop value return [max, hexColors[hexColors.length - 1]]; } const delta = max - min; - return hexColors.reduce((accu, stopColor, idx, srcArr) => { - const stopNumber = min + (delta * idx) / srcArr.length; - return [...accu, stopNumber, stopColor]; - }, []); + return hexColors.reduce( + (accu: Array, stopColor: string, idx: number, srcArr: string[]) => { + const stopNumber = min + (delta * idx) / srcArr.length; + return [...accu, stopNumber, stopColor]; + }, + [] + ); } export const COLOR_GRADIENTS = Object.keys(vislibColorMaps).map((colorRampName) => ({ @@ -102,7 +117,7 @@ export const COLOR_GRADIENTS = Object.keys(vislibColorMaps).map((colorRampName) export const COLOR_RAMP_NAMES = Object.keys(vislibColorMaps); -export function getLinearGradient(colorStrings) { +export function getLinearGradient(colorStrings: string[]): string { const intervals = colorStrings.length; let linearGradient = `linear-gradient(to right, ${colorStrings[0]} 0%,`; for (let i = 1; i < intervals - 1; i++) { @@ -112,7 +127,12 @@ export function getLinearGradient(colorStrings) { return `${linearGradient} ${colorStrings[colorStrings.length - 1]} 100%)`; } -const COLOR_PALETTES_CONFIGS = [ +export interface ColorPalette { + id: string; + colors: string[]; +} + +const COLOR_PALETTES_CONFIGS: ColorPalette[] = [ { id: 'palette_0', colors: euiPaletteColorBlind(), @@ -127,14 +147,14 @@ const COLOR_PALETTES_CONFIGS = [ }, ]; -export function getColorPalette(paletteId) { - const palette = COLOR_PALETTES_CONFIGS.find((palette) => palette.id === paletteId); +export function getColorPalette(paletteId: string): string[] | null { + const palette = COLOR_PALETTES_CONFIGS.find(({ id }: ColorPalette) => id === paletteId); return palette ? palette.colors : null; } export const COLOR_PALETTES = COLOR_PALETTES_CONFIGS.map((palette) => { const paletteDisplay = palette.colors.map((color) => { - const style = { + const style: React.CSSProperties = { backgroundColor: color, width: `${100 / palette.colors.length}%`, position: 'relative', diff --git a/x-pack/plugins/maps/public/classes/styles/components/color_gradient.js b/x-pack/plugins/maps/public/classes/styles/components/color_gradient.tsx similarity index 72% rename from x-pack/plugins/maps/public/classes/styles/components/color_gradient.js rename to x-pack/plugins/maps/public/classes/styles/components/color_gradient.tsx index bf7e88df3a694..b29146062e46d 100644 --- a/x-pack/plugins/maps/public/classes/styles/components/color_gradient.js +++ b/x-pack/plugins/maps/public/classes/styles/components/color_gradient.tsx @@ -11,17 +11,20 @@ import { getRGBColorRangeStrings, getLinearGradient, } from '../color_utils'; -import classNames from 'classnames'; -export const ColorGradient = ({ colorRamp, colorRampName, className }) => { +interface Props { + colorRamp?: string[]; + colorRampName?: string; +} + +export const ColorGradient = ({ colorRamp, colorRampName }: Props) => { if (!colorRamp && (!colorRampName || !COLOR_RAMP_NAMES.includes(colorRampName))) { return null; } - const classes = classNames('mapColorGradient', className); const rgbColorStrings = colorRampName ? getRGBColorRangeStrings(colorRampName, GRADIENT_INTERVALS) - : colorRamp; + : colorRamp!; const background = getLinearGradient(rgbColorStrings); - return

; + return
; }; diff --git a/x-pack/plugins/maps/public/classes/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.js.snap b/x-pack/plugins/maps/public/classes/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.tsx.snap similarity index 100% rename from x-pack/plugins/maps/public/classes/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.js.snap rename to x-pack/plugins/maps/public/classes/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.tsx.snap diff --git a/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_constants.js b/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_constants.ts similarity index 100% rename from x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_constants.js rename to x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_constants.ts diff --git a/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.test.js b/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.test.tsx similarity index 100% rename from x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.test.js rename to x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.test.tsx diff --git a/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.js b/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.tsx similarity index 85% rename from x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.js rename to x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.tsx index 6d38a7985269e..d15fdbd79de75 100644 --- a/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.js +++ b/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.tsx @@ -15,8 +15,13 @@ import { HEATMAP_COLOR_RAMP_LABEL, } from './heatmap_constants'; -export function HeatmapStyleEditor({ colorRampName, onHeatmapColorChange }) { - const onColorRampChange = (selectedColorRampName) => { +interface Props { + colorRampName: string; + onHeatmapColorChange: ({ colorRampName }: { colorRampName: string }) => void; +} + +export function HeatmapStyleEditor({ colorRampName, onHeatmapColorChange }: Props) { + const onColorRampChange = (selectedColorRampName: string) => { onHeatmapColorChange({ colorRampName: selectedColorRampName, }); diff --git a/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.js b/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.js index 3b5bcf591c2a6..5f920d0ba52d3 100644 --- a/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.js +++ b/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.js @@ -91,6 +91,7 @@ export class HeatmapStyle extends AbstractStyle { MAX_RANGE, GRADIENT_INTERVALS ); + // TODO handle null mbMap.setPaintProperty(layerId, 'heatmap-color', [ 'interpolate', ['linear'], diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/extract_color_from_style_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/extract_color_from_style_property.ts index 71f77bc313191..dadb3f201fa33 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/extract_color_from_style_property.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/extract_color_from_style_property.ts @@ -43,7 +43,7 @@ export function extractColorFromStyleProperty( } const palette = getColorPalette(dynamicOptions.colorCategory); - return palette[0]; + return palette ? palette[0] : defaultColor; } else { // return middle of gradient for dynamic style property if (dynamicOptions.useCustomColorRamp) { @@ -58,6 +58,7 @@ export function extractColorFromStyleProperty( if (!dynamicOptions.color) { return defaultColor; } - return getColorRampCenterColor(dynamicOptions.color); + const centerColor = getColorRampCenterColor(dynamicOptions.color); + return centerColor ? centerColor : defaultColor; } } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.test.ts b/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.test.ts new file mode 100644 index 0000000000000..bc032639dd07d --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('../../../kibana_services', () => { + const mockUiSettings = { + get: () => { + return undefined; + }, + }; + return { + getUiSettings: () => { + return mockUiSettings; + }, + }; +}); + +import { VECTOR_STYLES } from '../../../../common/constants'; +import { getDefaultStaticProperties } from './vector_style_defaults'; + +describe('getDefaultStaticProperties', () => { + test('Should use first color in DEFAULT_*_COLORS when no colors are used on the map', () => { + const styleProperties = getDefaultStaticProperties([]); + expect(styleProperties[VECTOR_STYLES.FILL_COLOR]!.options.color).toBe('#54B399'); + expect(styleProperties[VECTOR_STYLES.LINE_COLOR]!.options.color).toBe('#41937c'); + }); + + test('Should next color in DEFAULT_*_COLORS when colors are used on the map', () => { + const styleProperties = getDefaultStaticProperties(['#54B399']); + expect(styleProperties[VECTOR_STYLES.FILL_COLOR]!.options.color).toBe('#6092C0'); + expect(styleProperties[VECTOR_STYLES.LINE_COLOR]!.options.color).toBe('#4379aa'); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts b/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts index 86602381cf615..a6878a0d760c7 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts @@ -16,7 +16,6 @@ import { COLOR_PALETTES, DEFAULT_FILL_COLORS, DEFAULT_LINE_COLORS, - // @ts-ignore } from '../color_utils'; import { VectorStylePropertiesDescriptor } from '../../../../common/descriptor_types'; // @ts-ignore @@ -58,9 +57,13 @@ export function getDefaultProperties(mapColors: string[] = []): VectorStylePrope export function getDefaultStaticProperties( mapColors: string[] = [] ): VectorStylePropertiesDescriptor { - // Colors must be state-aware to reduce unnecessary incrementation - const lastColor = mapColors.pop(); - const nextColorIndex = (DEFAULT_FILL_COLORS.indexOf(lastColor) + 1) % DEFAULT_FILL_COLORS.length; + let nextColorIndex = 0; + if (mapColors.length) { + const lastColor = mapColors[mapColors.length - 1]; + if (DEFAULT_FILL_COLORS.includes(lastColor)) { + nextColorIndex = (DEFAULT_FILL_COLORS.indexOf(lastColor) + 1) % DEFAULT_FILL_COLORS.length; + } + } const nextFillColor = DEFAULT_FILL_COLORS[nextColorIndex]; const nextLineColor = DEFAULT_LINE_COLORS[nextColorIndex]; diff --git a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/flyout_body.tsx b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/flyout_body.tsx index 75fb7a5bc4acc..b287064938ce5 100644 --- a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/flyout_body.tsx +++ b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/flyout_body.tsx @@ -24,7 +24,7 @@ export const FlyoutBody = (props: Props) => { } const renderWizardArgs = { - previewLayer: props.previewLayer, + previewLayers: props.previewLayers, mapColors: props.mapColors, isIndexingTriggered: props.isIndexingTriggered, onRemove: props.onRemove, diff --git a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/index.ts b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/index.ts index 968429ce91226..470e83f2d8090 100644 --- a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/index.ts +++ b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/index.ts @@ -7,22 +7,24 @@ import { AnyAction, Dispatch } from 'redux'; import { connect } from 'react-redux'; import { FlyoutFooter } from './view'; -import { getSelectedLayer } from '../../../selectors/map_selectors'; -import { clearTransientLayerStateAndCloseFlyout } from '../../../actions'; +import { hasPreviewLayers, isLoadingPreviewLayers } from '../../../selectors/map_selectors'; +import { removePreviewLayers, updateFlyout } from '../../../actions'; import { MapStoreState } from '../../../reducers/store'; +import { FLYOUT_STATE } from '../../../reducers/ui'; function mapStateToProps(state: MapStoreState) { - const selectedLayer = getSelectedLayer(state); - const hasLayerSelected = !!selectedLayer; return { - hasLayerSelected, - isLoading: hasLayerSelected && selectedLayer!.isLayerLoading(), + hasPreviewLayers: hasPreviewLayers(state), + isLoading: isLoadingPreviewLayers(state), }; } function mapDispatchToProps(dispatch: Dispatch) { return { - closeFlyout: () => dispatch(clearTransientLayerStateAndCloseFlyout()), + closeFlyout: () => { + dispatch(updateFlyout(FLYOUT_STATE.NONE)); + dispatch(removePreviewLayers()); + }, }; } diff --git a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/view.tsx b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/view.tsx index 6f4d25a9c6c3e..2e122324c50fb 100644 --- a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/view.tsx +++ b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/view.tsx @@ -20,7 +20,7 @@ interface Props { disableNextButton: boolean; nextButtonText: string; closeFlyout: () => void; - hasLayerSelected: boolean; + hasPreviewLayers: boolean; isLoading: boolean; } @@ -30,14 +30,14 @@ export const FlyoutFooter = ({ disableNextButton, nextButtonText, closeFlyout, - hasLayerSelected, + hasPreviewLayers, isLoading, }: Props) => { const nextButton = showNextButton ? ( ) { return { - previewLayer: async (layerDescriptor: LayerDescriptor) => { - await dispatch(setSelectedLayer(null)); - await dispatch(removeTransientLayer()); - dispatch(addLayer(layerDescriptor)); - dispatch(setSelectedLayer(layerDescriptor.id)); - dispatch(setTransientLayer(layerDescriptor.id)); + addPreviewLayers: (layerDescriptors: LayerDescriptor[]) => { + dispatch(addPreviewLayers(layerDescriptors)); }, - removeTransientLayer: () => { - dispatch(setSelectedLayer(null)); - dispatch(removeTransientLayer()); - }, - selectLayerAndAdd: () => { - dispatch(setTransientLayer(null)); + promotePreviewLayers: () => { + dispatch(setFirstPreviewLayerToSelectedLayer()); dispatch(updateFlyout(FLYOUT_STATE.LAYER_PANEL)); + dispatch(promotePreviewLayers()); }, setIndexingTriggered: () => dispatch(updateIndexingStage(INDEXING_STAGE.TRIGGERED)), resetIndexing: () => dispatch(updateIndexingStage(null)), diff --git a/x-pack/plugins/maps/public/connected_components/add_layer_panel/view.tsx b/x-pack/plugins/maps/public/connected_components/add_layer_panel/view.tsx index d382a4085fe19..c1b6dcc1e12a6 100644 --- a/x-pack/plugins/maps/public/connected_components/add_layer_panel/view.tsx +++ b/x-pack/plugins/maps/public/connected_components/add_layer_panel/view.tsx @@ -17,17 +17,15 @@ interface Props { isIndexingReady: boolean; isIndexingSuccess: boolean; isIndexingTriggered: boolean; - previewLayer: (layerDescriptor: LayerDescriptor) => void; - removeTransientLayer: () => void; + addPreviewLayers: (layerDescriptors: LayerDescriptor[]) => void; + promotePreviewLayers: () => void; resetIndexing: () => void; - selectLayerAndAdd: () => void; setIndexingTriggered: () => void; } interface State { importView: boolean; isIndexingSource: boolean; - layerDescriptor: LayerDescriptor | null; layerImportAddReady: boolean; layerWizard: LayerWizard | null; } @@ -37,7 +35,6 @@ export class AddLayerPanel extends Component { state = { layerWizard: null, - layerDescriptor: null, // TODO get this from redux store instead of storing locally isIndexingSource: false, importView: false, layerImportAddReady: false, @@ -57,21 +54,13 @@ export class AddLayerPanel extends Component { } } - _previewLayer = (layerDescriptor: LayerDescriptor | null, isIndexingSource?: boolean) => { + _previewLayers = (layerDescriptors: LayerDescriptor[], isIndexingSource?: boolean) => { if (!this._isMounted) { return; } - if (!layerDescriptor) { - this.setState({ - layerDescriptor: null, - isIndexingSource: false, - }); - this.props.removeTransientLayer(); - return; - } - this.setState({ layerDescriptor, isIndexingSource: !!isIndexingSource }); - this.props.previewLayer(layerDescriptor); + this.setState({ isIndexingSource: layerDescriptors.length ? !!isIndexingSource : false }); + this.props.addPreviewLayers(layerDescriptors); }; _clearLayerData = ({ keepSourceType = false }: { keepSourceType: boolean }) => { @@ -80,7 +69,6 @@ export class AddLayerPanel extends Component { } const newState: Partial = { - layerDescriptor: null, isIndexingSource: false, }; if (!keepSourceType) { @@ -90,7 +78,7 @@ export class AddLayerPanel extends Component { // @ts-ignore this.setState(newState); - this.props.removeTransientLayer(); + this.props.addPreviewLayers([]); }; _onWizardSelect = (layerWizard: LayerWizard) => { @@ -101,7 +89,7 @@ export class AddLayerPanel extends Component { if (this.state.isIndexingSource && !this.props.isIndexingTriggered) { this.props.setIndexingTriggered(); } else { - this.props.selectLayerAndAdd(); + this.props.promotePreviewLayers(); if (this.state.importView) { this.setState({ layerImportAddReady: false, @@ -126,7 +114,7 @@ export class AddLayerPanel extends Component { }); const isNextBtnEnabled = this.state.importView ? this.props.isIndexingReady || this.props.isIndexingSuccess - : !!this.state.layerDescriptor; + : true; return ( @@ -141,7 +129,7 @@ export class AddLayerPanel extends Component { onClear={() => this._clearLayerData({ keepSourceType: false })} onRemove={() => this._clearLayerData({ keepSourceType: true })} onWizardSelect={this._onWizardSelect} - previewLayer={this._previewLayer} + previewLayers={this._previewLayers} /> { - await dispatch(removeTransientLayer()); await dispatch(setSelectedLayer(layerId)); dispatch(updateFlyout(FLYOUT_STATE.LAYER_PANEL)); }, diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js index c0ce24fef9cd8..b17078ae37113 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js @@ -239,7 +239,8 @@ export class TOCEntry extends React.Component { 'mapTocEntry-isDragging': this.props.isDragging, 'mapTocEntry-isDraggingOver': this.props.isDraggingOver, 'mapTocEntry-isSelected': - this.props.selectedLayer && this.props.selectedLayer.getId() === this.props.layer.getId(), + this.props.layer.isPreviewLayer() || + (this.props.selectedLayer && this.props.selectedLayer.getId() === this.props.layer.getId()), }); return ( diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.test.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.test.js index 90d756484c47f..543be9395d0bc 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.test.js +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.test.js @@ -21,6 +21,9 @@ const mockLayer = { getDisplayName: () => { return 'layer 1'; }, + isPreviewLayer: () => { + return false; + }, isVisible: () => { return true; }, diff --git a/x-pack/plugins/maps/public/reducers/map.d.ts b/x-pack/plugins/maps/public/reducers/map.d.ts index 8fc655b2c837a..33794fcf8657d 100644 --- a/x-pack/plugins/maps/public/reducers/map.d.ts +++ b/x-pack/plugins/maps/public/reducers/map.d.ts @@ -66,7 +66,6 @@ export type MapState = { openTooltips: TooltipState[]; mapState: MapContext; selectedLayerId: string | null; - __transientLayerId: string | null; layerList: LayerDescriptor[]; waitingForMapReadyLayerList: LayerDescriptor[]; settings: MapSettings; diff --git a/x-pack/plugins/maps/public/reducers/map.js b/x-pack/plugins/maps/public/reducers/map.js index c5f3968b749f1..9a661fe4833a8 100644 --- a/x-pack/plugins/maps/public/reducers/map.js +++ b/x-pack/plugins/maps/public/reducers/map.js @@ -6,7 +6,6 @@ import { SET_SELECTED_LAYER, - SET_TRANSIENT_LAYER, UPDATE_LAYER_ORDER, LAYER_DATA_LOAD_STARTED, LAYER_DATA_LOAD_ENDED, @@ -126,7 +125,6 @@ export const DEFAULT_MAP_STATE = { hideViewControl: false, }, selectedLayerId: null, - __transientLayerId: null, layerList: [], waitingForMapReadyLayerList: [], settings: getDefaultMapSettings(), @@ -285,9 +283,6 @@ export function map(state = DEFAULT_MAP_STATE, action) { case SET_SELECTED_LAYER: const selectedMatch = state.layerList.find((layer) => layer.id === action.selectedLayerId); return { ...state, selectedLayerId: selectedMatch ? action.selectedLayerId : null }; - case SET_TRANSIENT_LAYER: - const transientMatch = state.layerList.find((layer) => layer.id === action.transientLayerId); - return { ...state, __transientLayerId: transientMatch ? action.transientLayerId : null }; case UPDATE_LAYER_ORDER: return { ...state, diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts index 0789222b0bf38..fd887d360c2e0 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts @@ -137,9 +137,6 @@ export const getSelectedLayerId = ({ map }: MapStoreState): string | null => { return !map.selectedLayerId || !map.layerList ? null : map.selectedLayerId; }; -export const getTransientLayerId = ({ map }: MapStoreState): string | null => - map.__transientLayerId; - export const getLayerListRaw = ({ map }: MapStoreState): LayerDescriptor[] => map.layerList ? map.layerList : []; @@ -331,15 +328,28 @@ export const getSelectedLayer = createSelector( } ); -export const getMapColors = createSelector( - getTransientLayerId, - getLayerListRaw, - (transientLayerId, layerList) => - layerList.reduce((accu: string[], layer: LayerDescriptor) => { - if (layer.id === transientLayerId) { - return accu; - } - const color: string | undefined = _.get(layer, 'style.properties.fillColor.options.color'); +export const hasPreviewLayers = createSelector(getLayerList, (layerList) => { + return layerList.some((layer) => { + return layer.isPreviewLayer(); + }); +}); + +export const isLoadingPreviewLayers = createSelector(getLayerList, (layerList) => { + return layerList.some((layer) => { + return layer.isPreviewLayer() && layer.isLayerLoading(); + }); +}); + +export const getMapColors = createSelector(getLayerListRaw, (layerList) => + layerList + .filter((layerDescriptor) => { + return !layerDescriptor.__isPreviewLayer; + }) + .reduce((accu: string[], layerDescriptor: LayerDescriptor) => { + const color: string | undefined = _.get( + layerDescriptor, + 'style.properties.fillColor.options.color' + ); if (color) accu.push(color); return accu; }, []) @@ -373,24 +383,20 @@ export const getQueryableUniqueIndexPatternIds = createSelector(getLayerList, (l return _.uniq(indexPatternIds); }); -export const hasDirtyState = createSelector( - getLayerListRaw, - getTransientLayerId, - (layerListRaw, transientLayerId) => { - if (transientLayerId) { +export const hasDirtyState = createSelector(getLayerListRaw, (layerListRaw) => { + return layerListRaw.some((layerDescriptor) => { + if (layerDescriptor.__isPreviewLayer) { return true; } - return layerListRaw.some((layerDescriptor) => { - const trackedState = layerDescriptor[TRACKED_LAYER_DESCRIPTOR]; - if (!trackedState) { - return false; - } - const currentState = copyPersistentState(layerDescriptor); - return !_.isEqual(currentState, trackedState); - }); - } -); + const trackedState = layerDescriptor[TRACKED_LAYER_DESCRIPTOR]; + if (!trackedState) { + return false; + } + const currentState = copyPersistentState(layerDescriptor); + return !_.isEqual(currentState, trackedState); + }); +}); export const areLayersLoaded = createSelector( getLayerList, diff --git a/x-pack/plugins/ml/server/routes/job_validation.ts b/x-pack/plugins/ml/server/routes/job_validation.ts index 7c510b33d564c..0af8141a2a641 100644 --- a/x-pack/plugins/ml/server/routes/job_validation.ts +++ b/x-pack/plugins/ml/server/routes/job_validation.ts @@ -66,7 +66,7 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, let errorResp; const resp = await estimateBucketSpanFactory( context.ml!.mlClient.callAsCurrentUser, - context.core.elasticsearch.legacy.client.callAsInternalUser, + context.ml!.mlClient.callAsInternalUser, mlLicense.isSecurityEnabled() === false )(request.body) // this catch gets triggered when the estimation code runs without error @@ -187,7 +187,7 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, context.ml!.mlClient.callAsCurrentUser, request.body, version, - context.core.elasticsearch.legacy.client.callAsInternalUser, + context.ml!.mlClient.callAsInternalUser, mlLicense.isSecurityEnabled() === false ); diff --git a/x-pack/plugins/ml/server/shared_services/providers/system.ts b/x-pack/plugins/ml/server/shared_services/providers/system.ts index 698ac8e6261e5..33a4d854dd3e9 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/system.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/system.ts @@ -23,7 +23,7 @@ export interface MlSystemProvider { ): { mlCapabilities(): Promise; mlInfo(): Promise; - mlSearch(searchParams: SearchParams): Promise>; + mlAnomalySearch(searchParams: SearchParams): Promise>; }; } @@ -68,7 +68,7 @@ export function getMlSystemProvider( cloudId, }; }, - async mlSearch(searchParams: SearchParams): Promise> { + async mlAnomalySearch(searchParams: SearchParams): Promise> { isFullLicense(); return callAsCurrentUser('search', { ...searchParams, diff --git a/x-pack/plugins/security/common/licensing/license_features.ts b/x-pack/plugins/security/common/licensing/license_features.ts index 571d2630b2b17..8576e4bbc3555 100644 --- a/x-pack/plugins/security/common/licensing/license_features.ts +++ b/x-pack/plugins/security/common/licensing/license_features.ts @@ -38,6 +38,11 @@ export interface SecurityLicenseFeatures { */ readonly allowAccessAgreement: boolean; + /** + * Indicates whether we allow logging of audit events. + */ + readonly allowAuditLogging: boolean; + /** * Indicates whether we allow users to define document level security in roles. */ diff --git a/x-pack/plugins/security/common/licensing/license_service.test.ts b/x-pack/plugins/security/common/licensing/license_service.test.ts index 89901d663d82a..77e6460b7669a 100644 --- a/x-pack/plugins/security/common/licensing/license_service.test.ts +++ b/x-pack/plugins/security/common/licensing/license_service.test.ts @@ -24,6 +24,7 @@ describe('license features', function () { layout: 'error-es-unavailable', allowRbac: false, allowSubFeaturePrivileges: false, + allowAuditLogging: false, }); }); @@ -44,6 +45,7 @@ describe('license features', function () { layout: 'error-xpack-unavailable', allowRbac: false, allowSubFeaturePrivileges: false, + allowAuditLogging: false, }); }); @@ -63,6 +65,7 @@ describe('license features', function () { Array [ Object { "allowAccessAgreement": false, + "allowAuditLogging": false, "allowLogin": false, "allowRbac": false, "allowRoleDocumentLevelSecurity": false, @@ -82,6 +85,7 @@ describe('license features', function () { Array [ Object { "allowAccessAgreement": true, + "allowAuditLogging": true, "allowLogin": true, "allowRbac": true, "allowRoleDocumentLevelSecurity": true, @@ -118,6 +122,7 @@ describe('license features', function () { allowRoleFieldLevelSecurity: false, allowRbac: true, allowSubFeaturePrivileges: false, + allowAuditLogging: false, }); expect(getFeatureSpy).toHaveBeenCalledTimes(1); expect(getFeatureSpy).toHaveBeenCalledWith('security'); @@ -141,6 +146,7 @@ describe('license features', function () { allowRoleFieldLevelSecurity: false, allowRbac: false, allowSubFeaturePrivileges: false, + allowAuditLogging: false, }); }); @@ -163,6 +169,7 @@ describe('license features', function () { allowRoleFieldLevelSecurity: false, allowRbac: true, allowSubFeaturePrivileges: true, + allowAuditLogging: true, }); }); @@ -185,6 +192,30 @@ describe('license features', function () { allowRoleFieldLevelSecurity: true, allowRbac: true, allowSubFeaturePrivileges: true, + allowAuditLogging: true, + }); + }); + + it('should allow all basic features + audit logging for standard license', () => { + const mockRawLicense = licensingMock.createLicense({ + license: { mode: 'standard', type: 'standard' }, + features: { security: { isEnabled: true, isAvailable: true } }, + }); + + const serviceSetup = new SecurityLicenseService().setup({ + license$: of(mockRawLicense), + }); + expect(serviceSetup.license.getFeatures()).toEqual({ + showLogin: true, + allowLogin: true, + showLinks: true, + showRoleMappingsManagement: false, + allowAccessAgreement: false, + allowRoleDocumentLevelSecurity: false, + allowRoleFieldLevelSecurity: false, + allowRbac: true, + allowSubFeaturePrivileges: false, + allowAuditLogging: true, }); }); }); diff --git a/x-pack/plugins/security/common/licensing/license_service.ts b/x-pack/plugins/security/common/licensing/license_service.ts index 53cae857e5d66..75c7670f28a67 100644 --- a/x-pack/plugins/security/common/licensing/license_service.ts +++ b/x-pack/plugins/security/common/licensing/license_service.ts @@ -72,6 +72,7 @@ export class SecurityLicenseService { showLinks: false, showRoleMappingsManagement: false, allowAccessAgreement: false, + allowAuditLogging: false, allowRoleDocumentLevelSecurity: false, allowRoleFieldLevelSecurity: false, allowRbac: false, @@ -90,6 +91,7 @@ export class SecurityLicenseService { showLinks: false, showRoleMappingsManagement: false, allowAccessAgreement: false, + allowAuditLogging: false, allowRoleDocumentLevelSecurity: false, allowRoleFieldLevelSecurity: false, allowRbac: false, @@ -97,6 +99,7 @@ export class SecurityLicenseService { }; } + const isLicenseStandardOrBetter = rawLicense.hasAtLeast('standard'); const isLicenseGoldOrBetter = rawLicense.hasAtLeast('gold'); const isLicensePlatinumOrBetter = rawLicense.hasAtLeast('platinum'); return { @@ -105,6 +108,7 @@ export class SecurityLicenseService { showLinks: true, showRoleMappingsManagement: isLicenseGoldOrBetter, allowAccessAgreement: isLicenseGoldOrBetter, + allowAuditLogging: isLicenseStandardOrBetter, allowSubFeaturePrivileges: isLicenseGoldOrBetter, // Only platinum and trial licenses are compliant with field- and document-level security. allowRoleDocumentLevelSecurity: isLicensePlatinumOrBetter, diff --git a/x-pack/plugins/security/server/audit/audit_service.test.ts b/x-pack/plugins/security/server/audit/audit_service.test.ts new file mode 100644 index 0000000000000..94a2ada8df1da --- /dev/null +++ b/x-pack/plugins/security/server/audit/audit_service.test.ts @@ -0,0 +1,165 @@ +/* + * 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 { AuditService } from './audit_service'; +import { loggingServiceMock } from 'src/core/server/mocks'; +import { licenseMock } from '../../common/licensing/index.mock'; +import { ConfigSchema, ConfigType } from '../config'; +import { SecurityLicenseFeatures } from '../../common/licensing'; +import { BehaviorSubject } from 'rxjs'; + +const createConfig = (settings: Partial) => { + return ConfigSchema.validate(settings); +}; + +const config = createConfig({ + enabled: true, +}); + +describe('#setup', () => { + it('returns the expected contract', () => { + const logger = loggingServiceMock.createLogger(); + const auditService = new AuditService(logger); + const license = licenseMock.create(); + expect(auditService.setup({ license, config })).toMatchInlineSnapshot(` + Object { + "getLogger": [Function], + } + `); + }); +}); + +test(`calls the underlying logger with the provided message and requisite tags`, () => { + const pluginId = 'foo'; + + const logger = loggingServiceMock.createLogger(); + const license = licenseMock.create(); + license.features$ = new BehaviorSubject({ + allowAuditLogging: true, + } as SecurityLicenseFeatures).asObservable(); + + const auditService = new AuditService(logger).setup({ license, config }); + + const auditLogger = auditService.getLogger(pluginId); + + const eventType = 'bar'; + const message = 'this is my audit message'; + auditLogger.log(eventType, message); + + expect(logger.info).toHaveBeenCalledTimes(1); + expect(logger.info).toHaveBeenCalledWith(message, { + eventType, + tags: [pluginId, eventType], + }); +}); + +test(`calls the underlying logger with the provided metadata`, () => { + const pluginId = 'foo'; + + const logger = loggingServiceMock.createLogger(); + const license = licenseMock.create(); + license.features$ = new BehaviorSubject({ + allowAuditLogging: true, + } as SecurityLicenseFeatures).asObservable(); + + const auditService = new AuditService(logger).setup({ license, config }); + + const auditLogger = auditService.getLogger(pluginId); + + const eventType = 'bar'; + const message = 'this is my audit message'; + const metadata = Object.freeze({ + property1: 'value1', + property2: false, + property3: 123, + }); + auditLogger.log(eventType, message, metadata); + + expect(logger.info).toHaveBeenCalledTimes(1); + expect(logger.info).toHaveBeenCalledWith(message, { + eventType, + tags: [pluginId, eventType], + property1: 'value1', + property2: false, + property3: 123, + }); +}); + +test(`does not call the underlying logger if license does not support audit logging`, () => { + const pluginId = 'foo'; + + const logger = loggingServiceMock.createLogger(); + const license = licenseMock.create(); + license.features$ = new BehaviorSubject({ + allowAuditLogging: false, + } as SecurityLicenseFeatures).asObservable(); + + const auditService = new AuditService(logger).setup({ license, config }); + + const auditLogger = auditService.getLogger(pluginId); + + const eventType = 'bar'; + const message = 'this is my audit message'; + auditLogger.log(eventType, message); + + expect(logger.info).not.toHaveBeenCalled(); +}); + +test(`does not call the underlying logger if security audit logging is not enabled`, () => { + const pluginId = 'foo'; + + const logger = loggingServiceMock.createLogger(); + const license = licenseMock.create(); + license.features$ = new BehaviorSubject({ + allowAuditLogging: true, + } as SecurityLicenseFeatures).asObservable(); + + const auditService = new AuditService(logger).setup({ + license, + config: createConfig({ + enabled: false, + }), + }); + + const auditLogger = auditService.getLogger(pluginId); + + const eventType = 'bar'; + const message = 'this is my audit message'; + auditLogger.log(eventType, message); + + expect(logger.info).not.toHaveBeenCalled(); +}); + +test(`calls the underlying logger after license upgrade`, () => { + const pluginId = 'foo'; + + const logger = loggingServiceMock.createLogger(); + const license = licenseMock.create(); + + const features$ = new BehaviorSubject({ + allowAuditLogging: false, + } as SecurityLicenseFeatures); + + license.features$ = features$.asObservable(); + + const auditService = new AuditService(logger).setup({ license, config }); + + const auditLogger = auditService.getLogger(pluginId); + + const eventType = 'bar'; + const message = 'this is my audit message'; + auditLogger.log(eventType, message); + + expect(logger.info).not.toHaveBeenCalled(); + + // perform license upgrade + features$.next({ + allowAuditLogging: true, + } as SecurityLicenseFeatures); + + auditLogger.log(eventType, message); + + expect(logger.info).toHaveBeenCalledTimes(1); +}); diff --git a/x-pack/plugins/security/server/audit/audit_service.ts b/x-pack/plugins/security/server/audit/audit_service.ts new file mode 100644 index 0000000000000..93e69fd2601e9 --- /dev/null +++ b/x-pack/plugins/security/server/audit/audit_service.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 { Subscription } from 'rxjs'; +import { Logger } from '../../../../../src/core/server'; +import { SecurityLicense } from '../../common/licensing'; +import { ConfigType } from '../config'; + +export interface AuditLogger { + log: (eventType: string, message: string, data?: Record) => void; +} + +export interface AuditServiceSetup { + getLogger: (id?: string) => AuditLogger; +} + +interface AuditServiceSetupParams { + license: SecurityLicense; + config: ConfigType['audit']; +} + +export class AuditService { + private licenseFeaturesSubscription?: Subscription; + private auditLoggingEnabled = false; + + constructor(private readonly logger: Logger) {} + + setup({ license, config }: AuditServiceSetupParams): AuditServiceSetup { + if (config.enabled) { + this.licenseFeaturesSubscription = license.features$.subscribe(({ allowAuditLogging }) => { + this.auditLoggingEnabled = allowAuditLogging; + }); + } + + return { + getLogger: (id?: string): AuditLogger => { + return { + log: (eventType: string, message: string, data?: Record) => { + if (!this.auditLoggingEnabled) { + return; + } + + this.logger.info(message, { + tags: id ? [id, eventType] : [eventType], + eventType, + ...data, + }); + }, + }; + }, + }; + } + + stop() { + if (this.licenseFeaturesSubscription) { + this.licenseFeaturesSubscription.unsubscribe(); + this.licenseFeaturesSubscription = undefined; + } + } +} diff --git a/x-pack/plugins/security/server/audit/index.mock.ts b/x-pack/plugins/security/server/audit/index.mock.ts index 888aa3361faf0..07341cc06e889 100644 --- a/x-pack/plugins/security/server/audit/index.mock.ts +++ b/x-pack/plugins/security/server/audit/index.mock.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SecurityAuditLogger } from './audit_logger'; +import { SecurityAuditLogger } from './security_audit_logger'; +import { AuditService } from './audit_service'; export const securityAuditLoggerMock = { create() { @@ -15,3 +16,11 @@ export const securityAuditLoggerMock = { } as unknown) as jest.Mocked; }, }; + +export const auditServiceMock = { + create() { + return { + getLogger: jest.fn(), + } as jest.Mocked>; + }, +}; diff --git a/x-pack/plugins/security/server/audit/index.ts b/x-pack/plugins/security/server/audit/index.ts index 3ab253151b805..3db160c703e34 100644 --- a/x-pack/plugins/security/server/audit/index.ts +++ b/x-pack/plugins/security/server/audit/index.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { SecurityAuditLogger } from './audit_logger'; +export { AuditService, AuditServiceSetup, AuditLogger } from './audit_service'; +export { SecurityAuditLogger } from './security_audit_logger'; diff --git a/x-pack/plugins/security/server/audit/audit_logger.test.ts b/x-pack/plugins/security/server/audit/security_audit_logger.test.ts similarity index 91% rename from x-pack/plugins/security/server/audit/audit_logger.test.ts rename to x-pack/plugins/security/server/audit/security_audit_logger.test.ts index 4dfd69a2ccb1f..c6883f681cf41 100644 --- a/x-pack/plugins/security/server/audit/audit_logger.test.ts +++ b/x-pack/plugins/security/server/audit/security_audit_logger.test.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 { SecurityAuditLogger } from './audit_logger'; +import { SecurityAuditLogger } from './security_audit_logger'; const createMockAuditLogger = () => { return { @@ -14,7 +14,7 @@ const createMockAuditLogger = () => { describe(`#savedObjectsAuthorizationFailure`, () => { test('logs via auditLogger', () => { const auditLogger = createMockAuditLogger(); - const securityAuditLogger = new SecurityAuditLogger(() => auditLogger); + const securityAuditLogger = new SecurityAuditLogger(auditLogger); const username = 'foo-user'; const action = 'foo-action'; const types = ['foo-type-1', 'foo-type-2']; @@ -64,7 +64,7 @@ describe(`#savedObjectsAuthorizationFailure`, () => { describe(`#savedObjectsAuthorizationSuccess`, () => { test('logs via auditLogger', () => { const auditLogger = createMockAuditLogger(); - const securityAuditLogger = new SecurityAuditLogger(() => auditLogger); + const securityAuditLogger = new SecurityAuditLogger(auditLogger); const username = 'foo-user'; const action = 'foo-action'; const types = ['foo-type-1', 'foo-type-2']; @@ -96,7 +96,7 @@ describe(`#savedObjectsAuthorizationSuccess`, () => { describe(`#accessAgreementAcknowledged`, () => { test('logs via auditLogger', () => { const auditLogger = createMockAuditLogger(); - const securityAuditLogger = new SecurityAuditLogger(() => auditLogger); + const securityAuditLogger = new SecurityAuditLogger(auditLogger); const username = 'foo-user'; const provider = { type: 'saml', name: 'saml1' }; diff --git a/x-pack/plugins/security/server/audit/audit_logger.ts b/x-pack/plugins/security/server/audit/security_audit_logger.ts similarity index 89% rename from x-pack/plugins/security/server/audit/audit_logger.ts rename to x-pack/plugins/security/server/audit/security_audit_logger.ts index d7243ecbe13f8..87f7201f85665 100644 --- a/x-pack/plugins/security/server/audit/audit_logger.ts +++ b/x-pack/plugins/security/server/audit/security_audit_logger.ts @@ -5,10 +5,10 @@ */ import { AuthenticationProvider } from '../../common/types'; -import { LegacyAPI } from '../plugin'; +import { AuditLogger } from './audit_service'; export class SecurityAuditLogger { - constructor(private readonly getAuditLogger: () => LegacyAPI['auditLogger']) {} + constructor(private readonly logger: AuditLogger) {} savedObjectsAuthorizationFailure( username: string, @@ -23,7 +23,7 @@ export class SecurityAuditLogger { const missingString = missing .map(({ spaceId, privilege }) => `${spaceId ? `(${spaceId})` : ''}${privilege}`) .join(','); - this.getAuditLogger().log( + this.logger.log( 'saved_objects_authorization_failure', `${username} unauthorized to [${action}] [${typesString}]${spacesString}: missing [${missingString}]`, { @@ -46,7 +46,7 @@ export class SecurityAuditLogger { ) { const typesString = types.join(','); const spacesString = spaceIds.length ? ` in [${spaceIds.join(',')}]` : ''; - this.getAuditLogger().log( + this.logger.log( 'saved_objects_authorization_success', `${username} authorized to [${action}] [${typesString}]${spacesString}`, { @@ -60,7 +60,7 @@ export class SecurityAuditLogger { } accessAgreementAcknowledged(username: string, provider: AuthenticationProvider) { - this.getAuditLogger().log( + this.logger.log( 'access_agreement_acknowledged', `${username} acknowledged access agreement (${provider.type}/${provider.name}).`, { username, provider } diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index 0de86c72002c9..a0a06b537213d 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -27,6 +27,7 @@ export { SAMLLogin, OIDCLogin, } from './authentication'; +export { AuditLogger } from './audit'; export { SecurityPluginSetup }; export { AuthenticatedUser } from '../common/model'; diff --git a/x-pack/plugins/security/server/mocks.ts b/x-pack/plugins/security/server/mocks.ts index a6407366bbd3b..72a946d6c5155 100644 --- a/x-pack/plugins/security/server/mocks.ts +++ b/x-pack/plugins/security/server/mocks.ts @@ -9,10 +9,12 @@ import { SecurityPluginSetup } from './plugin'; import { authenticationMock } from './authentication/index.mock'; import { authorizationMock } from './authorization/index.mock'; import { licenseMock } from '../common/licensing/index.mock'; +import { auditServiceMock } from './audit/index.mock'; function createSetupMock() { const mockAuthz = authorizationMock.create(); return { + audit: auditServiceMock.create(), authc: authenticationMock.create(), authz: { actions: mockAuthz.actions, diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index d58c999ddccdf..fc49bdd9bc0c3 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -25,6 +25,7 @@ describe('Security Plugin', () => { idleTimeout: 1500, lifespan: null, }, + audit: { enabled: false }, authc: { selector: { enabled: false }, providers: ['saml', 'token'], @@ -50,9 +51,11 @@ describe('Security Plugin', () => { await expect(plugin.setup(mockCoreSetup, mockDependencies)).resolves.toMatchInlineSnapshot(` Object { "__legacyCompat": Object { - "registerLegacyAPI": [Function], "registerPrivilegesWithCluster": [Function], }, + "audit": Object { + "getLogger": [Function], + }, "authc": Object { "areAPIKeysEnabled": [Function], "createAPIKey": [Function], diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 89cffde92d564..cdfc6f0ae542f 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -24,7 +24,7 @@ import { ConfigSchema, createConfig } from './config'; import { defineRoutes } from './routes'; import { SecurityLicenseService, SecurityLicense } from '../common/licensing'; import { setupSavedObjects } from './saved_objects'; -import { SecurityAuditLogger } from './audit'; +import { AuditService, SecurityAuditLogger, AuditServiceSetup } from './audit'; import { elasticsearchClientPlugin } from './elasticsearch_client_plugin'; export type SpacesService = Pick< @@ -34,16 +34,6 @@ export type SpacesService = Pick< export type FeaturesService = Pick; -/** - * Describes a set of APIs that is available in the legacy platform only and required by this plugin - * to function properly. - */ -export interface LegacyAPI { - auditLogger: { - log: (eventType: string, message: string, data?: Record) => void; - }; -} - /** * Describes public Security plugin contract returned at the `setup` stage. */ @@ -60,6 +50,7 @@ export interface SecurityPluginSetup { >; authz: Pick; license: SecurityLicense; + audit: Pick; /** * If Spaces plugin is available it's supposed to register its SpacesService with Security plugin @@ -72,7 +63,6 @@ export interface SecurityPluginSetup { registerSpacesService: (service: SpacesService) => void; __legacyCompat: { - registerLegacyAPI: (legacyAPI: LegacyAPI) => void; registerPrivilegesWithCluster: () => void; }; } @@ -90,14 +80,7 @@ export class Plugin { private clusterClient?: ICustomClusterClient; private spacesService?: SpacesService | symbol = Symbol('not accessed'); private securityLicenseService?: SecurityLicenseService; - - private legacyAPI?: LegacyAPI; - private readonly getLegacyAPI = () => { - if (!this.legacyAPI) { - throw new Error('Legacy API is not registered!'); - } - return this.legacyAPI; - }; + private readonly auditService = new AuditService(this.initializerContext.logger.get('audit')); private readonly getSpacesService = () => { // Changing property value from Symbol to undefined denotes the fact that property was accessed. @@ -135,7 +118,9 @@ export class Plugin { license$: licensing.license$, }); - const auditLogger = new SecurityAuditLogger(() => this.getLegacyAPI().auditLogger); + const audit = this.auditService.setup({ license, config: config.audit }); + const auditLogger = new SecurityAuditLogger(audit.getLogger()); + const authc = await setupAuthentication({ auditLogger, http: core.http, @@ -178,6 +163,10 @@ export class Plugin { }); return deepFreeze({ + audit: { + getLogger: audit.getLogger, + }, + authc: { isAuthenticated: authc.isAuthenticated, getCurrentUser: authc.getCurrentUser, @@ -205,8 +194,6 @@ export class Plugin { }, __legacyCompat: { - registerLegacyAPI: (legacyAPI: LegacyAPI) => (this.legacyAPI = legacyAPI), - registerPrivilegesWithCluster: async () => await authz.registerPrivilegesWithCluster(), }, }); @@ -228,6 +215,8 @@ export class Plugin { this.securityLicenseService.stop(); this.securityLicenseService = undefined; } + + this.auditService.stop(); } private wasSpacesServiceAccessed() { diff --git a/x-pack/plugins/security/server/routes/views/login.test.ts b/x-pack/plugins/security/server/routes/views/login.test.ts index 5c41a48bf5ee4..fee3adbb19f97 100644 --- a/x-pack/plugins/security/server/routes/views/login.test.ts +++ b/x-pack/plugins/security/server/routes/views/login.test.ts @@ -172,6 +172,7 @@ describe('Login view routes', () => { showLinks: false, showRoleMappingsManagement: true, allowSubFeaturePrivileges: true, + allowAuditLogging: true, showLogin: true, }); diff --git a/x-pack/plugins/siem/kibana.json b/x-pack/plugins/siem/kibana.json index 1106781fd45e4..6b43b41df8eee 100644 --- a/x-pack/plugins/siem/kibana.json +++ b/x-pack/plugins/siem/kibana.json @@ -24,7 +24,8 @@ "newsfeed", "security", "spaces", - "usageCollection" + "usageCollection", + "lists" ], "server": true, "ui": true diff --git a/x-pack/plugins/siem/server/lib/detection_engine/README.md b/x-pack/plugins/siem/server/lib/detection_engine/README.md index 610e82fd5f6ee..695165e1990a9 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/README.md +++ b/x-pack/plugins/siem/server/lib/detection_engine/README.md @@ -165,3 +165,12 @@ go about doing so. `./signals/set_status_with_id.sh open` will update the status of the sample signal to open `./signals/set_status_with_query.sh closed` will update the status of the signals in the result of the query to closed. `./signals/set_status_with_query.sh open` will update the status of the signals in the result of the query to open. + +### Large List Exceptions + +To test out the functionality of large lists with rules, the user will need to import a list and post a rule with a reference to that exception list. The following outlines an example using the sample json rule provided in the repo. + +* First, set the appropriate env var in order to enable exceptions features`export ELASTIC_XPACK_SIEM_LISTS_FEATURE=true` and `export ELASTIC_XPACK_SIEM_EXCEPTIONS_LISTS=true` and start kibana +* Second, import a list of ips from a file called `ci-badguys.txt`. The command should look like this: +`cd $HOME/kibana/x-pack/plugins/lists/server/scripts && ./import_list_items_by_filename.sh ip ~/ci-badguys.txt` +* Then, from the detection engine scripts folder (`cd kibana/x-pack/plugins/siem/server/lib/detection_engine/scripts`) run `./post_rule.sh rules/queries/lists/query_with_list_plugin.json` diff --git a/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list_plugin.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list_plugin.json new file mode 100644 index 0000000000000..fa6fe6ac71117 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list_plugin.json @@ -0,0 +1,24 @@ +{ + "name": "Query with a list", + "description": "Query with a list only generate signals if source.ip is not in list", + "rule_id": "query-with-list", + "risk_score": 2, + "severity": "high", + "type": "query", + "query": "host.name: *", + "interval": "30s", + "language": "kuery", + "exceptions_list": [ + { + "field": "source.ip", + "values_operator": "excluded", + "values_type": "list", + "values": [ + { + "id": "ci-badguys.txt", + "name": "ip" + } + ] + } + ] +} diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts index 251a1e6d118ff..2d75ba4f42d12 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -101,7 +101,10 @@ export const sampleDocNoSortIdNoVersion = (someUuid: string = sampleIdGuid): Sig }, }); -export const sampleDocWithSortId = (someUuid: string = sampleIdGuid): SignalSourceHit => ({ +export const sampleDocWithSortId = ( + someUuid: string = sampleIdGuid, + ip?: string +): SignalSourceHit => ({ _index: 'myFakeSignalIndex', _type: 'doc', _score: 100, @@ -110,6 +113,9 @@ export const sampleDocWithSortId = (someUuid: string = sampleIdGuid): SignalSour _source: { someKey: 'someValue', '@timestamp': '2020-04-20T21:27:45+0000', + source: { + ip: ip ?? '127.0.0.1', + }, }, sort: ['1234567891111'], }); @@ -313,7 +319,8 @@ export const sampleDocSearchResultsNoSortIdNoHits = ( export const repeatedSearchResultsWithSortId = ( total: number, pageSize: number, - guids: string[] + guids: string[], + ips?: string[] ) => ({ took: 10, timed_out: false, @@ -327,7 +334,7 @@ export const repeatedSearchResultsWithSortId = ( total, max_score: 100, hits: Array.from({ length: pageSize }).map((x, index) => ({ - ...sampleDocWithSortId(guids[index]), + ...sampleDocWithSortId(guids[index], ips ? ips[index] : '127.0.0.1'), })), }, }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts index 9ac4d4087016a..5862e6c481431 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -86,5 +86,5 @@ export const bulkCreateMlSignals = async ( const anomalyResults = params.someResult; const ecsResults = transformAnomalyResultsToEcs(anomalyResults); - return singleBulkCreate({ ...params, someResult: ecsResults }); + return singleBulkCreate({ ...params, filteredEvents: ecsResults }); }; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/filter_events_with_list.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/filter_events_with_list.test.ts new file mode 100644 index 0000000000000..86efdb6603493 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/filter_events_with_list.test.ts @@ -0,0 +1,242 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import uuid from 'uuid'; +import { filterEventsAgainstList } from './filter_events_with_list'; +import { mockLogger, repeatedSearchResultsWithSortId } from './__mocks__/es_results'; + +import { ListClient } from '../../../../../lists/server'; + +const someGuids = Array.from({ length: 13 }).map((x) => uuid.v4()); + +describe('filterEventsAgainstList', () => { + it('should respond with eventSearchResult if exceptionList is empty', async () => { + const res = await filterEventsAgainstList({ + logger: mockLogger, + listClient: ({ + getListItemByValues: async () => [], + } as unknown) as ListClient, + exceptionsList: undefined, + eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [ + '1.1.1.1', + '2.2.2.2', + '3.3.3.3', + '7.7.7.7', + ]), + }); + expect(res.hits.hits.length).toEqual(4); + }); + + it('should throw an error if malformed exception list present', async () => { + let message = ''; + try { + await filterEventsAgainstList({ + logger: mockLogger, + listClient: ({ + getListItemByValues: async () => [], + } as unknown) as ListClient, + exceptionsList: [ + { + field: 'source.ip', + values_operator: 'excluded', + values_type: 'list', + values: undefined, + }, + ], + eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [ + '1.1.1.1', + '2.2.2.2', + '3.3.3.3', + '7.7.7.7', + ]), + }); + } catch (exc) { + message = exc.message; + } + expect(message).toEqual( + 'Failed to query lists index. Reason: Malformed exception list provided' + ); + }); + + it('should throw an error if unsupported exception type', async () => { + let message = ''; + try { + await filterEventsAgainstList({ + logger: mockLogger, + listClient: ({ + getListItemByValues: async () => [], + } as unknown) as ListClient, + exceptionsList: [ + { + field: 'source.ip', + values_operator: 'excluded', + values_type: 'list', + values: [ + { + id: 'ci-badguys.txt', + name: 'unsupportedListPluginType', + }, + ], + }, + ], + eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [ + '1.1.1.1', + '2.2.2.2', + '3.3.3.3', + '7.7.7.7', + ]), + }); + } catch (exc) { + message = exc.message; + } + expect(message).toEqual( + 'Failed to query lists index. Reason: Unsupported list type used, please use one of ip,keyword' + ); + }); + + describe('operator_type is includes', () => { + it('should respond with same list if no items match value list', async () => { + const res = await filterEventsAgainstList({ + logger: mockLogger, + listClient: ({ + getListItemByValues: async () => [], + } as unknown) as ListClient, + exceptionsList: [ + { + field: 'source.ip', + values_operator: 'included', + values_type: 'list', + values: [ + { + id: 'ci-badguys.txt', + name: 'ip', + }, + ], + }, + ], + eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3)), + }); + expect(res.hits.hits.length).toEqual(4); + }); + it('should respond with less items in the list if some values match', async () => { + let outerType = ''; + let outerListId = ''; + const res = await filterEventsAgainstList({ + logger: mockLogger, + listClient: ({ + getListItemByValues: async ({ + value, + type, + listId, + }: { + type: string; + listId: string; + value: string[]; + }) => { + outerType = type; + outerListId = listId; + return value.slice(0, 2).map((item) => ({ + value: item, + })); + }, + } as unknown) as ListClient, + exceptionsList: [ + { + field: 'source.ip', + values_operator: 'included', + values_type: 'list', + values: [ + { + id: 'ci-badguys.txt', + name: 'ip', + }, + ], + }, + ], + eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [ + '1.1.1.1', + '2.2.2.2', + '3.3.3.3', + '7.7.7.7', + ]), + }); + expect(outerType).toEqual('ip'); + expect(outerListId).toEqual('ci-badguys.txt'); + expect(res.hits.hits.length).toEqual(2); + }); + }); + describe('operator type is excluded', () => { + it('should respond with empty list if no items match value list', async () => { + const res = await filterEventsAgainstList({ + logger: mockLogger, + listClient: ({ + getListItemByValues: async () => [], + } as unknown) as ListClient, + exceptionsList: [ + { + field: 'source.ip', + values_operator: 'excluded', + values_type: 'list', + values: [ + { + id: 'ci-badguys.txt', + name: 'ip', + }, + ], + }, + ], + eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3)), + }); + expect(res.hits.hits.length).toEqual(0); + }); + it('should respond with less items in the list if some values match', async () => { + let outerType = ''; + let outerListId = ''; + const res = await filterEventsAgainstList({ + logger: mockLogger, + listClient: ({ + getListItemByValues: async ({ + value, + type, + listId, + }: { + type: string; + listId: string; + value: string[]; + }) => { + outerType = type; + outerListId = listId; + return value.slice(0, 2).map((item) => ({ + value: item, + })); + }, + } as unknown) as ListClient, + exceptionsList: [ + { + field: 'source.ip', + values_operator: 'excluded', + values_type: 'list', + values: [ + { + id: 'ci-badguys.txt', + name: 'ip', + }, + ], + }, + ], + eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [ + '1.1.1.1', + '2.2.2.2', + '3.3.3.3', + '7.7.7.7', + ]), + }); + expect(outerType).toEqual('ip'); + expect(outerListId).toEqual('ci-badguys.txt'); + expect(res.hits.hits.length).toEqual(2); + }); + }); +}); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/filter_events_with_list.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/filter_events_with_list.ts new file mode 100644 index 0000000000000..400bb5dda46e7 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/filter_events_with_list.ts @@ -0,0 +1,111 @@ +/* + * 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 { get } from 'lodash/fp'; +import { Logger } from 'src/core/server'; + +import { type } from '../../../../../lists/common/schemas/common'; +import { ListClient } from '../../../../../lists/server'; +import { SignalSearchResponse, SearchTypes } from './types'; +import { RuleAlertParams } from '../types'; +import { List } from '../routes/schemas/types/lists_default_array'; + +interface FilterEventsAgainstList { + listClient: ListClient; + exceptionsList: RuleAlertParams['exceptions_list']; + logger: Logger; + eventSearchResult: SignalSearchResponse; +} + +export const filterEventsAgainstList = async ({ + listClient, + exceptionsList, + logger, + eventSearchResult, +}: FilterEventsAgainstList): Promise => { + try { + if (exceptionsList == null || exceptionsList.length === 0) { + return eventSearchResult; + } + + // narrow unioned type to be single + const isStringableType = (val: SearchTypes) => + ['string', 'number', 'boolean'].includes(typeof val); + // grab the signals with values found in the given exception lists. + const filteredHitsPromises = exceptionsList + .filter((exceptionItem: List) => exceptionItem.values_type === 'list') + .map(async (exceptionItem: List) => { + if (exceptionItem.values == null || exceptionItem.values.length === 0) { + throw new Error('Malformed exception list provided'); + } + if (!type.is(exceptionItem.values[0].name)) { + throw new Error( + `Unsupported list type used, please use one of ${Object.keys(type.keys).join()}` + ); + } + if (!exceptionItem.values[0].id) { + throw new Error(`Missing list id for exception on field ${exceptionItem.field}`); + } + // acquire the list values we are checking for. + const valuesOfGivenType = eventSearchResult.hits.hits.reduce((acc, searchResultItem) => { + const valueField = get(exceptionItem.field, searchResultItem._source); + if (valueField != null && isStringableType(valueField)) { + acc.add(valueField.toString()); + } + return acc; + }, new Set()); + + // matched will contain any list items that matched with the + // values passed in from the Set. + const matchedListItems = await listClient.getListItemByValues({ + listId: exceptionItem.values[0].id, + type: exceptionItem.values[0].name, + value: [...valuesOfGivenType], + }); + + // create a set of list values that were a hit - easier to work with + const matchedListItemsSet = new Set( + matchedListItems.map((item) => item.value) + ); + + // do a single search after with these values. + // painless script to do nested query in elasticsearch + // filter out the search results that match with the values found in the list. + const operator = exceptionItem.values_operator; + const filteredEvents = eventSearchResult.hits.hits.filter((item) => { + const eventItem = get(exceptionItem.field, item._source); + if (operator === 'included') { + if (eventItem != null) { + return !matchedListItemsSet.has(eventItem); + } + } else if (operator === 'excluded') { + if (eventItem != null) { + return matchedListItemsSet.has(eventItem); + } + } + return false; + }); + const diff = eventSearchResult.hits.hits.length - filteredEvents.length; + logger.debug(`Lists filtered out ${diff} events`); + return filteredEvents; + }); + + const filteredHits = await Promise.all(filteredHitsPromises); + const toReturn: SignalSearchResponse = { + took: eventSearchResult.took, + timed_out: eventSearchResult.timed_out, + _shards: eventSearchResult._shards, + hits: { + total: filteredHits.length, + max_score: eventSearchResult.hits.max_score, + hits: filteredHits.flat(), + }, + }; + + return toReturn; + } catch (exc) { + throw new Error(`Failed to query lists index. Reason: ${exc.message}`); + } +}; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/find_ml_signals.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/find_ml_signals.ts index 342976f3fd0fc..e95b713105fc6 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/find_ml_signals.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/find_ml_signals.ts @@ -27,14 +27,14 @@ export const findMlSignals = async ({ from: string; to: string; }) => { - const { mlSearch } = ml.mlSystemProvider(callCluster, request); + const { mlAnomalySearch } = ml.mlSystemProvider(callCluster, request); const params = { jobIds: [jobId], threshold: anomalyThreshold, earliestMs: dateMath.parse(from)?.valueOf() ?? 0, latestMs: dateMath.parse(to)?.valueOf() ?? 0, }; - const relevantAnomalies = await getAnomalies(params, mlSearch); + const relevantAnomalies = await getAnomalies(params, mlAnomalySearch); return relevantAnomalies; }; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index 208f0e680722d..7479ab54af6e6 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -10,58 +10,28 @@ import { sampleRuleGuid, mockLogger, repeatedSearchResultsWithSortId, - sampleBulkCreateDuplicateResult, - sampleDocSearchResultsNoSortId, - sampleDocSearchResultsNoSortIdNoHits, } from './__mocks__/es_results'; import { searchAfterAndBulkCreate } from './search_after_bulk_create'; import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; import uuid from 'uuid'; +import { ListClient } from '../../../../../lists/server'; +import { ListItemArraySchema } from '../../../../../lists/common/schemas'; describe('searchAfterAndBulkCreate', () => { let mockService: AlertServicesMock; let inputIndexPattern: string[] = []; + const someGuids = Array.from({ length: 13 }).map(() => uuid.v4()); beforeEach(() => { jest.clearAllMocks(); inputIndexPattern = ['auditbeat-*']; mockService = alertsMock.createAlertServices(); }); - test('if successful with empty search results', async () => { - const sampleParams = sampleRuleAlertParams(); - const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ - someResult: sampleEmptyDocSearchResults(), - ruleParams: sampleParams, - services: mockService, - logger: mockLogger, - id: sampleRuleGuid, - inputIndexPattern, - signalsIndex: DEFAULT_SIGNALS_INDEX, - name: 'rule-name', - actions: [], - createdAt: '2020-01-28T15:58:34.810Z', - updatedAt: '2020-01-28T15:59:14.004Z', - createdBy: 'elastic', - updatedBy: 'elastic', - interval: '5m', - enabled: true, - pageSize: 1, - filter: undefined, - refresh: false, - tags: ['some fake tag 1', 'some fake tag 2'], - throttle: 'no_actions', - }); - expect(mockService.callCluster).toHaveBeenCalledTimes(0); - expect(success).toEqual(true); - expect(createdSignalsCount).toEqual(0); - expect(lastLookBackDate).toBeNull(); - }); - - test('if successful iteration of while loop with maxDocs', async () => { + test('should return success with number of searches less than max signals', async () => { const sampleParams = sampleRuleAlertParams(30); - const someGuids = Array.from({ length: 13 }).map((x) => uuid.v4()); mockService.callCluster + .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3))) .mockResolvedValueOnce({ took: 100, errors: false, @@ -76,7 +46,7 @@ describe('searchAfterAndBulkCreate', () => { }, ], }) - .mockResolvedValueOnce(repeatedSearchResultsWithSortId(3, 1, someGuids.slice(0, 3))) + .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(3, 6))) .mockResolvedValueOnce({ took: 100, errors: false, @@ -91,7 +61,22 @@ describe('searchAfterAndBulkCreate', () => { }, ], }) - .mockResolvedValueOnce(repeatedSearchResultsWithSortId(3, 1, someGuids.slice(3, 6))) + .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(6, 9))) + .mockResolvedValueOnce({ + took: 100, + errors: false, + items: [ + { + fakeItemValue: 'fakeItemKey', + }, + { + create: { + status: 201, + }, + }, + ], + }) + .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(9, 12))) .mockResolvedValueOnce({ took: 100, errors: false, @@ -107,8 +92,23 @@ describe('searchAfterAndBulkCreate', () => { ], }); const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ - someResult: repeatedSearchResultsWithSortId(3, 1, someGuids.slice(6, 9)), ruleParams: sampleParams, + listClient: ({ + getListItemByValues: async () => [], + } as unknown) as ListClient, + exceptionsList: [ + { + field: 'source.ip', + values_operator: 'included', + values_type: 'list', + values: [ + { + id: 'ci-badguys.txt', + name: 'ip', + }, + ], + }, + ], services: mockService, logger: mockLogger, id: sampleRuleGuid, @@ -128,63 +128,63 @@ describe('searchAfterAndBulkCreate', () => { tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); - expect(mockService.callCluster).toHaveBeenCalledTimes(5); expect(success).toEqual(true); - expect(createdSignalsCount).toEqual(3); + expect(mockService.callCluster).toHaveBeenCalledTimes(8); + expect(createdSignalsCount).toEqual(4); expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); - test('if unsuccessful first bulk create', async () => { - const someGuids = Array.from({ length: 4 }).map((x) => uuid.v4()); - const sampleParams = sampleRuleAlertParams(10); - mockService.callCluster.mockResolvedValue(sampleBulkCreateDuplicateResult); + test('should return success when no search results are in the allowlist', async () => { + const sampleParams = sampleRuleAlertParams(30); + mockService.callCluster + .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3))) + .mockResolvedValueOnce({ + took: 100, + errors: false, + items: [ + { + fakeItemValue: 'fakeItemKey', + }, + { + create: { + status: 201, + }, + }, + { + create: { + status: 201, + }, + }, + { + create: { + status: 201, + }, + }, + { + create: { + status: 201, + }, + }, + ], + }); const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ - someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), ruleParams: sampleParams, - services: mockService, - logger: mockLogger, - id: sampleRuleGuid, - inputIndexPattern, - signalsIndex: DEFAULT_SIGNALS_INDEX, - name: 'rule-name', - actions: [], - createdAt: '2020-01-28T15:58:34.810Z', - updatedAt: '2020-01-28T15:59:14.004Z', - createdBy: 'elastic', - updatedBy: 'elastic', - interval: '5m', - enabled: true, - pageSize: 1, - filter: undefined, - refresh: false, - tags: ['some fake tag 1', 'some fake tag 2'], - throttle: 'no_actions', - }); - expect(mockLogger.error).toHaveBeenCalled(); - expect(success).toEqual(false); - expect(createdSignalsCount).toEqual(1); - expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); - }); - - test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids', async () => { - const sampleParams = sampleRuleAlertParams(); - mockService.callCluster.mockResolvedValueOnce({ - took: 100, - errors: false, - items: [ + listClient: ({ + getListItemByValues: async () => [], + } as unknown) as ListClient, + exceptionsList: [ { - fakeItemValue: 'fakeItemKey', - }, - { - create: { - status: 201, - }, + field: 'source.ip', + values_operator: 'included', + values_type: 'list', + values: [ + { + id: 'ci-badguys.txt', + name: 'ip', + }, + ], }, ], - }); - const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ - someResult: sampleDocSearchResultsNoSortId(), - ruleParams: sampleParams, services: mockService, logger: mockLogger, id: sampleRuleGuid, @@ -204,31 +204,59 @@ describe('searchAfterAndBulkCreate', () => { tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); - expect(mockLogger.error).toHaveBeenCalled(); - expect(success).toEqual(false); - expect(createdSignalsCount).toEqual(1); + expect(success).toEqual(true); + expect(mockService.callCluster).toHaveBeenCalledTimes(2); + expect(createdSignalsCount).toEqual(4); // should not create any signals because all events were in the allowlist expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); - test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids and 0 total hits', async () => { - const sampleParams = sampleRuleAlertParams(); - mockService.callCluster.mockResolvedValueOnce({ - took: 100, - errors: false, - items: [ - { - fakeItemValue: 'fakeItemKey', - }, - { - create: { - status: 201, + test('should return success when no exceptions list provided', async () => { + const sampleParams = sampleRuleAlertParams(30); + mockService.callCluster + .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3))) + .mockResolvedValueOnce({ + took: 100, + errors: false, + items: [ + { + fakeItemValue: 'fakeItemKey', }, - }, - ], - }); + { + create: { + status: 201, + }, + }, + { + create: { + status: 201, + }, + }, + { + create: { + status: 201, + }, + }, + { + create: { + status: 201, + }, + }, + ], + }); const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ - someResult: sampleDocSearchResultsNoSortIdNoHits(), ruleParams: sampleParams, + listClient: ({ + getListItemByValues: async ({ + value, + }: { + type: string; + listId: string; + value: string[]; + }) => { + return value.map((item) => ({ value: item })); + }, + } as unknown) as ListClient, + exceptionsList: undefined, services: mockService, logger: mockLogger, id: sampleRuleGuid, @@ -249,31 +277,35 @@ describe('searchAfterAndBulkCreate', () => { throttle: 'no_actions', }); expect(success).toEqual(true); - expect(createdSignalsCount).toEqual(1); + expect(mockService.callCluster).toHaveBeenCalledTimes(2); + expect(createdSignalsCount).toEqual(4); // should not create any signals because all events were in the allowlist expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); - test('if successful iteration of while loop with maxDocs and search after returns results with no sort ids', async () => { + test('if unsuccessful first bulk create', async () => { const sampleParams = sampleRuleAlertParams(10); - const someGuids = Array.from({ length: 4 }).map((x) => uuid.v4()); mockService.callCluster - .mockResolvedValueOnce({ - took: 100, - errors: false, - items: [ - { - fakeItemValue: 'fakeItemKey', - }, - { - create: { - status: 201, - }, - }, - ], - }) - .mockResolvedValueOnce(sampleDocSearchResultsNoSortId()); + .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3))) + .mockRejectedValue(new Error('bulk failed')); // Added this recently const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ - someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), + listClient: ({ + getListItemByValues: async () => { + return ([] as unknown) as ListItemArraySchema; + }, + } as unknown) as ListClient, + exceptionsList: [ + { + field: 'source.ip', + values_operator: 'included', + values_type: 'list', + values: [ + { + id: 'ci-badguys.txt', + name: 'ip', + }, + ], + }, + ], ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -294,32 +326,40 @@ describe('searchAfterAndBulkCreate', () => { tags: ['some fake tag 1', 'some fake tag 2'], throttle: 'no_actions', }); - expect(success).toEqual(true); - expect(createdSignalsCount).toEqual(1); + expect(mockLogger.error).toHaveBeenCalled(); + expect(success).toEqual(false); + expect(createdSignalsCount).toEqual(0); expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); - test('if successful iteration of while loop with maxDocs and search after returns empty results with no sort ids', async () => { - const sampleParams = sampleRuleAlertParams(10); - const someGuids = Array.from({ length: 4 }).map((x) => uuid.v4()); - mockService.callCluster - .mockResolvedValueOnce({ - took: 100, - errors: false, - items: [ - { - fakeItemValue: 'fakeItemKey', - }, - { - create: { - status: 201, - }, - }, - ], - }) - .mockResolvedValueOnce(sampleEmptyDocSearchResults()); + test('should return success with 0 total hits', async () => { + const sampleParams = sampleRuleAlertParams(); + mockService.callCluster.mockResolvedValueOnce(sampleEmptyDocSearchResults()); const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ - someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), + listClient: ({ + getListItemByValues: async ({ + value, + }: { + type: string; + listId: string; + value: string[]; + }) => { + return value.map((item) => ({ value: item })); + }, + } as unknown) as ListClient, + exceptionsList: [ + { + field: 'source.ip', + values_operator: 'included', + values_type: 'list', + values: [ + { + id: 'ci-badguys.txt', + name: 'ip', + }, + ], + }, + ], ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -341,13 +381,12 @@ describe('searchAfterAndBulkCreate', () => { throttle: 'no_actions', }); expect(success).toEqual(true); - expect(createdSignalsCount).toEqual(1); - expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); + expect(createdSignalsCount).toEqual(0); + expect(lastLookBackDate).toEqual(null); }); test('if returns false when singleSearchAfter throws an exception', async () => { const sampleParams = sampleRuleAlertParams(10); - const someGuids = Array.from({ length: 4 }).map((x) => uuid.v4()); mockService.callCluster .mockResolvedValueOnce({ took: 100, @@ -367,7 +406,30 @@ describe('searchAfterAndBulkCreate', () => { throw Error('Fake Error'); }); const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ - someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), + listClient: ({ + getListItemByValues: async ({ + value, + }: { + type: string; + listId: string; + value: string[]; + }) => { + return value.map((item) => ({ value: item })); + }, + } as unknown) as ListClient, + exceptionsList: [ + { + field: 'source.ip', + values_operator: 'included', + values_type: 'list', + values: [ + { + id: 'ci-badguys.txt', + name: 'ip', + }, + ], + }, + ], ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -389,7 +451,7 @@ describe('searchAfterAndBulkCreate', () => { throttle: 'no_actions', }); expect(success).toEqual(false); - expect(createdSignalsCount).toEqual(1); - expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); + expect(createdSignalsCount).toEqual(0); // should not create signals if search threw error + expect(lastLookBackDate).toEqual(null); }); }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts index acf3e9bfb055c..05cdccedbc2c1 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -4,18 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ListClient } from '../../../../../lists/server'; import { AlertServices } from '../../../../../alerting/server'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; -import { RuleTypeParams, RefreshTypes } from '../types'; +import { RuleTypeParams, RefreshTypes, RuleAlertParams } from '../types'; import { Logger } from '../../../../../../../src/core/server'; import { singleSearchAfter } from './single_search_after'; import { singleBulkCreate } from './single_bulk_create'; import { SignalSearchResponse } from './types'; +import { filterEventsAgainstList } from './filter_events_with_list'; interface SearchAfterAndBulkCreateParams { - someResult: SignalSearchResponse; ruleParams: RuleTypeParams; services: AlertServices; + listClient: ListClient | undefined; // TODO: undefined is for temporary development, remove before merged + exceptionsList: RuleAlertParams['exceptions_list']; logger: Logger; id: string; inputIndexPattern: string[]; @@ -45,9 +48,10 @@ export interface SearchAfterAndBulkCreateReturnType { // search_after through documents and re-index using bulk endpoint. export const searchAfterAndBulkCreate = async ({ - someResult, ruleParams, + exceptionsList, services, + listClient, logger, id, inputIndexPattern, @@ -73,71 +77,31 @@ export const searchAfterAndBulkCreate = async ({ lastLookBackDate: null, createdSignalsCount: 0, }; - if (someResult.hits.hits.length === 0) { - toReturn.success = true; - return toReturn; - } - logger.debug('[+] starting bulk insertion'); - const { bulkCreateDuration, createdItemsCount } = await singleBulkCreate({ - someResult, - ruleParams, - services, - logger, - id, - signalsIndex, - actions, - name, - createdAt, - createdBy, - updatedAt, - updatedBy, - interval, - enabled, - refresh, - tags, - throttle, - }); + let sortId; // tells us where to start our next search_after query + let searchResultSize = 0; - if (createdItemsCount > 0) { - toReturn.createdSignalsCount = createdItemsCount; - toReturn.lastLookBackDate = - someResult.hits.hits.length > 0 - ? new Date(someResult.hits.hits[someResult.hits.hits.length - 1]?._source['@timestamp']) - : null; - } + /* + The purpose of `maxResults` is to ensure we do not perform + extra search_after's. This will be reset on each + iteration, although it really only matters for the first + iteration of the loop. + e.g. if maxSignals = 100 but our search result only yields + 27 documents, there is no point in performing another search + since we know there are no more events that match our rule, + and thus, no more signals we could possibly generate. + However, if maxSignals = 500 and our search yields a total + of 3050 results we don't want to make 3050 signals, + we only want 500. So maxResults will help us control how + many times we perform a search_after + */ + let maxResults = ruleParams.maxSignals; - if (bulkCreateDuration) { - toReturn.bulkCreateTimes.push(bulkCreateDuration); - } - const totalHits = - typeof someResult.hits.total === 'number' ? someResult.hits.total : someResult.hits.total.value; - // maxTotalHitsSize represents the total number of docs to - // query for, no matter the size of each individual page of search results. - // If the total number of hits for the overall search result is greater than - // maxSignals, default to requesting a total of maxSignals, otherwise use the - // totalHits in the response from the searchAfter query. - const maxTotalHitsSize = Math.min(totalHits, ruleParams.maxSignals); + // Get - // number of docs in the current search result - let hitsSize = someResult.hits.hits.length; - logger.debug(`first size: ${hitsSize}`); - let sortIds = someResult.hits.hits[0].sort; - if (sortIds == null && totalHits > 0) { - logger.error('sortIds was empty on first search but expected more'); - toReturn.success = false; - return toReturn; - } else if (sortIds == null && totalHits === 0) { - toReturn.success = true; - return toReturn; - } - let sortId; - if (sortIds != null) { - sortId = sortIds[0]; - } - while (hitsSize < maxTotalHitsSize && hitsSize !== 0) { + while (searchResultSize < maxResults) { try { - logger.debug(`sortIds: ${sortIds}`); + logger.debug(`sortIds: ${sortId}`); const { searchResult, searchDuration, @@ -152,25 +116,60 @@ export const searchAfterAndBulkCreate = async ({ pageSize, // maximum number of docs to receive per search result. }); toReturn.searchAfterTimes.push(searchDuration); + toReturn.lastLookBackDate = + searchResult.hits.hits.length > 0 + ? new Date( + searchResult.hits.hits[searchResult.hits.hits.length - 1]?._source['@timestamp'] + ) + : null; + const totalHits = + typeof searchResult.hits.total === 'number' + ? searchResult.hits.total + : searchResult.hits.total.value; + logger.debug(`totalHits: ${totalHits}`); + + // re-calculate maxResults to ensure if our search results + // are less than max signals, we are not attempting to + // create more signals than there are total search results. + maxResults = Math.min(totalHits, ruleParams.maxSignals); + searchResultSize += searchResult.hits.hits.length; if (searchResult.hits.hits.length === 0) { toReturn.success = true; return toReturn; } - hitsSize += searchResult.hits.hits.length; - logger.debug(`size adjusted: ${hitsSize}`); - sortIds = searchResult.hits.hits[0].sort; - if (sortIds == null) { - logger.debug('sortIds was empty on search'); + + // filter out the search results that match with the values found in the list. + // the resulting set are valid signals that are not on the allowlist. + const filteredEvents = + listClient != null + ? await filterEventsAgainstList({ + listClient, + exceptionsList, + logger, + eventSearchResult: searchResult, + }) + : searchResult; + + if (filteredEvents.hits.hits.length === 0) { + // everything in the events were allowed, so no need to generate signals toReturn.success = true; - return toReturn; // no more search results + return toReturn; + } + + // cap max signals created to be no more than maxSignals + if (toReturn.createdSignalsCount + filteredEvents.hits.hits.length > ruleParams.maxSignals) { + const tempSignalsToIndex = filteredEvents.hits.hits.slice( + 0, + ruleParams.maxSignals - toReturn.createdSignalsCount + ); + filteredEvents.hits.hits = tempSignalsToIndex; } - sortId = sortIds[0]; logger.debug('next bulk index'); const { bulkCreateDuration: bulkDuration, createdItemsCount: createdCount, } = await singleBulkCreate({ - someResult: searchResult, + filteredEvents, ruleParams, services, logger, @@ -189,17 +188,25 @@ export const searchAfterAndBulkCreate = async ({ throttle, }); logger.debug('finished next bulk index'); + logger.debug(`created ${createdCount} signals`); toReturn.createdSignalsCount += createdCount; if (bulkDuration) { toReturn.bulkCreateTimes.push(bulkDuration); } + + if (filteredEvents.hits.hits[0].sort == null) { + logger.debug('sortIds was empty on search'); + toReturn.success = true; + return toReturn; // no more search results + } + sortId = filteredEvents.hits.hits[0].sort[0]; } catch (exc) { logger.error(`[-] search_after and bulk threw an error ${exc}`); toReturn.success = false; return toReturn; } } - logger.debug(`[+] completed bulk index of ${maxTotalHitsSize}`); + logger.debug(`[+] completed bulk index of ${toReturn.createdSignalsCount}`); toReturn.success = true; return toReturn; }; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index 0c7f0839f8daf..ea7255b8a925a 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -17,6 +17,7 @@ import { scheduleNotificationActions } from '../notifications/schedule_notificat import { RuleAlertType } from '../rules/types'; import { findMlSignals } from './find_ml_signals'; import { bulkCreateMlSignals } from './bulk_create_ml_signals'; +import { ListPluginSetup } from '../../../../../lists/server/types'; jest.mock('./rule_status_saved_objects_client'); jest.mock('./rule_status_service'); @@ -68,6 +69,11 @@ describe('rules_notification_alert_type', () => { modulesProvider: jest.fn(), resultsServiceProvider: jest.fn(), }; + const listMock = { + getListClient: () => ({ + getListItemByValues: () => [], + }), + }; let payload: jest.Mocked; let alert: ReturnType; let logger: ReturnType; @@ -110,6 +116,7 @@ describe('rules_notification_alert_type', () => { logger, version, ml: mlMock, + lists: (listMock as unknown) as ListPluginSetup, }); }); @@ -199,6 +206,7 @@ describe('rules_notification_alert_type', () => { logger, version, ml: undefined, + lists: undefined, }); await alert.executor(payload); expect(logger.error).toHaveBeenCalled(); @@ -358,7 +366,7 @@ describe('rules_notification_alert_type', () => { }); it('when error was thrown', async () => { - (searchAfterAndBulkCreate as jest.Mock).mockResolvedValue({}); + (searchAfterAndBulkCreate as jest.Mock).mockRejectedValue({}); await alert.executor(payload); expect(logger.error).toHaveBeenCalled(); expect(logger.error.mock.calls[0][0]).toContain('An error occurred during rule execution'); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 24cb9102915f8..6885b4c814679 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -4,14 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { performance } from 'perf_hooks'; +/* eslint-disable complexity */ + import { Logger, KibanaRequest } from 'src/core/server'; import { SIGNALS_ID, DEFAULT_SEARCH_AFTER_PAGE_SIZE } from '../../../../common/constants'; import { isJobStarted, isMlRule } from '../../../../common/machine_learning/helpers'; import { SetupPlugins } from '../../../plugin'; -import { buildEventsSearchQuery } from './build_events_query'; +import { ListClient } from '../../../../../lists/server'; + import { getInputIndex } from './get_input_output_index'; import { searchAfterAndBulkCreate, @@ -19,7 +21,7 @@ import { } from './search_after_bulk_create'; import { getFilter } from './get_filter'; import { SignalRuleAlertTypeDefinition, RuleAlertAttributes } from './types'; -import { getGapBetweenRuns, makeFloatString, parseScheduleDates } from './utils'; +import { getGapBetweenRuns, parseScheduleDates } from './utils'; import { signalParamsSchema } from './signal_params_schema'; import { siemRuleActionGroups } from './siem_rule_action_groups'; import { findMlSignals } from './find_ml_signals'; @@ -32,15 +34,18 @@ import { ruleStatusServiceFactory } from './rule_status_service'; import { buildRuleMessageFactory } from './rule_messages'; import { ruleStatusSavedObjectsClientFactory } from './rule_status_saved_objects_client'; import { getNotificationResultsLink } from '../notifications/utils'; +import { hasListsFeature } from '../feature_flags'; export const signalRulesAlertType = ({ logger, version, ml, + lists, }: { logger: Logger; version: string; ml: SetupPlugins['ml']; + lists: SetupPlugins['lists'] | undefined; }): SignalRuleAlertTypeDefinition => { return { id: SIGNALS_ID, @@ -51,7 +56,14 @@ export const signalRulesAlertType = ({ params: signalParamsSchema(), }, producer: 'siem', - async executor({ previousStartedAt, alertId, services, params }) { + async executor({ + previousStartedAt, + alertId, + services, + params, + spaceId, + updatedBy: updatedByUser, + }) { const { anomalyThreshold, from, @@ -67,7 +79,7 @@ export const signalRulesAlertType = ({ query, to, type, - exceptions_list, + exceptions_list: exceptionsList, } = params; const searchAfterSize = Math.min(maxSignals, DEFAULT_SEARCH_AFTER_PAGE_SIZE); let hasError: boolean = false; @@ -123,7 +135,6 @@ export const signalRulesAlertType = ({ hasError = true; await ruleStatusService.error(gapMessage, { gap: gapString }); } - try { if (isMlRule(type)) { if (ml == null) { @@ -161,7 +172,7 @@ export const signalRulesAlertType = ({ ml, callCluster: scopedMlCallCluster, // This is needed to satisfy the ML Services API, but can be empty as it is - // currently unused by the mlSearch function. + // currently unused by the mlAnomalySearch function. request: ({} as unknown) as KibanaRequest, jobId: machineLearningJobId, anomalyThreshold, @@ -199,6 +210,18 @@ export const signalRulesAlertType = ({ result.bulkCreateTimes.push(bulkCreateDuration); } } else { + let listClient: ListClient | undefined; + if (hasListsFeature()) { + if (lists == null) { + throw new Error('lists plugin unavailable during rule execution'); + } + listClient = await lists.getListClient( + services.callCluster, + spaceId, + updatedByUser ?? 'elastic' + ); + } + const inputIndex = await getInputIndex(services, version, index); const esFilter = await getFilter({ type, @@ -208,34 +231,13 @@ export const signalRulesAlertType = ({ savedId, services, index: inputIndex, - lists: exceptions_list, + // temporary filter out list type + lists: exceptionsList?.filter((item) => item.values_type !== 'list'), }); - const noReIndex = buildEventsSearchQuery({ - index: inputIndex, - from, - to, - filter: esFilter, - size: searchAfterSize, - searchAfterSortId: undefined, - }); - - logger.debug(buildRuleMessage('[+] Initial search call')); - const start = performance.now(); - const noReIndexResult = await services.callCluster('search', noReIndex); - const end = performance.now(); - - const signalCount = noReIndexResult.hits.total.value; - if (signalCount !== 0) { - logger.info( - buildRuleMessage( - `Found ${signalCount} signals from the indexes of "[${inputIndex.join(', ')}]"` - ) - ); - } - result = await searchAfterAndBulkCreate({ - someResult: noReIndexResult, + listClient, + exceptionsList, ruleParams: params, services, logger, @@ -256,7 +258,6 @@ export const signalRulesAlertType = ({ tags, throttle, }); - result.searchAfterTimes.push(makeFloatString(end - start)); } if (result.success) { @@ -293,6 +294,11 @@ export const signalRulesAlertType = ({ } logger.debug(buildRuleMessage('[+] Signal Rule execution completed.')); + logger.debug( + buildRuleMessage( + `[+] Finished indexing ${result.createdSignalsCount} signals into ${outputIndex}` + ) + ); if (!hasError) { await ruleStatusService.success('succeeded', { bulkCreateTimeDurations: result.bulkCreateTimes, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts index 6f3cc6e708fce..265f986533134 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts @@ -141,7 +141,7 @@ describe('singleBulkCreate', () => { ], }); const { success, createdItemsCount } = await singleBulkCreate({ - someResult: sampleDocSearchResultsNoSortId(), + filteredEvents: sampleDocSearchResultsNoSortId(), ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -175,7 +175,7 @@ describe('singleBulkCreate', () => { ], }); const { success, createdItemsCount } = await singleBulkCreate({ - someResult: sampleDocSearchResultsNoSortIdNoVersion(), + filteredEvents: sampleDocSearchResultsNoSortIdNoVersion(), ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -201,7 +201,7 @@ describe('singleBulkCreate', () => { const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockResolvedValue(false); const { success, createdItemsCount } = await singleBulkCreate({ - someResult: sampleEmptyDocSearchResults(), + filteredEvents: sampleEmptyDocSearchResults(), ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -228,7 +228,7 @@ describe('singleBulkCreate', () => { const sampleSearchResult = sampleDocSearchResultsNoSortId; mockService.callCluster.mockResolvedValue(sampleBulkCreateDuplicateResult); const { success, createdItemsCount } = await singleBulkCreate({ - someResult: sampleSearchResult(), + filteredEvents: sampleSearchResult(), ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -257,7 +257,7 @@ describe('singleBulkCreate', () => { const sampleSearchResult = sampleDocSearchResultsNoSortId; mockService.callCluster.mockResolvedValue(sampleBulkCreateErrorResult); const { success, createdItemsCount } = await singleBulkCreate({ - someResult: sampleSearchResult(), + filteredEvents: sampleSearchResult(), ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -352,7 +352,7 @@ describe('singleBulkCreate', () => { const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockResolvedValue(sampleBulkCreateDuplicateResult); const { success, createdItemsCount } = await singleBulkCreate({ - someResult: sampleDocSearchResultsNoSortId(), + filteredEvents: sampleDocSearchResultsNoSortId(), ruleParams: sampleParams, services: mockService, logger: mockLogger, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts index c162c8855b091..39aecde470e0b 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts @@ -15,7 +15,7 @@ import { buildBulkBody } from './build_bulk_body'; import { Logger } from '../../../../../../../src/core/server'; interface SingleBulkCreateParams { - someResult: SignalSearchResponse; + filteredEvents: SignalSearchResponse; ruleParams: RuleTypeParams; services: AlertServices; logger: Logger; @@ -64,7 +64,7 @@ export interface SingleBulkCreateResponse { // Bulk Index documents. export const singleBulkCreate = async ({ - someResult, + filteredEvents, ruleParams, services, logger, @@ -82,8 +82,8 @@ export const singleBulkCreate = async ({ tags, throttle, }: SingleBulkCreateParams): Promise => { - someResult.hits.hits = filterDuplicateRules(id, someResult); - if (someResult.hits.hits.length === 0) { + filteredEvents.hits.hits = filterDuplicateRules(id, filteredEvents); + if (filteredEvents.hits.hits.length === 0) { return { success: true, createdItemsCount: 0 }; } // index documents after creating an ID based on the @@ -95,7 +95,7 @@ export const singleBulkCreate = async ({ // while preventing duplicates from being added to the // signals index if rules are re-run over the same time // span. Also allow for versioning. - const bulkBody = someResult.hits.hits.flatMap((doc) => [ + const bulkBody = filteredEvents.hits.hits.flatMap((doc) => [ { create: { _index: signalsIndex, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/single_search_after.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_search_after.test.ts index 580080966457e..2aa42234460d8 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/single_search_after.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_search_after.test.ts @@ -22,18 +22,17 @@ describe('singleSearchAfter', () => { test('if singleSearchAfter works without a given sort id', async () => { let searchAfterSortId; mockService.callCluster.mockResolvedValue(sampleDocSearchResultsNoSortId); - await expect( - singleSearchAfter({ - searchAfterSortId, - index: [], - from: 'now-360s', - to: 'now', - services: mockService, - logger: mockLogger, - pageSize: 1, - filter: undefined, - }) - ).rejects.toThrow('Attempted to search after with empty sort id'); + const { searchResult } = await singleSearchAfter({ + searchAfterSortId, + index: [], + from: 'now-360s', + to: 'now', + services: mockService, + logger: mockLogger, + pageSize: 1, + filter: undefined, + }); + expect(searchResult).toEqual(sampleDocSearchResultsNoSortId); }); test('if singleSearchAfter works with a given sort id', async () => { const searchAfterSortId = '1234567891111'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/single_search_after.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_search_after.ts index 8071c18713c19..a7086a4fb229e 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/single_search_after.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_search_after.ts @@ -36,9 +36,6 @@ export const singleSearchAfter = async ({ searchResult: SignalSearchResponse; searchDuration: string; }> => { - if (searchAfterSortId == null) { - throw Error('Attempted to search after with empty sort id'); - } try { const searchAfterQuery = buildEventsSearchQuery({ index, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/types.ts index b493bab8b4610..32b13c5251a6b 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/types.ts @@ -100,6 +100,7 @@ export interface GetResponse { _source: SearchTypes; } +export type EventSearchResponse = SearchResponse; export type SignalSearchResponse = SearchResponse; export type SignalSourceHit = SignalSearchResponse['hits']['hits'][number]; diff --git a/x-pack/plugins/siem/server/lib/machine_learning/authz.test.ts b/x-pack/plugins/siem/server/lib/machine_learning/authz.test.ts index 93c3a74c71378..93aa6fca87607 100644 --- a/x-pack/plugins/siem/server/lib/machine_learning/authz.test.ts +++ b/x-pack/plugins/siem/server/lib/machine_learning/authz.test.ts @@ -173,7 +173,7 @@ describe('mlAuthz', () => { const mockMlCapabilities = jest.fn(); mlMock.mlSystemProvider.mockImplementation(() => ({ mlInfo: jest.fn(), - mlSearch: jest.fn(), + mlAnomalySearch: jest.fn(), mlCapabilities: mockMlCapabilities, })); @@ -194,7 +194,7 @@ describe('mlAuthz', () => { const mockMlCapabilities = jest.fn(); mlMock.mlSystemProvider.mockImplementation(() => ({ mlInfo: jest.fn(), - mlSearch: jest.fn(), + mlAnomalySearch: jest.fn(), mlCapabilities: mockMlCapabilities, })); diff --git a/x-pack/plugins/siem/server/lib/machine_learning/index.test.ts b/x-pack/plugins/siem/server/lib/machine_learning/index.test.ts index 35a080f5ade76..63e3f3487e482 100644 --- a/x-pack/plugins/siem/server/lib/machine_learning/index.test.ts +++ b/x-pack/plugins/siem/server/lib/machine_learning/index.test.ts @@ -26,17 +26,17 @@ describe('getAnomalies', () => { }; }); - it('calls the provided mlSearch function', () => { - const mockMlSearch = jest.fn(); - getAnomalies(searchParams, mockMlSearch); + it('calls the provided mlAnomalySearch function', () => { + const mockMlAnomalySearch = jest.fn(); + getAnomalies(searchParams, mockMlAnomalySearch); - expect(mockMlSearch).toHaveBeenCalled(); + expect(mockMlAnomalySearch).toHaveBeenCalled(); }); it('passes anomalyThreshold as part of the query', () => { - const mockMlSearch = jest.fn(); - getAnomalies(searchParams, mockMlSearch); - const filters = getFiltersFromMock(mockMlSearch); + const mockMlAnomalySearch = jest.fn(); + getAnomalies(searchParams, mockMlAnomalySearch); + const filters = getFiltersFromMock(mockMlAnomalySearch); const criteria = getBoolCriteriaFromFilters(filters); expect(criteria).toEqual( @@ -45,9 +45,9 @@ describe('getAnomalies', () => { }); it('passes time range as part of the query', () => { - const mockMlSearch = jest.fn(); - getAnomalies(searchParams, mockMlSearch); - const filters = getFiltersFromMock(mockMlSearch); + const mockMlAnomalySearch = jest.fn(); + getAnomalies(searchParams, mockMlAnomalySearch); + const filters = getFiltersFromMock(mockMlAnomalySearch); const criteria = getBoolCriteriaFromFilters(filters); expect(criteria).toEqual( @@ -66,9 +66,9 @@ describe('getAnomalies', () => { }); it('passes a single jobId as part of the query', () => { - const mockMlSearch = jest.fn(); - getAnomalies(searchParams, mockMlSearch); - const filters = getFiltersFromMock(mockMlSearch); + const mockMlAnomalySearch = jest.fn(); + getAnomalies(searchParams, mockMlAnomalySearch); + const filters = getFiltersFromMock(mockMlAnomalySearch); const criteria = getBoolCriteriaFromFilters(filters); expect(criteria).toEqual( @@ -84,10 +84,10 @@ describe('getAnomalies', () => { }); it('passes multiple jobIds as part of the query', () => { - const mockMlSearch = jest.fn(); + const mockMlAnomalySearch = jest.fn(); searchParams.jobIds = ['jobId1', 'jobId2']; - getAnomalies(searchParams, mockMlSearch); - const filters = getFiltersFromMock(mockMlSearch); + getAnomalies(searchParams, mockMlAnomalySearch); + const filters = getFiltersFromMock(mockMlAnomalySearch); const criteria = getBoolCriteriaFromFilters(filters); expect(criteria).toEqual( diff --git a/x-pack/plugins/siem/server/lib/machine_learning/index.ts b/x-pack/plugins/siem/server/lib/machine_learning/index.ts index 5ff164a3f778c..ad2f1e5a8285c 100644 --- a/x-pack/plugins/siem/server/lib/machine_learning/index.ts +++ b/x-pack/plugins/siem/server/lib/machine_learning/index.ts @@ -10,7 +10,7 @@ import { AnomalyRecordDoc as Anomaly } from '../../../../ml/server'; export { Anomaly }; export type AnomalyResults = SearchResponse; -type MlSearch = (searchParams: SearchParams) => Promise>; +type MlAnomalySearch = (searchParams: SearchParams) => Promise>; export interface AnomaliesSearchParams { jobIds: string[]; @@ -22,11 +22,11 @@ export interface AnomaliesSearchParams { export const getAnomalies = async ( params: AnomaliesSearchParams, - mlSearch: MlSearch + mlAnomalySearch: MlAnomalySearch ): Promise => { const boolCriteria = buildCriteria(params); - return mlSearch({ + return mlAnomalySearch({ size: params.maxRecords || 100, body: { query: { diff --git a/x-pack/plugins/siem/server/plugin.ts b/x-pack/plugins/siem/server/plugin.ts index 3c336991f3d9d..5a47efd458888 100644 --- a/x-pack/plugins/siem/server/plugin.ts +++ b/x-pack/plugins/siem/server/plugin.ts @@ -19,6 +19,7 @@ import { PluginSetupContract as AlertingSetup } from '../../alerting/server'; import { SecurityPluginSetup as SecuritySetup } from '../../security/server'; import { PluginSetupContract as FeaturesSetup } from '../../features/server'; import { MlPluginSetup as MlSetup } from '../../ml/server'; +import { ListPluginSetup } from '../../lists/server'; import { EncryptedSavedObjectsPluginSetup as EncryptedSavedObjectsSetup } from '../../encrypted_saved_objects/server'; import { SpacesPluginSetup as SpacesSetup } from '../../spaces/server'; import { LicensingPluginSetup } from '../../licensing/server'; @@ -52,6 +53,7 @@ export interface SetupPlugins { security?: SecuritySetup; spaces?: SpacesSetup; ml?: MlSetup; + lists?: ListPluginSetup; } export interface StartPlugins { @@ -194,6 +196,7 @@ export class Plugin implements IPlugin { http: (http as unknown) as CoreSetup['http'], getStartServices: async () => [coreStart, {}, {}], authorization: securityMock.createSetup().authz, - getSpacesAuditLogger: () => ({} as SpacesAuditLogger), + auditLogger: {} as SpacesAuditLogger, config$: Rx.of(spacesConfig), }); diff --git a/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts b/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts index 1a32e861b22e1..0abf545fa7493 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts @@ -41,7 +41,7 @@ describe('createSpacesTutorialContextFactory', () => { http: coreMock.createSetup().http, getStartServices: async () => [coreMock.createStart(), {}, {}], authorization: securityMock.createSetup().authz, - getSpacesAuditLogger: () => ({} as SpacesAuditLogger), + auditLogger: {} as SpacesAuditLogger, config$: Rx.of(spacesConfig), }); const contextFactory = createSpacesTutorialContextFactory(spacesService); diff --git a/x-pack/plugins/spaces/server/plugin.test.ts b/x-pack/plugins/spaces/server/plugin.test.ts index 7126f96f4f829..a82f2370cc124 100644 --- a/x-pack/plugins/spaces/server/plugin.test.ts +++ b/x-pack/plugins/spaces/server/plugin.test.ts @@ -23,9 +23,6 @@ describe('Spaces Plugin', () => { const spacesSetup = await plugin.setup(core, { features, licensing }); expect(spacesSetup).toMatchInlineSnapshot(` Object { - "__legacyCompat": Object { - "registerLegacyAPI": [Function], - }, "spacesService": Object { "getActiveSpace": [Function], "getBasePath": [Function], diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index 36809bf0e9e7a..af54effcaeca6 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -14,8 +14,6 @@ import { } from '../../features/server'; import { SecurityPluginSetup } from '../../security/server'; import { LicensingPluginSetup } from '../../licensing/server'; -// @ts-ignore -import { AuditLogger } from '../../../../server/lib/audit_logger'; import { SpacesAuditLogger } from './lib/audit_logger'; import { createSpacesTutorialContextFactory } from './lib/spaces_tutorial_context_factory'; import { registerSpacesUsageCollector } from './usage_collection'; @@ -31,16 +29,6 @@ import { SpacesSavedObjectsService } from './saved_objects'; import { DefaultSpaceService } from './default_space'; import { SpacesLicenseService } from '../common/licensing'; -/** - * Describes a set of APIs that is available in the legacy platform only and required by this plugin - * to function properly. - */ -export interface LegacyAPI { - auditLogger: { - create: (pluginId: string) => AuditLogger; - }; -} - export interface PluginsSetup { features: FeaturesPluginSetup; licensing: LicensingPluginSetup; @@ -55,9 +43,6 @@ export interface PluginsStart { export interface SpacesPluginSetup { spacesService: SpacesServiceSetup; - __legacyCompat: { - registerLegacyAPI: (legacyAPI: LegacyAPI) => void; - }; } export class Plugin { @@ -73,24 +58,6 @@ export class Plugin { private defaultSpaceService?: DefaultSpaceService; - private legacyAPI?: LegacyAPI; - private readonly getLegacyAPI = () => { - if (!this.legacyAPI) { - throw new Error('Legacy API is not registered!'); - } - return this.legacyAPI; - }; - - private spacesAuditLogger?: SpacesAuditLogger; - private readonly getSpacesAuditLogger = () => { - if (!this.spacesAuditLogger) { - this.spacesAuditLogger = new SpacesAuditLogger( - this.getLegacyAPI().auditLogger.create(this.pluginId) - ); - } - return this.spacesAuditLogger; - }; - constructor(initializerContext: PluginInitializerContext) { this.config$ = initializerContext.config.create(); this.kibanaIndexConfig$ = initializerContext.config.legacy.globalConfig$; @@ -109,7 +76,7 @@ export class Plugin { http: core.http, getStartServices: core.getStartServices, authorization: plugins.security ? plugins.security.authz : null, - getSpacesAuditLogger: this.getSpacesAuditLogger, + auditLogger: new SpacesAuditLogger(plugins.security?.audit.getLogger(this.pluginId)), config$: this.config$, }); @@ -177,11 +144,6 @@ export class Plugin { return { spacesService, - __legacyCompat: { - registerLegacyAPI: (legacyAPI: LegacyAPI) => { - this.legacyAPI = legacyAPI; - }, - }, }; } diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts index 632e64156291c..09fc990e9935c 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts @@ -78,7 +78,7 @@ describe('copy to space', () => { http: (httpService as unknown) as CoreSetup['http'], getStartServices: async () => [coreStart, {}, {}], authorization: securityMock.createSetup().authz, - getSpacesAuditLogger: () => ({} as SpacesAuditLogger), + auditLogger: {} as SpacesAuditLogger, config$: Rx.of(spacesConfig), }); diff --git a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts index 511e9676940d2..774b794d77e29 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts @@ -50,7 +50,7 @@ describe('Spaces Public API', () => { http: (httpService as unknown) as CoreSetup['http'], getStartServices: async () => [coreStart, {}, {}], authorization: securityMock.createSetup().authz, - getSpacesAuditLogger: () => ({} as SpacesAuditLogger), + auditLogger: {} as SpacesAuditLogger, config$: Rx.of(spacesConfig), }); diff --git a/x-pack/plugins/spaces/server/routes/api/external/get.test.ts b/x-pack/plugins/spaces/server/routes/api/external/get.test.ts index 3eb9b676bcc61..19f9b81baa0b0 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get.test.ts @@ -43,7 +43,7 @@ describe('GET space', () => { http: (httpService as unknown) as CoreSetup['http'], getStartServices: async () => [coreStart, {}, {}], authorization: securityMock.createSetup().authz, - getSpacesAuditLogger: () => ({} as SpacesAuditLogger), + auditLogger: {} as SpacesAuditLogger, config$: Rx.of(spacesConfig), }); diff --git a/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts b/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts index 5847b3f84f41d..380cc9dbe5abf 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts @@ -43,7 +43,7 @@ describe('GET /spaces/space', () => { http: (httpService as unknown) as CoreSetup['http'], getStartServices: async () => [coreStart, {}, {}], authorization: securityMock.createSetup().authz, - getSpacesAuditLogger: () => ({} as SpacesAuditLogger), + auditLogger: {} as SpacesAuditLogger, config$: Rx.of(spacesConfig), }); diff --git a/x-pack/plugins/spaces/server/routes/api/external/post.test.ts b/x-pack/plugins/spaces/server/routes/api/external/post.test.ts index 51fcfbfeaa95d..ca3afc04b9798 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/post.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/post.test.ts @@ -43,7 +43,7 @@ describe('Spaces Public API', () => { http: (httpService as unknown) as CoreSetup['http'], getStartServices: async () => [coreStart, {}, {}], authorization: securityMock.createSetup().authz, - getSpacesAuditLogger: () => ({} as SpacesAuditLogger), + auditLogger: {} as SpacesAuditLogger, config$: Rx.of(spacesConfig), }); diff --git a/x-pack/plugins/spaces/server/routes/api/external/put.test.ts b/x-pack/plugins/spaces/server/routes/api/external/put.test.ts index 3575d89b151e8..62444fd3e4dfd 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/put.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/put.test.ts @@ -44,7 +44,7 @@ describe('PUT /api/spaces/space', () => { http: (httpService as unknown) as CoreSetup['http'], getStartServices: async () => [coreStart, {}, {}], authorization: securityMock.createSetup().authz, - getSpacesAuditLogger: () => ({} as SpacesAuditLogger), + auditLogger: {} as SpacesAuditLogger, config$: Rx.of(spacesConfig), }); diff --git a/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.test.ts b/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.test.ts index 82de102e119c7..086d5f5bc94bb 100644 --- a/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.test.ts @@ -24,7 +24,7 @@ describe('GET /internal/spaces/_active_space', () => { http: (httpService as unknown) as CoreSetup['http'], getStartServices: async () => [coreStart, {}, {}], authorization: null, - getSpacesAuditLogger: () => ({} as SpacesAuditLogger), + auditLogger: {} as SpacesAuditLogger, config$: Rx.of(spacesConfig), }); diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts index 3ea1da1c835b2..3e1a849a9bdfa 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts @@ -71,7 +71,7 @@ const createService = async (serverBasePath: string = '') => { getStartServices: async () => [coreStart, {}, {}], config$: Rx.of(spacesConfig), authorization: securityMock.createSetup().authz, - getSpacesAuditLogger: () => new SpacesAuditLogger({}), + auditLogger: new SpacesAuditLogger(), }); return spacesServiceSetup; diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts index c2cc26d85fcfb..759b0606a5e8b 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts @@ -15,6 +15,7 @@ import { getSpaceIdFromPath, addSpaceIdToPath } from '../../common/lib/spaces_ur import { DEFAULT_SPACE_ID } from '../../common/constants'; import { spaceIdToNamespace, namespaceToSpaceId } from '../lib/utils/namespace'; import { Space } from '../../common/model/space'; +import { SpacesAuditLogger } from '../lib/audit_logger'; type RequestFacade = KibanaRequest | Legacy.Request; @@ -39,7 +40,7 @@ interface SpacesServiceDeps { getStartServices: CoreSetup['getStartServices']; authorization: SecurityPluginSetup['authz'] | null; config$: Observable; - getSpacesAuditLogger(): any; + auditLogger: SpacesAuditLogger; } export class SpacesService { @@ -52,7 +53,7 @@ export class SpacesService { getStartServices, authorization, config$, - getSpacesAuditLogger, + auditLogger, }: SpacesServiceDeps): Promise { const getSpaceId = (request: RequestFacade) => { // Currently utilized by reporting @@ -81,7 +82,7 @@ export class SpacesService { ); return new SpacesClient( - getSpacesAuditLogger(), + auditLogger, (message: string) => { this.log.debug(message); }, diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index 3a4453e7ae40a..403179ec4ac28 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -23,10 +23,13 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await ml.securityCommon.cleanMlRoles(); await ml.testResources.deleteIndexPattern('kibana_sample_data_logs'); + await ml.testResources.deleteIndexPattern('ft_farequote'); await esArchiver.unload('ml/ecommerce'); await esArchiver.unload('ml/categorization'); await esArchiver.unload('ml/sample_logs'); + await esArchiver.unload('ml/farequote'); + await esArchiver.unload('ml/bm_classification'); await ml.testResources.resetKibanaTimeZone(); }); diff --git a/x-pack/test/detection_engine_api_integration/common/config.ts b/x-pack/test/detection_engine_api_integration/common/config.ts index d59ab4a3adc4b..0e3f3d94ed675 100644 --- a/x-pack/test/detection_engine_api_integration/common/config.ts +++ b/x-pack/test/detection_engine_api_integration/common/config.ts @@ -79,6 +79,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ])}`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, '--xpack.eventLog.logEntries=true', + '--xpack.lists.enabled=true', ...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`), `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`, `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'actions')}`, diff --git a/x-pack/test/functional/services/ml/anomalies_table.ts b/x-pack/test/functional/services/ml/anomalies_table.ts index c8701099dcd7a..26af97d008feb 100644 --- a/x-pack/test/functional/services/ml/anomalies_table.ts +++ b/x-pack/test/functional/services/ml/anomalies_table.ts @@ -19,7 +19,7 @@ export function MachineLearningAnomaliesTableProvider({ getService }: FtrProvide const tableRows = await testSubjects.findAll('mlAnomaliesTable > ~mlAnomaliesListRow'); expect(tableRows.length).to.be.greaterThan( 0, - 'Anomalies table should have at least one row (got 0)' + `Anomalies table should have at least one row (got '${tableRows.length}')` ); }, }; diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index 3f3f6cdde1724..897f37821001e 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -140,7 +140,10 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { async getJobState(jobId: string): Promise { const jobStats = await this.getADJobStats(jobId); - expect(jobStats.jobs).to.have.length(1); + expect(jobStats.jobs).to.have.length( + 1, + `Expected job stats to have exactly one job (got '${jobStats.length}')` + ); const state: JOB_STATE = jobStats.jobs[0].state; return state; @@ -178,7 +181,10 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { .expect(200) .then((res: any) => res.body); - expect(datafeedStats.datafeeds).to.have.length(1); + expect(datafeedStats.datafeeds).to.have.length( + 1, + `Expected datafeed stats to have exactly one datafeed (got '${datafeedStats.datafeeds.length}')` + ); const state: DATAFEED_STATE = datafeedStats.datafeeds[0].state; return state; @@ -206,7 +212,10 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { .expect(200) .then((res: any) => res.body); - expect(analyticsStats.data_frame_analytics).to.have.length(1); + expect(analyticsStats.data_frame_analytics).to.have.length( + 1, + `Expected dataframe analytics stats to have exactly one object (got '${analyticsStats.data_frame_analytics.length}')` + ); const state: DATA_FRAME_TASK_STATE = analyticsStats.data_frame_analytics[0].state; return state; @@ -414,7 +423,10 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { async getADJobRecordCount(jobId: string): Promise { const jobStats = await this.getADJobStats(jobId); - expect(jobStats.jobs).to.have.length(1); + expect(jobStats.jobs).to.have.length( + 1, + `Expected job stats to have exactly one job (got '${jobStats.jobs.length}')` + ); const processedRecordCount: number = jobStats.jobs[0].data_counts.processed_record_count; return processedRecordCount; diff --git a/x-pack/test/functional/services/ml/custom_urls.ts b/x-pack/test/functional/services/ml/custom_urls.ts index 6842908462018..4acbd23cd3580 100644 --- a/x-pack/test/functional/services/ml/custom_urls.ts +++ b/x-pack/test/functional/services/ml/custom_urls.ts @@ -19,7 +19,10 @@ export function MachineLearningCustomUrlsProvider({ getService }: FtrProviderCon 'mlJobCustomUrlLabelInput', 'value' ); - expect(actualCustomUrlLabel).to.eql(expectedValue); + expect(actualCustomUrlLabel).to.eql( + expectedValue, + `Expected custom url label to be '${expectedValue}' (got '${actualCustomUrlLabel}')` + ); }, async setCustomUrlLabel(customUrlsLabel: string) { @@ -29,11 +32,16 @@ export function MachineLearningCustomUrlsProvider({ getService }: FtrProviderCon await this.assertCustomUrlLabelValue(customUrlsLabel); }, - async assertCustomUrlItem(index: number, label: string) { + async assertCustomUrlItem(index: number, expectedLabel: string) { await testSubjects.existOrFail(`mlJobEditCustomUrlItem_${index}`); - expect( - await testSubjects.getAttribute(`mlJobEditCustomUrlLabelInput_${index}`, 'value') - ).to.eql(label); + const actualLabel = await testSubjects.getAttribute( + `mlJobEditCustomUrlLabelInput_${index}`, + 'value' + ); + expect(actualLabel).to.eql( + expectedLabel, + `Expected custom url item to be '${expectedLabel}' (got '${actualLabel}')` + ); }, /** diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts index d67f6bc946df2..cff7e00eef688 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts @@ -75,7 +75,7 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( )) === 'true'; expect(actualCheckState).to.eql( expectedCheckState, - `Advanced editor switch check state should be ${expectedCheckState} (got ${actualCheckState})` + `Advanced editor switch check state should be '${expectedCheckState}' (got '${actualCheckState}')` ); }, @@ -317,7 +317,7 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( const actualCheckState = await this.getCreateIndexPatternSwitchCheckState(); expect(actualCheckState).to.eql( expectedCheckState, - `Create index pattern switch check state should be ${expectedCheckState} (got ${actualCheckState})` + `Create index pattern switch check state should be '${expectedCheckState}' (got '${actualCheckState}')` ); }, diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_table.ts b/x-pack/test/functional/services/ml/data_frame_analytics_table.ts index 2a621aaf28fe3..d5f4ee63f615b 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_table.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_table.ts @@ -107,7 +107,12 @@ export function MachineLearningDataFrameAnalyticsTableProvider({ getService }: F public async assertAnalyticsRowFields(analyticsId: string, expectedRow: object) { const rows = await this.parseAnalyticsTable(); const analyticsRow = rows.filter((row) => row.id === analyticsId)[0]; - expect(analyticsRow).to.eql(expectedRow); + expect(analyticsRow).to.eql( + expectedRow, + `Expected analytics row to be '${JSON.stringify(expectedRow)}' (got '${JSON.stringify( + analyticsRow + )}')` + ); } public async openRowActions(analyticsId: string) { diff --git a/x-pack/test/functional/services/ml/data_visualizer_index_based.ts b/x-pack/test/functional/services/ml/data_visualizer_index_based.ts index 792dd5f90ca11..7789ca78363df 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_index_based.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_index_based.ts @@ -23,7 +23,10 @@ export function MachineLearningDataVisualizerIndexBasedProvider({ async assertTotalDocumentCount(expectedTotalDocCount: number) { await retry.tryForTime(5000, async () => { const docCount = await testSubjects.getVisibleText('mlDataVisualizerTotalDocCount'); - expect(docCount).to.eql(expectedTotalDocCount); + expect(docCount).to.eql( + expectedTotalDocCount, + `Expected total document count to be '${expectedTotalDocCount}' (got '${docCount}')` + ); }); }, @@ -34,7 +37,10 @@ export function MachineLearningDataVisualizerIndexBasedProvider({ async assertFieldsPanelsExist(expectedPanelCount: number) { const allPanels = await testSubjects.findAll('~mlDataVisualizerFieldsPanel'); - expect(allPanels).to.have.length(expectedPanelCount); + expect(allPanels).to.have.length( + expectedPanelCount, + `Expected field panels count to be '${expectedPanelCount}' (got '${allPanels.length}')` + ); }, async assertFieldsPanelForTypesExist(fieldTypes: ML_JOB_FIELD_TYPES[]) { @@ -50,7 +56,10 @@ export function MachineLearningDataVisualizerIndexBasedProvider({ const filteredCards = await testSubjects.findAll( `mlDataVisualizerFieldsPanel ${panelFieldTypes} > ~mlFieldDataCard` ); - expect(filteredCards).to.have.length(expectedCardCount); + expect(filteredCards).to.have.length( + expectedCardCount, + `Expected field card count for panels '${panelFieldTypes}' to be '${expectedCardCount}' (got '${filteredCards.length}')` + ); }); }, @@ -60,7 +69,10 @@ export function MachineLearningDataVisualizerIndexBasedProvider({ ); const searchBarInput = await searchBar.findByTagName('input'); const actualSearchValue = await searchBarInput.getAttribute('value'); - expect(actualSearchValue).to.eql(expectedSearchValue); + expect(actualSearchValue).to.eql( + expectedSearchValue, + `Expected search value for field types '${fieldTypes}' to be '${expectedSearchValue}' (got '${actualSearchValue}')` + ); }, async filterFieldsPanelWithSearchString( @@ -91,7 +103,10 @@ export function MachineLearningDataVisualizerIndexBasedProvider({ 'mlDataVisualizerFieldTypesSelect', 'value' ); - expect(actualTypeValue).to.eql(expectedTypeValue); + expect(actualTypeValue).to.eql( + expectedTypeValue, + `Expected fields panel type value to be '${expectedTypeValue}' (got '${actualTypeValue}')` + ); }, async setFieldsPanelTypeInputValue( diff --git a/x-pack/test/functional/services/ml/job_table.ts b/x-pack/test/functional/services/ml/job_table.ts index cfb3ed8977716..a72d9c204060b 100644 --- a/x-pack/test/functional/services/ml/job_table.ts +++ b/x-pack/test/functional/services/ml/job_table.ts @@ -163,7 +163,10 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte await this.refreshJobList(); const rows = await this.parseJobTable(); const jobRow = rows.filter((row) => row.id === jobId)[0]; - expect(jobRow).to.eql(expectedRow); + expect(jobRow).to.eql( + expectedRow, + `Expected job row to be '${JSON.stringify(expectedRow)}' (got '${JSON.stringify(jobRow)}')` + ); } public async assertJobRowDetailsCounts( diff --git a/x-pack/test/functional/services/ml/job_wizard_advanced.ts b/x-pack/test/functional/services/ml/job_wizard_advanced.ts index 755091ca10f3b..e4d2ecf66f646 100644 --- a/x-pack/test/functional/services/ml/job_wizard_advanced.ts +++ b/x-pack/test/functional/services/ml/job_wizard_advanced.ts @@ -35,7 +35,10 @@ export function MachineLearningJobWizardAdvancedProvider( const actualValue = await aceEditor.getValue( 'mlAdvancedDatafeedQueryEditor > codeEditorContainer' ); - expect(actualValue).to.eql(expectedValue); + expect(actualValue).to.eql( + expectedValue, + `Expected datafeed query editor value to be '${expectedValue}' (got '${actualValue}')` + ); }, async assertQueryDelayInputExists() { @@ -44,7 +47,10 @@ export function MachineLearningJobWizardAdvancedProvider( async assertQueryDelayValue(expectedValue: string) { const actualQueryDelay = await this.getValueOrPlaceholder('mlJobWizardInputQueryDelay'); - expect(actualQueryDelay).to.eql(expectedValue); + expect(actualQueryDelay).to.eql( + expectedValue, + `Expected query delay value to be '${expectedValue}' (got '${actualQueryDelay}')` + ); }, async setQueryDelay(queryDelay: string) { @@ -61,7 +67,10 @@ export function MachineLearningJobWizardAdvancedProvider( async assertFrequencyValue(expectedValue: string) { const actualFrequency = await this.getValueOrPlaceholder('mlJobWizardInputFrequency'); - expect(actualFrequency).to.eql(expectedValue); + expect(actualFrequency).to.eql( + expectedValue, + `Expected frequency value to be '${expectedValue}' (got '${actualFrequency}')` + ); }, async setFrequency(frequency: string) { @@ -78,7 +87,10 @@ export function MachineLearningJobWizardAdvancedProvider( async assertScrollSizeValue(expectedValue: string) { const actualScrollSize = await this.getValueOrPlaceholder('mlJobWizardInputScrollSize'); - expect(actualScrollSize).to.eql(expectedValue); + expect(actualScrollSize).to.eql( + expectedValue, + `Expected scroll size value to be '${expectedValue}' (got '${actualScrollSize}')` + ); }, async setScrollSize(scrollSize: string) { @@ -97,7 +109,10 @@ export function MachineLearningJobWizardAdvancedProvider( const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( 'mlTimeFieldNameSelect > comboBoxInput' ); - expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected time field selection to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); }, async selectTimeField(identifier: string) { @@ -113,7 +128,10 @@ export function MachineLearningJobWizardAdvancedProvider( const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( 'mlCategorizationFieldNameSelect > comboBoxInput' ); - expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected categorization field selection to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); }, async selectCategorizationField(identifier: string) { @@ -129,7 +147,10 @@ export function MachineLearningJobWizardAdvancedProvider( const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( 'mlSummaryCountFieldNameSelect > comboBoxInput' ); - expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected summary count field selection to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); }, async selectSummaryCountField(identifier: string) { @@ -160,7 +181,10 @@ export function MachineLearningJobWizardAdvancedProvider( const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( 'mlAdvancedFunctionSelect > comboBoxInput' ); - expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected detector function selection to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); }, async selectDetectorFunction(identifier: string) { @@ -176,7 +200,10 @@ export function MachineLearningJobWizardAdvancedProvider( const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( 'mlAdvancedFieldSelect > comboBoxInput' ); - expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected detector field selection to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); }, async selectDetectorField(identifier: string) { @@ -192,7 +219,10 @@ export function MachineLearningJobWizardAdvancedProvider( const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( 'mlAdvancedByFieldSelect > comboBoxInput' ); - expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected detector by field selection to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); }, async selectDetectorByField(identifier: string) { @@ -208,7 +238,10 @@ export function MachineLearningJobWizardAdvancedProvider( const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( 'mlAdvancedOverFieldSelect > comboBoxInput' ); - expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected detector over field selection to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); }, async selectDetectorOverField(identifier: string) { @@ -224,7 +257,10 @@ export function MachineLearningJobWizardAdvancedProvider( const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( 'mlAdvancedPartitionFieldSelect > comboBoxInput' ); - expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected detector partition field selection to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); }, async selectDetectorPartitionField(identifier: string) { @@ -240,7 +276,10 @@ export function MachineLearningJobWizardAdvancedProvider( const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( 'mlAdvancedExcludeFrequentSelect > comboBoxInput' ); - expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected detector exclude frequent selection to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); }, async selectDetectorExcludeFrequent(identifier: string) { @@ -257,7 +296,10 @@ export function MachineLearningJobWizardAdvancedProvider( 'mlAdvancedDetectorDescriptionInput', 'value' ); - expect(actualDetectorDescription).to.eql(expectedValue); + expect(actualDetectorDescription).to.eql( + expectedValue, + `Expected detector description value to be '${expectedValue}' (got '${actualDetectorDescription}')` + ); }, async setDetectorDescription(description: string) { @@ -287,13 +329,19 @@ export function MachineLearningJobWizardAdvancedProvider( const actualDetectorIdentifier = await testSubjects.getVisibleText( `mlAdvancedDetector ${detectorIndex} > mlDetectorIdentifier` ); - expect(actualDetectorIdentifier).to.eql(expectedDetectorName); + expect(actualDetectorIdentifier).to.eql( + expectedDetectorName, + `Expected detector name to be '${expectedDetectorName}' (got '${actualDetectorIdentifier}')` + ); if (expectedDetectorDescription !== undefined) { const actualDetectorDescription = await testSubjects.getVisibleText( `mlAdvancedDetector ${detectorIndex} > mlDetectorDescription` ); - expect(actualDetectorDescription).to.eql(expectedDetectorDescription); + expect(actualDetectorDescription).to.eql( + expectedDetectorDescription, + `Expected detector description to be '${expectedDetectorDescription}' (got '${actualDetectorDescription}')` + ); } }, diff --git a/x-pack/test/functional/services/ml/job_wizard_categorization.ts b/x-pack/test/functional/services/ml/job_wizard_categorization.ts index 97d45701a2685..705cc29938dfb 100644 --- a/x-pack/test/functional/services/ml/job_wizard_categorization.ts +++ b/x-pack/test/functional/services/ml/job_wizard_categorization.ts @@ -41,7 +41,7 @@ export function MachineLearningJobWizardCategorizationProvider({ getService }: F ); expect(comboBoxSelectedOptions).to.eql( expectedIdentifier, - `Expected categorization field selection to be '${expectedIdentifier}' (got ${comboBoxSelectedOptions}')` + `Expected categorization field selection to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` ); }, @@ -56,7 +56,7 @@ export function MachineLearningJobWizardCategorizationProvider({ getService }: F const rows = await body[0].findAllByTagName('tr'); expect(rows.length).to.eql( exampleCount, - `Expected categorization field examples table to have '${exampleCount}' rows (got ${rows.length}')` + `Expected categorization field examples table to have '${exampleCount}' rows (got '${rows.length}')` ); }, }; diff --git a/x-pack/test/functional/services/ml/job_wizard_common.ts b/x-pack/test/functional/services/ml/job_wizard_common.ts index af33ec2301edc..2843c36e08a1d 100644 --- a/x-pack/test/functional/services/ml/job_wizard_common.ts +++ b/x-pack/test/functional/services/ml/job_wizard_common.ts @@ -101,7 +101,10 @@ export function MachineLearningJobWizardCommonProvider( const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( 'mlJobWizardAggSelection > comboBoxInput' ); - expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected agg and field selection to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); }, async selectAggAndField(identifier: string, isIdentifierKeptInField: boolean) { @@ -118,7 +121,10 @@ export function MachineLearningJobWizardCommonProvider( 'mlJobWizardInputBucketSpan', 'value' ); - expect(actualBucketSpan).to.eql(expectedValue); + expect(actualBucketSpan).to.eql( + expectedValue, + `Expected bucket span value to be '${expectedValue}' (got '${actualBucketSpan}')` + ); }, async setBucketSpan(bucketSpan: string) { @@ -135,7 +141,10 @@ export function MachineLearningJobWizardCommonProvider( async assertJobIdValue(expectedValue: string) { const actualJobId = await testSubjects.getAttribute('mlJobWizardInputJobId', 'value'); - expect(actualJobId).to.eql(expectedValue); + expect(actualJobId).to.eql( + expectedValue, + `Expected job id value to be '${expectedValue}' (got '${actualJobId}')` + ); }, async setJobId(jobId: string) { @@ -153,7 +162,10 @@ export function MachineLearningJobWizardCommonProvider( const actualJobDescription = await testSubjects.getVisibleText( 'mlJobWizardInputJobDescription' ); - expect(actualJobDescription).to.eql(expectedValue); + expect(actualJobDescription).to.eql( + expectedValue, + `Expected job description value to be '${expectedValue}' (got '${actualJobDescription}')` + ); }, async setJobDescription(jobDescription: string) { @@ -174,12 +186,20 @@ export function MachineLearningJobWizardCommonProvider( }, async assertJobGroupSelection(jobGroups: string[]) { - expect(await this.getSelectedJobGroups()).to.eql(jobGroups); + const actualJobGroupSelection = await this.getSelectedJobGroups(); + expect(actualJobGroupSelection).to.eql( + jobGroups, + `Expected job group selection to be '${jobGroups}' (got '${actualJobGroupSelection}')` + ); }, async addJobGroup(jobGroup: string) { await comboBox.setCustom('mlJobWizardComboBoxJobGroups > comboBoxInput', jobGroup); - expect(await this.getSelectedJobGroups()).to.contain(jobGroup); + const actualJobGroupSelection = await this.getSelectedJobGroups(); + expect(actualJobGroupSelection).to.contain( + jobGroup, + `Expected job group selection to contain '${jobGroup}' (got '${actualJobGroupSelection}')` + ); }, async getSelectedCalendars(): Promise { @@ -190,13 +210,21 @@ export function MachineLearningJobWizardCommonProvider( }, async assertCalendarsSelection(calendars: string[]) { - expect(await this.getSelectedCalendars()).to.eql(calendars); + const actualCalendarSelection = await this.getSelectedCalendars(); + expect(actualCalendarSelection).to.eql( + calendars, + `Expected calendar selection to be '${calendars}' (got '${actualCalendarSelection}')` + ); }, async addCalendar(calendarId: string) { await this.ensureAdditionalSettingsSectionOpen(); await comboBox.setCustom('mlJobWizardComboBoxCalendars > comboBoxInput', calendarId); - expect(await this.getSelectedCalendars()).to.contain(calendarId); + const actualCalendarSelection = await this.getSelectedCalendars(); + expect(actualCalendarSelection).to.contain( + calendarId, + `Expected calendar selection to conatin '${calendarId}' (got '${actualCalendarSelection}')` + ); }, async assertModelPlotSwitchExists( @@ -229,14 +257,19 @@ export function MachineLearningJobWizardCommonProvider( const actualCheckedState = await this.getModelPlotSwitchCheckedState({ withAdvancedSection: sectionOptions.withAdvancedSection, }); - expect(actualCheckedState).to.eql(expectedValue); + expect(actualCheckedState).to.eql( + expectedValue, + `Expected model plot switch to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + actualCheckedState ? 'enabled' : 'disabled' + }')` + ); }, async assertModelPlotSwitchEnabled(expectedValue: boolean) { const isEnabled = await testSubjects.isEnabled('mlJobWizardSwitchModelPlot'); expect(isEnabled).to.eql( expectedValue, - `Expected model plot switch to be '${expectedValue ? 'enabled' : 'disabled'}' (got ${ + `Expected model plot switch to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ isEnabled ? 'enabled' : 'disabled' }')` ); @@ -272,7 +305,12 @@ export function MachineLearningJobWizardCommonProvider( const actualCheckedState = await this.getDedicatedIndexSwitchCheckedState({ withAdvancedSection: sectionOptions.withAdvancedSection, }); - expect(actualCheckedState).to.eql(expectedValue); + expect(actualCheckedState).to.eql( + expectedValue, + `Expected dedicated index switch to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + actualCheckedState ? 'enabled' : 'disabled' + }')` + ); }, async activateDedicatedIndexSwitch( @@ -318,7 +356,10 @@ export function MachineLearningJobWizardCommonProvider( subj = advancedSectionSelector(subj); } const actualModelMemoryLimit = await testSubjects.getAttribute(subj, 'value'); - expect(actualModelMemoryLimit).to.eql(expectedValue); + expect(actualModelMemoryLimit).to.eql( + expectedValue, + `Expected model memory limit value to be '${expectedValue}' (got '${actualModelMemoryLimit}')` + ); }, async setModelMemoryLimit( @@ -347,12 +388,20 @@ export function MachineLearningJobWizardCommonProvider( }, async assertInfluencerSelection(influencers: string[]) { - expect(await this.getSelectedInfluencers()).to.eql(influencers); + const actualInfluencerSelection = await this.getSelectedInfluencers(); + expect(actualInfluencerSelection).to.eql( + influencers, + `Expected influencer selection to be '${influencers}' (got '${actualInfluencerSelection}')` + ); }, async addInfluencer(influencer: string) { await comboBox.set('mlInfluencerSelect > comboBoxInput', influencer); - expect(await this.getSelectedInfluencers()).to.contain(influencer); + const actualInfluencerSelection = await this.getSelectedInfluencers(); + expect(actualInfluencerSelection).to.contain( + influencer, + `Expected influencer selection to contain '${influencer}' (got '${actualInfluencerSelection}')` + ); }, async assertAnomalyChartExists(chartType: string, preSelector?: string) { @@ -367,9 +416,13 @@ export function MachineLearningJobWizardCommonProvider( ) { await testSubjects.existOrFail(`mlDetector ${detectorPosition}`); await testSubjects.existOrFail(`mlDetector ${detectorPosition} > mlDetectorTitle`); - expect( - await testSubjects.getVisibleText(`mlDetector ${detectorPosition} > mlDetectorTitle`) - ).to.eql(aggAndFieldIdentifier); + const actualDetectorTitle = await testSubjects.getVisibleText( + `mlDetector ${detectorPosition} > mlDetectorTitle` + ); + expect(actualDetectorTitle).to.eql( + aggAndFieldIdentifier, + `Expected detector title at position '${detectorPosition}' to be '${aggAndFieldIdentifier}' (got '${actualDetectorTitle}')` + ); await this.assertAnomalyChartExists(chartType, `mlDetector ${detectorPosition}`); }, @@ -393,10 +446,15 @@ export function MachineLearningJobWizardCommonProvider( async assertDateRangeSelection(expectedStartDate: string, expectedEndDate: string) { await retry.tryForTime(5000, async () => { - expect(await this.getSelectedDateRange()).to.eql({ - startDate: expectedStartDate, - endDate: expectedEndDate, - }); + const { startDate, endDate } = await this.getSelectedDateRange(); + expect(startDate).to.eql( + expectedStartDate, + `Expected start date to be '${expectedStartDate}' (got '${startDate}')` + ); + expect(endDate).to.eql( + expectedEndDate, + `Expected end date to be '${expectedEndDate}' (got '${endDate}')` + ); }); }, diff --git a/x-pack/test/functional/services/ml/job_wizard_multi_metric.ts b/x-pack/test/functional/services/ml/job_wizard_multi_metric.ts index 2fb768d924cff..c1c945933b106 100644 --- a/x-pack/test/functional/services/ml/job_wizard_multi_metric.ts +++ b/x-pack/test/functional/services/ml/job_wizard_multi_metric.ts @@ -20,7 +20,10 @@ export function MachineLearningJobWizardMultiMetricProvider({ getService }: FtrP const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( 'mlMultiMetricSplitFieldSelect > comboBoxInput' ); - expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected split field selection to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); }, async selectSplitField(identifier: string) { @@ -33,15 +36,21 @@ export function MachineLearningJobWizardMultiMetricProvider({ getService }: FtrP await testSubjects.existOrFail(`mlDataSplit > mlSplitCard front`); }, - async assertDetectorSplitFrontCardTitle(frontCardTitle: string) { - expect( - await testSubjects.getVisibleText(`mlDataSplit > mlSplitCard front > mlSplitCardTitle`) - ).to.eql(frontCardTitle); + async assertDetectorSplitFrontCardTitle(expectedFrontCardTitle: string) { + const actualFrontCardTitle = await testSubjects.getVisibleText( + `mlDataSplit > mlSplitCard front > mlSplitCardTitle` + ); + expect(actualFrontCardTitle).to.eql( + expectedFrontCardTitle, + `Expected front card title to be '${expectedFrontCardTitle}' (got '${actualFrontCardTitle}')` + ); }, - async assertDetectorSplitNumberOfBackCards(numberOfBackCards: number) { - expect(await testSubjects.findAll(`mlDataSplit > mlSplitCard back`)).to.have.length( - numberOfBackCards + async assertDetectorSplitNumberOfBackCards(expectedNumberOfBackCards: number) { + const allBackCards = await testSubjects.findAll(`mlDataSplit > mlSplitCard back`); + expect(allBackCards).to.have.length( + expectedNumberOfBackCards, + `Expected number of back cards to be '${expectedNumberOfBackCards}' (got '${allBackCards.length}')` ); }, }; diff --git a/x-pack/test/functional/services/ml/job_wizard_population.ts b/x-pack/test/functional/services/ml/job_wizard_population.ts index 8ff9d5c12a642..88b773b201c51 100644 --- a/x-pack/test/functional/services/ml/job_wizard_population.ts +++ b/x-pack/test/functional/services/ml/job_wizard_population.ts @@ -20,7 +20,10 @@ export function MachineLearningJobWizardPopulationProvider({ getService }: FtrPr const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( 'mlPopulationSplitFieldSelect > comboBoxInput' ); - expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected population field selection to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); }, async selectPopulationField(identifier: string) { @@ -41,7 +44,10 @@ export function MachineLearningJobWizardPopulationProvider({ getService }: FtrPr const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( `mlDetector ${detectorPosition} > mlByFieldSelect > comboBoxInput` ); - expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected detector split field selection to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); }, async selectDetectorSplitField(detectorPosition: number, identifier: string) { @@ -59,23 +65,30 @@ export function MachineLearningJobWizardPopulationProvider({ getService }: FtrPr ); }, - async assertDetectorSplitFrontCardTitle(detectorPosition: number, frontCardTitle: string) { - expect( - await testSubjects.getVisibleText( - `mlDetector ${detectorPosition} > mlDataSplit > mlSplitCard front > mlSplitCardTitle` - ) - ).to.eql(frontCardTitle); + async assertDetectorSplitFrontCardTitle( + detectorPosition: number, + expectedFrontCardTitle: string + ) { + const actualSplitFrontCardTitle = await testSubjects.getVisibleText( + `mlDetector ${detectorPosition} > mlDataSplit > mlSplitCard front > mlSplitCardTitle` + ); + expect(actualSplitFrontCardTitle).to.eql( + expectedFrontCardTitle, + `Expected front card title for detector position '${detectorPosition}' to be '${expectedFrontCardTitle}' (got '${actualSplitFrontCardTitle}')` + ); }, async assertDetectorSplitNumberOfBackCards( detectorPosition: number, - numberOfBackCards: number + expectedNumberOfBackCards: number ) { - expect( - await testSubjects.findAll( - `mlDetector ${detectorPosition} > mlDataSplit > mlSplitCard back` - ) - ).to.have.length(numberOfBackCards); + const allBackCards = await testSubjects.findAll( + `mlDetector ${detectorPosition} > mlDataSplit > mlSplitCard back` + ); + expect(allBackCards).to.have.length( + expectedNumberOfBackCards, + `Expected number of back cards for detector position '${detectorPosition}' to be '${expectedNumberOfBackCards}' (got '${allBackCards.length}')` + ); }, }; } diff --git a/x-pack/test/functional/services/ml/navigation.ts b/x-pack/test/functional/services/ml/navigation.ts index b0f993eab1a2b..8454a0b071b8f 100644 --- a/x-pack/test/functional/services/ml/navigation.ts +++ b/x-pack/test/functional/services/ml/navigation.ts @@ -25,8 +25,10 @@ export function MachineLearningNavigationProvider({ async assertTabsExist(tabTypeSubject: string, areaSubjects: string[]) { await retry.tryForTime(10000, async () => { - expect(await testSubjects.findAll(`~${tabTypeSubject}`, 3)).to.have.length( - areaSubjects.length + const allTabs = await testSubjects.findAll(`~${tabTypeSubject}`, 3); + expect(allTabs).to.have.length( + areaSubjects.length, + `Expected number of '${tabTypeSubject}' to be '${areaSubjects.length}' (got '${allTabs.length}')` ); for (const areaSubj of areaSubjects) { await testSubjects.existOrFail(`~${tabTypeSubject}&~${areaSubj}`, { timeout: 1000 }); diff --git a/x-pack/test/functional/services/transform/api.ts b/x-pack/test/functional/services/transform/api.ts index 5c7f04e7bc1a5..a805f5a3b6013 100644 --- a/x-pack/test/functional/services/transform/api.ts +++ b/x-pack/test/functional/services/transform/api.ts @@ -54,7 +54,10 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) { .expect(200) .then((res: any) => res.body); - expect(statsResponse.transforms).to.have.length(1); + expect(statsResponse.transforms).to.have.length( + 1, + `Expected transform stats to contain exactly 1 object (got '${statsResponse.transforms.length}')` + ); return statsResponse.transforms[0]; }, diff --git a/x-pack/test/functional/services/transform/transform_table.ts b/x-pack/test/functional/services/transform/transform_table.ts index 7bbe6724e3fff..3155ef0b26050 100644 --- a/x-pack/test/functional/services/transform/transform_table.ts +++ b/x-pack/test/functional/services/transform/transform_table.ts @@ -98,7 +98,10 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { uniqueColumnValues.sort(); // check if the returned unique value matches the supplied filter value - expect(uniqueColumnValues).to.eql(expectedColumnValues); + expect(uniqueColumnValues).to.eql( + expectedColumnValues, + `Expected '${tableSubj}' column values to be '${expectedColumnValues}' (got '${uniqueColumnValues}')` + ); }); } @@ -123,7 +126,12 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { public async assertTransformRowFields(transformId: string, expectedRow: object) { const rows = await this.parseTransformTable(); const transformRow = rows.filter((row) => row.id === transformId)[0]; - expect(transformRow).to.eql(expectedRow); + expect(transformRow).to.eql( + expectedRow, + `Expected transform row to be '${JSON.stringify(expectedRow)}' (got '${JSON.stringify( + transformRow + )}')` + ); } public async assertTransformExpandedRow() { diff --git a/x-pack/test/functional/services/transform/wizard.ts b/x-pack/test/functional/services/transform/wizard.ts index f2753ab645b9d..6a99e6ed007b6 100644 --- a/x-pack/test/functional/services/transform/wizard.ts +++ b/x-pack/test/functional/services/transform/wizard.ts @@ -120,20 +120,20 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { }); }, - async assertIndexPreview(columns: number, rows: number) { + async assertIndexPreview(columns: number, expectedNumberOfRows: number) { await retry.tryForTime(2000, async () => { // get a 2D array of rows and cell values const rowsData = await this.parseEuiDataGrid('transformIndexPreview'); expect(rowsData).to.length( - rows, - `EuiDataGrid rows should be ${rows} (got ${rowsData.length})` + expectedNumberOfRows, + `EuiDataGrid rows should be '${expectedNumberOfRows}' (got '${rowsData.length}')` ); rowsData.map((r, i) => expect(r).to.length( columns, - `EuiDataGrid row #${i + 1} column count should be ${columns} (got ${r.length})` + `EuiDataGrid row #${i + 1} column count should be '${columns}' (got '${r.length}')` ) ); }); @@ -185,7 +185,7 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { 'true'; expect(actualCheckState).to.eql( expectedCheckState, - `Advanced query editor switch check state should be ${expectedCheckState} (got ${actualCheckState})` + `Advanced query editor switch check state should be '${expectedCheckState}' (got '${actualCheckState}')` ); }, @@ -198,7 +198,10 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( 'transformGroupBySelection > comboBoxInput' ); - expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected group by value to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); }); }, @@ -214,7 +217,7 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { ); expect(actualLabel).to.eql( expectedLabel, - `Label for group by entry ${index} should be '${expectedLabel}' (got '${actualLabel}')` + `Label for group by entry '${index}' should be '${expectedLabel}' (got '${actualLabel}')` ); if (expectedIntervalLabel !== undefined) { @@ -223,7 +226,7 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { ); expect(actualIntervalLabel).to.eql( expectedIntervalLabel, - `Label for group by entry ${index} should be '${expectedIntervalLabel}' (got '${actualIntervalLabel}')` + `Label for group by entry '${index}' should be '${expectedIntervalLabel}' (got '${actualIntervalLabel}')` ); } }, @@ -248,7 +251,10 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( 'transformAggregationSelection > comboBoxInput' ); - expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected aggregation value to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); }); }, @@ -260,7 +266,7 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { ); expect(actualLabel).to.eql( expectedLabel, - `Label for aggregation entry ${index} should be '${expectedLabel}' (got '${actualLabel}')` + `Label for aggregation entry '${index}' should be '${expectedLabel}' (got '${actualLabel}')` ); }, @@ -278,7 +284,11 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { // const advancedEditorValue = JSON.parse(advancedEditorString); // expect(advancedEditorValue).to.eql(expectedValue); - expect(advancedEditorString.split('\n').splice(0, 3)).to.eql(expectedValue); + const splicedAdvancedEditorValue = advancedEditorString.split('\n').splice(0, 3); + expect(splicedAdvancedEditorValue).to.eql( + expectedValue, + `Expected the first editor lines to be '${expectedValue}' (got '${splicedAdvancedEditorValue}')` + ); }, async assertAdvancedPivotEditorSwitchExists() { @@ -291,7 +301,7 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { 'true'; expect(actualCheckState).to.eql( expectedCheckState, - `Advanced pivot editor switch check state should be ${expectedCheckState} (got ${actualCheckState})` + `Advanced pivot editor switch check state should be '${expectedCheckState}' (got '${actualCheckState}')` ); }, @@ -310,7 +320,7 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { const actualTransformId = await testSubjects.getAttribute('transformIdInput', 'value'); expect(actualTransformId).to.eql( expectedValue, - `Transform id input text should be ${expectedValue} (got ${actualTransformId})` + `Transform id input text should be '${expectedValue}' (got '${actualTransformId}')` ); }, @@ -330,7 +340,7 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { ); expect(actualTransformDescription).to.eql( expectedValue, - `Transform description input text should be ${expectedValue} (got ${actualTransformDescription})` + `Transform description input text should be '${expectedValue}' (got '${actualTransformDescription}')` ); }, @@ -352,7 +362,7 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { ); expect(actualDestinationIndex).to.eql( expectedValue, - `Destination index input text should be ${expectedValue} (got ${actualDestinationIndex})` + `Destination index input text should be '${expectedValue}' (got '${actualDestinationIndex}')` ); }, @@ -373,7 +383,7 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { 'true'; expect(actualCheckState).to.eql( expectedCheckState, - `Create index pattern switch check state should be ${expectedCheckState} (got ${actualCheckState})` + `Create index pattern switch check state should be '${expectedCheckState}' (got '${actualCheckState}')` ); }, @@ -387,7 +397,7 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { 'true'; expect(actualCheckState).to.eql( expectedCheckState, - `Continuous mode switch check state should be ${expectedCheckState} (got ${actualCheckState})` + `Continuous mode switch check state should be '${expectedCheckState}' (got '${actualCheckState}')` ); }, @@ -405,7 +415,7 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { expectedValue, `Expected 'Create and start' button to be '${ expectedValue ? 'enabled' : 'disabled' - }' (got ${isEnabled ? 'enabled' : 'disabled'}')` + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` ); }, @@ -421,7 +431,7 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { const isEnabled = await testSubjects.isEnabled('transformWizardCreateButton'); expect(isEnabled).to.eql( expectedValue, - `Expected 'Create' button to be '${expectedValue ? 'enabled' : 'disabled'}' (got ${ + `Expected 'Create' button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ isEnabled ? 'enabled' : 'disabled' }')` ); @@ -441,7 +451,7 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { expectedValue, `Expected 'Copy to clipboard' button to be '${ expectedValue ? 'enabled' : 'disabled' - }' (got ${isEnabled ? 'enabled' : 'disabled'}')` + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` ); }, @@ -460,7 +470,7 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { const isEnabled = await testSubjects.isEnabled('transformWizardStartButton'); expect(isEnabled).to.eql( expectedValue, - `Expected 'Start' button to be '${expectedValue ? 'enabled' : 'disabled'}' (got ${ + `Expected 'Start' button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ isEnabled ? 'enabled' : 'disabled' }')` );