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/.ci/packer_cache.sh b/.ci/packer_cache.sh index ab68a60dcfc27..d47ef93172a9d 100755 --- a/.ci/packer_cache.sh +++ b/.ci/packer_cache.sh @@ -35,20 +35,20 @@ mkdir -p ".geckodriver" cp "node_modules/geckodriver/geckodriver.tar.gz" .geckodriver/geckodriver.tar.gz echo "$geckodriverPkgVersion" > .geckodriver/pkgVersion +echo "Creating bootstrap_cache archive" + # archive cacheable directories mkdir -p "$HOME/.kibana/bootstrap_cache" tar -cf "$HOME/.kibana/bootstrap_cache/$branch.tar" \ - node_modules \ - packages/*/node_modules \ - x-pack/node_modules \ - x-pack/legacy/plugins/*/node_modules \ x-pack/legacy/plugins/reporting/.chromium \ - test/plugin_functional/plugins/*/node_modules \ - examples/*/node_modules \ .es \ .chromedriver \ .geckodriver; +echo "Adding node_modules" +# Find all of the node_modules directories that aren't test fixtures, and aren't inside other node_modules directories, and append them to the tar +find . -type d -name node_modules -not -path '*__fixtures__*' -prune -print0 | xargs -0I % tar -rf "$HOME/.kibana/bootstrap_cache/$branch.tar" "%" + echo "created $HOME/.kibana/bootstrap_cache/$branch.tar" if [ "$branch" == "master" ]; then diff --git a/.eslintignore b/.eslintignore index c3d7930732fa2..fbdd70703f3c4 100644 --- a/.eslintignore +++ b/.eslintignore @@ -26,7 +26,8 @@ target /src/plugins/vis_type_timelion/public/_generated_/** /src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.* /x-pack/legacy/plugins/**/__tests__/fixtures/** -/x-pack/plugins/apm/e2e/cypress/**/snapshots.js +/x-pack/plugins/apm/e2e/**/snapshots.js +/x-pack/plugins/apm/e2e/tmp/* /x-pack/plugins/canvas/canvas_plugin /x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts /x-pack/plugins/canvas/shareable_runtime/build diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6df4136ef74af..83f4f90b9204d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -6,10 +6,7 @@ /x-pack/plugins/dashboard_enhanced/ @elastic/kibana-app /x-pack/plugins/lens/ @elastic/kibana-app /x-pack/plugins/graph/ @elastic/kibana-app -/src/legacy/server/url_shortening/ @elastic/kibana-app -/src/legacy/server/sample_data/ @elastic/kibana-app /src/legacy/core_plugins/kibana/public/local_application_service/ @elastic/kibana-app -/src/legacy/core_plugins/kibana/public/dev_tools/ @elastic/kibana-app /src/plugins/dashboard/ @elastic/kibana-app /src/plugins/discover/ @elastic/kibana-app /src/plugins/input_control_vis/ @elastic/kibana-app @@ -158,7 +155,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 @@ -180,7 +176,7 @@ /x-pack/plugins/telemetry_collection_xpack/ @elastic/pulse # Kibana Alerting Services -/x-pack/plugins/alerting/ @elastic/kibana-alerting-services +/x-pack/plugins/alerts/ @elastic/kibana-alerting-services /x-pack/plugins/actions/ @elastic/kibana-alerting-services /x-pack/plugins/event_log/ @elastic/kibana-alerting-services /x-pack/plugins/task_manager/ @elastic/kibana-alerting-services diff --git a/.gitignore b/.gitignore index f843609d32f7e..b3911d0f8d0c2 100644 --- a/.gitignore +++ b/.gitignore @@ -44,8 +44,8 @@ package-lock.json *.sublime-* npm-debug.log* .tern-project -x-pack/plugins/apm/tsconfig.json -apm.tsconfig.json -/x-pack/legacy/plugins/apm/e2e/snapshots.js -/x-pack/plugins/apm/e2e/snapshots.js .nyc_output + +# apm plugin +/x-pack/plugins/apm/tsconfig.json +apm.tsconfig.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1053cc2f65396..4bf659345d387 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,26 +13,44 @@ A high level overview of our contributing guidelines. - ["My issue isn't getting enough attention"](#my-issue-isnt-getting-enough-attention) - ["I want to help!"](#i-want-to-help) - [How We Use Git and GitHub](#how-we-use-git-and-github) + - [Forking](#forking) - [Branching](#branching) - [Commits and Merging](#commits-and-merging) + - [Rebasing and fixing merge conflicts](#rebasing-and-fixing-merge-conflicts) - [What Goes Into a Pull Request](#what-goes-into-a-pull-request) - [Contributing Code](#contributing-code) - [Setting Up Your Development Environment](#setting-up-your-development-environment) + - [Increase node.js heap size](#increase-nodejs-heap-size) + - [Running Elasticsearch Locally](#running-elasticsearch-locally) + - [Nightly snapshot (recommended)](#nightly-snapshot-recommended) + - [Keeping data between snapshots](#keeping-data-between-snapshots) + - [Source](#source) + - [Archive](#archive) + - [Sample Data](#sample-data) + - [Running Elasticsearch Remotely](#running-elasticsearch-remotely) + - [Running remote clusters](#running-remote-clusters) + - [Running Kibana](#running-kibana) + - [Running Kibana in Open-Source mode](#running-kibana-in-open-source-mode) + - [Unsupported URL Type](#unsupported-url-type) - [Customizing `config/kibana.dev.yml`](#customizing-configkibanadevyml) + - [Potential Optimization Pitfalls](#potential-optimization-pitfalls) - [Setting Up SSL](#setting-up-ssl) - [Linting](#linting) + - [Setup Guide for VS Code Users](#setup-guide-for-vs-code-users) - [Internationalization](#internationalization) - [Localization](#localization) + - [Styling with SASS](#styling-with-sass) - [Testing and Building](#testing-and-building) - [Debugging server code](#debugging-server-code) - [Instrumenting with Elastic APM](#instrumenting-with-elastic-apm) - - [Debugging Unit Tests](#debugging-unit-tests) - - [Unit Testing Plugins](#unit-testing-plugins) - - [Automated Accessibility Testing](#automated-accessibility-testing) - - [Cross-browser compatibility](#cross-browser-compatibility) - - [Testing compatibility locally](#testing-compatibility-locally) - - [Running Browser Automation Tests](#running-browser-automation-tests) - - [Browser Automation Notes](#browser-automation-notes) + - [Unit testing frameworks](#unit-testing-frameworks) + - [Running specific Kibana tests](#running-specific-kibana-tests) + - [Debugging Unit Tests](#debugging-unit-tests) + - [Unit Testing Plugins](#unit-testing-plugins) + - [Automated Accessibility Testing](#automated-accessibility-testing) + - [Cross-browser compatibility](#cross-browser-compatibility) + - [Testing compatibility locally](#testing-compatibility-locally) + - [Running Browser Automation Tests](#running-browser-automation-tests) - [Building OS packages](#building-os-packages) - [Writing documentation](#writing-documentation) - [Release Notes Process](#release-notes-process) @@ -414,7 +432,7 @@ extract them to a `JSON` file or integrate translations back to Kibana. To know We cannot support accepting contributions to the translations from any source other than the translators we have engaged to do the work. We are still to develop a proper process to accept any contributed translations. We certainly appreciate that people care enough about the localization effort to want to help improve the quality. We aim to build out a more comprehensive localization process for the future and will notify you once contributions can be supported, but for the time being, we are not able to incorporate suggestions. -### Syling with SASS +### Styling with SASS When writing a new component, create a sibling SASS file of the same name and import directly into the JS/TS component file. Doing so ensures the styles are never separated or lost on import and allows for better modularization (smaller individual plugin asset footprint). @@ -467,10 +485,10 @@ macOS users on a machine with a discrete graphics card may see significant speed - Uncheck the "Prefer integrated to discrete GPU" option - Restart iTerm -### Debugging Server Code +#### Debugging Server Code `yarn debug` will start the server with Node's inspect flag. Kibana's development mode will start three processes on ports `9229`, `9230`, and `9231`. Chrome's developer tools need to be configured to connect to all three connections. Add `localhost:` for each Kibana process in Chrome's developer tools connection tab. -### Instrumenting with Elastic APM +#### Instrumenting with Elastic APM Kibana ships with the [Elastic APM Node.js Agent](https://github.com/elastic/apm-agent-nodejs) built-in for debugging purposes. Its default configuration is meant to be used by core Kibana developers only, but it can easily be re-configured to your needs. @@ -501,13 +519,13 @@ ELASTIC_APM_ACTIVE=true yarn start Once the agent is active, it will trace all incoming HTTP requests to Kibana, monitor for errors, and collect process-level metrics. The collected data will be sent to the APM Server and is viewable in the APM UI in Kibana. -### Unit testing frameworks +#### Unit testing frameworks Kibana is migrating unit testing from Mocha to Jest. Legacy unit tests still exist in Mocha but all new unit tests should be written in Jest. Mocha tests are contained in `__tests__` directories. Whereas Jest tests are stored in the same directory as source code files with the `.test.js` suffix. -### Running specific Kibana tests +#### Running specific Kibana tests The following table outlines possible test file locations and how to invoke them: @@ -540,7 +558,7 @@ Test runner arguments: yarn test:ftr:runner --config test/api_integration/config.js --grep='should return 404 if id does not match any sample data sets' ``` -### Debugging Unit Tests +#### Debugging Unit Tests The standard `yarn test` task runs several sub tasks and can take several minutes to complete, making debugging failures pretty painful. In order to ease the pain specialized tasks provide alternate methods for running the tests. @@ -567,7 +585,7 @@ In the screenshot below, you'll notice the URL is `localhost:9876/debug.html`. Y ![Browser test debugging](http://i.imgur.com/DwHxgfq.png) -### Unit Testing Plugins +#### Unit Testing Plugins This should work super if you're using the [Kibana plugin generator](https://github.com/elastic/kibana/tree/master/packages/kbn-plugin-generator). If you're not using the generator, well, you're on your own. We suggest you look at how the generator works. @@ -578,7 +596,7 @@ yarn test:mocha yarn test:karma:debug # remove the debug flag to run them once and close ``` -### Automated Accessibility Testing +#### Automated Accessibility Testing To run the tests locally: @@ -595,11 +613,11 @@ can be run locally using their browser plugins: - [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US) - [Firefox](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/) -### Cross-browser Compatibility +#### Cross-browser Compatibility -#### Testing Compatibility Locally +##### Testing Compatibility Locally -##### Testing IE on OS X +###### Testing IE on OS X * [Download VMWare Fusion](http://www.vmware.com/products/fusion/fusion-evaluation.html). * [Download IE virtual machines](https://developer.microsoft.com/en-us/microsoft-edge/tools/vms/#downloads) for VMWare. @@ -610,7 +628,7 @@ can be run locally using their browser plugins: * Now you can run your VM, open the browser, and navigate to `http://computer.local:5601` to test Kibana. * Alternatively you can use browserstack -#### Running Browser Automation Tests +##### Running Browser Automation Tests [Read about the `FunctionalTestRunner`](https://www.elastic.co/guide/en/kibana/current/development-functional-tests.html) to learn more about how you can run and develop functional tests for Kibana core and plugins. diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.euiicontype.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.euiicontype.md index fe95cb38cd97c..e30e8262f40b2 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.euiicontype.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.euiicontype.md @@ -4,7 +4,7 @@ ## ChromeNavLink.euiIconType property -A EUI iconType that will be used for the app's icon. This icon takes precendence over the `icon` property. +A EUI iconType that will be used for the app's icon. This icon takes precedence over the `icon` property. Signature: diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.href.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.href.md new file mode 100644 index 0000000000000..a8af0c997ca78 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.href.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeNavLink](./kibana-plugin-core-public.chromenavlink.md) > [href](./kibana-plugin-core-public.chromenavlink.href.md) + +## ChromeNavLink.href property + +Settled state between `url`, `baseUrl`, and `active` + +Signature: + +```typescript +readonly href?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md index a9fabb38df869..0349e865bff97 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md @@ -20,8 +20,9 @@ export interface ChromeNavLink | [category](./kibana-plugin-core-public.chromenavlink.category.md) | AppCategory | The category the app lives in | | [disabled](./kibana-plugin-core-public.chromenavlink.disabled.md) | boolean | Disables a link from being clickable. | | [disableSubUrlTracking](./kibana-plugin-core-public.chromenavlink.disablesuburltracking.md) | boolean | A flag that tells legacy chrome to ignore the link when tracking sub-urls | -| [euiIconType](./kibana-plugin-core-public.chromenavlink.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precendence over the icon property. | +| [euiIconType](./kibana-plugin-core-public.chromenavlink.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precedence over the icon property. | | [hidden](./kibana-plugin-core-public.chromenavlink.hidden.md) | boolean | Hides a link from the navigation. | +| [href](./kibana-plugin-core-public.chromenavlink.href.md) | string | Settled state between url, baseUrl, and active | | [icon](./kibana-plugin-core-public.chromenavlink.icon.md) | string | A URL to an image file used as an icon. Used as a fallback if euiIconType is not provided. | | [id](./kibana-plugin-core-public.chromenavlink.id.md) | string | A unique identifier for looking up links. | | [linkToLastSubUrl](./kibana-plugin-core-public.chromenavlink.linktolastsuburl.md) | boolean | Whether or not the subUrl feature should be enabled. | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlinkupdateablefields.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlinkupdateablefields.md index 7f6dc7e0d5640..bd5a1399cded7 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlinkupdateablefields.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlinkupdateablefields.md @@ -8,5 +8,5 @@ Signature: ```typescript -export declare type ChromeNavLinkUpdateableFields = Partial>; +export declare type ChromeNavLinkUpdateableFields = Partial>; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.adminclient.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.adminclient.md deleted file mode 100644 index 3fcb855586129..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.adminclient.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) > [adminClient](./kibana-plugin-core-server.elasticsearchservicesetup.adminclient.md) - -## ElasticsearchServiceSetup.adminClient property - -> Warning: This API is now obsolete. -> -> Use [ElasticsearchServiceStart.legacy.client](./kibana-plugin-core-server.elasticsearchservicestart.legacy.md) instead. -> -> A client for the `admin` cluster. All Elasticsearch config value changes are processed under the hood. See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). -> - -Signature: - -```typescript -readonly adminClient: IClusterClient; -``` - -## Example - - -```js -const client = core.elasticsearch.adminClient; - -``` - diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.createclient.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.createclient.md deleted file mode 100644 index 75bf6c6aa461b..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.createclient.md +++ /dev/null @@ -1,28 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) > [createClient](./kibana-plugin-core-server.elasticsearchservicesetup.createclient.md) - -## ElasticsearchServiceSetup.createClient property - -> Warning: This API is now obsolete. -> -> Use [ElasticsearchServiceStart.legacy.createClient](./kibana-plugin-core-server.elasticsearchservicestart.legacy.md) instead. -> -> Create application specific Elasticsearch cluster API client with customized config. See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). -> - -Signature: - -```typescript -readonly createClient: (type: string, clientConfig?: Partial) => ICustomClusterClient; -``` - -## Example - - -```js -const client = elasticsearch.createCluster('my-app-name', config); -const data = await client.callAsInternalUser(); - -``` - diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.dataclient.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.dataclient.md deleted file mode 100644 index 867cafa957f42..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.dataclient.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) > [dataClient](./kibana-plugin-core-server.elasticsearchservicesetup.dataclient.md) - -## ElasticsearchServiceSetup.dataClient property - -> Warning: This API is now obsolete. -> -> Use [ElasticsearchServiceStart.legacy.client](./kibana-plugin-core-server.elasticsearchservicestart.legacy.md) instead. -> -> A client for the `data` cluster. All Elasticsearch config value changes are processed under the hood. See [IClusterClient](./kibana-plugin-core-server.iclusterclient.md). -> - -Signature: - -```typescript -readonly dataClient: IClusterClient; -``` - -## Example - - -```js -const client = core.elasticsearch.dataClient; - -``` - diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.legacy.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.legacy.md new file mode 100644 index 0000000000000..e8c4c63dc6a96 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.legacy.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) > [legacy](./kibana-plugin-core-server.elasticsearchservicesetup.legacy.md) + +## ElasticsearchServiceSetup.legacy property + +> Warning: This API is now obsolete. +> +> Use [ElasticsearchServiceStart.legacy](./kibana-plugin-core-server.elasticsearchservicestart.legacy.md) instead. +> + +Signature: + +```typescript +legacy: { + readonly createClient: (type: string, clientConfig?: Partial) => ICustomClusterClient; + readonly client: IClusterClient; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.md index ee56f8b4a6284..c1e23527e9516 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicesetup.md @@ -15,7 +15,5 @@ export interface ElasticsearchServiceSetup | Property | Type | Description | | --- | --- | --- | -| [adminClient](./kibana-plugin-core-server.elasticsearchservicesetup.adminclient.md) | IClusterClient | | -| [createClient](./kibana-plugin-core-server.elasticsearchservicesetup.createclient.md) | (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => ICustomClusterClient | | -| [dataClient](./kibana-plugin-core-server.elasticsearchservicesetup.dataclient.md) | IClusterClient | | +| [legacy](./kibana-plugin-core-server.elasticsearchservicesetup.legacy.md) | {
readonly createClient: (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => ICustomClusterClient;
readonly client: IClusterClient;
} | | diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.legacy.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.legacy.md index 08765aaf93d3d..667a36091f232 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.legacy.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.legacy.md @@ -4,6 +4,11 @@ ## ElasticsearchServiceStart.legacy property +> Warning: This API is now obsolete. +> +> Provided for the backward compatibility. Switch to the new elasticsearch client as soon as https://github.com/elastic/kibana/issues/35508 done. +> + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 14e01fda3d287..147a72016b235 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -125,7 +125,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginConfigDescriptor](./kibana-plugin-core-server.pluginconfigdescriptor.md) | Describes a plugin configuration properties. | | [PluginInitializerContext](./kibana-plugin-core-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | | [PluginManifest](./kibana-plugin-core-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. | -| [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.dataClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request | +| [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.legacy.client](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request | | [RouteConfig](./kibana-plugin-core-server.routeconfig.md) | Route specific configuration. | | [RouteConfigOptions](./kibana-plugin-core-server.routeconfigoptions.md) | Additional route options. | | [RouteConfigOptionsBody](./kibana-plugin-core-server.routeconfigoptionsbody.md) | Additional body options for a route | diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md index 6b3fc4c03ec73..99be0676bcda3 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md @@ -6,7 +6,7 @@ Plugin specific context passed to a route handler. -Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.dataClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request +Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.legacy.client](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request Signature: 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/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md index 58690300b3bd6..85eb4825bc2e3 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md @@ -7,5 +7,5 @@ Signature: ```typescript -QueryStringInput: React.FC> +QueryStringInput: React.FC> ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md index b015ebfcbaada..fc141b8c89c18 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md @@ -7,7 +7,7 @@ Signature: ```typescript -SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "customSubmitButton" | "screenTitle" | "dataTestSubj" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "timeHistory" | "onFiltersUpdated">, any> & { - WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; +SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "customSubmitButton" | "screenTitle" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "timeHistory" | "onFiltersUpdated">, any> & { + WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; } ``` 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/index.asciidoc b/docs/index.asciidoc index 5474772ab7da8..add91600a34ea 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -3,11 +3,11 @@ :include-xpack: true :lang: en -:kib-repo-dir: {docdir} +:kib-repo-dir: {kibana-root}/docs :blog-ref: https://www.elastic.co/blog/ :wikipedia: https://en.wikipedia.org/wiki -include::{asciidoc-dir}/../../shared/versions/stack/{source_branch}.asciidoc[] +include::{docs-root}/shared/versions/stack/{source_branch}.asciidoc[] :docker-repo: docker.elastic.co/kibana/kibana :docker-image: docker.elastic.co/kibana/kibana:{version} @@ -18,7 +18,7 @@ include::{asciidoc-dir}/../../shared/versions/stack/{source_branch}.asciidoc[] :blob: {repo}blob/{branch}/ :security-ref: https://www.elastic.co/community/security/ -include::{asciidoc-dir}/../../shared/attributes.asciidoc[] +include::{docs-root}/shared/attributes.asciidoc[] include::user/index.asciidoc[] diff --git a/docs/settings/ml-settings.asciidoc b/docs/settings/ml-settings.asciidoc index 24e38e73bca9b..83443636fa633 100644 --- a/docs/settings/ml-settings.asciidoc +++ b/docs/settings/ml-settings.asciidoc @@ -6,14 +6,14 @@ ++++ You do not need to configure any settings to use {kib} {ml-features}. They are -enabled by default. +enabled by default. [[general-ml-settings-kb]] ==== General {ml} settings [cols="2*<"] |=== -| `xpack.ml.enabled` +| `xpack.ml.enabled` {ess-icon} | Set to `true` (default) to enable {kib} {ml-features}. + + If set to `false` in `kibana.yml`, the {ml} icon is hidden in this {kib} @@ -23,13 +23,7 @@ enabled by default. |=== -[[data-visualizer-settings]] -==== {data-viz} settings +[[advanced-ml-settings-kb]] +==== Advanced {ml} settings -[cols="2*<"] -|=== -| `xpack.ml.file_data_visualizer.max_file_size` - | Sets the file size limit when importing data in the {data-viz}. The default - value is `100MB`. The highest supported value for this setting is `1GB`. - -|=== +Refer to <>. \ No newline at end of file diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 6596f93a88f51..42d616c80119b 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -627,17 +627,17 @@ Valid locales are: `en`, `zh-CN`, `ja-JP`. *Default: `en`* |=== -include::{docdir}/settings/alert-action-settings.asciidoc[] -include::{docdir}/settings/apm-settings.asciidoc[] -include::{docdir}/settings/dev-settings.asciidoc[] -include::{docdir}/settings/graph-settings.asciidoc[] -include::{docdir}/settings/infrastructure-ui-settings.asciidoc[] -include::{docdir}/settings/i18n-settings.asciidoc[] -include::{docdir}/settings/logs-ui-settings.asciidoc[] -include::{docdir}/settings/ml-settings.asciidoc[] -include::{docdir}/settings/monitoring-settings.asciidoc[] -include::{docdir}/settings/reporting-settings.asciidoc[] +include::{kib-repo-dir}/settings/alert-action-settings.asciidoc[] +include::{kib-repo-dir}/settings/apm-settings.asciidoc[] +include::{kib-repo-dir}/settings/dev-settings.asciidoc[] +include::{kib-repo-dir}/settings/graph-settings.asciidoc[] +include::{kib-repo-dir}/settings/infrastructure-ui-settings.asciidoc[] +include::{kib-repo-dir}/settings/i18n-settings.asciidoc[] +include::{kib-repo-dir}/settings/logs-ui-settings.asciidoc[] +include::{kib-repo-dir}/settings/ml-settings.asciidoc[] +include::{kib-repo-dir}/settings/monitoring-settings.asciidoc[] +include::{kib-repo-dir}/settings/reporting-settings.asciidoc[] include::secure-settings.asciidoc[] -include::{docdir}/settings/security-settings.asciidoc[] -include::{docdir}/settings/spaces-settings.asciidoc[] -include::{docdir}/settings/telemetry-settings.asciidoc[] +include::{kib-repo-dir}/settings/security-settings.asciidoc[] +include::{kib-repo-dir}/settings/spaces-settings.asciidoc[] +include::{kib-repo-dir}/settings/telemetry-settings.asciidoc[] 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/examples/alerting_example/kibana.json b/examples/alerting_example/kibana.json index 76c549adc7970..2b6389649cef9 100644 --- a/examples/alerting_example/kibana.json +++ b/examples/alerting_example/kibana.json @@ -4,6 +4,6 @@ "kibanaVersion": "kibana", "server": true, "ui": true, - "requiredPlugins": ["triggers_actions_ui", "charts", "data", "alerting", "actions"], + "requiredPlugins": ["triggers_actions_ui", "charts", "data", "alerts", "actions"], "optionalPlugins": [] } diff --git a/examples/alerting_example/public/alert_types/astros.tsx b/examples/alerting_example/public/alert_types/astros.tsx index 2e263e454fa0c..d52223cb6b988 100644 --- a/examples/alerting_example/public/alert_types/astros.tsx +++ b/examples/alerting_example/public/alert_types/astros.tsx @@ -32,12 +32,12 @@ import { import { i18n } from '@kbn/i18n'; import { flatten } from 'lodash'; import { ALERTING_EXAMPLE_APP_ID, Craft, Operator } from '../../common/constants'; -import { SanitizedAlert } from '../../../../x-pack/plugins/alerting/common'; -import { PluginSetupContract as AlertingSetup } from '../../../../x-pack/plugins/alerting/public'; +import { SanitizedAlert } from '../../../../x-pack/plugins/alerts/common'; +import { PluginSetupContract as AlertingSetup } from '../../../../x-pack/plugins/alerts/public'; import { AlertTypeModel } from '../../../../x-pack/plugins/triggers_actions_ui/public'; -export function registerNavigation(alerting: AlertingSetup) { - alerting.registerNavigation( +export function registerNavigation(alerts: AlertingSetup) { + alerts.registerNavigation( ALERTING_EXAMPLE_APP_ID, 'example.people-in-space', (alert: SanitizedAlert) => `/astros/${alert.id}` diff --git a/examples/alerting_example/public/alert_types/index.ts b/examples/alerting_example/public/alert_types/index.ts index 96d9c09d15836..db9f855b573e8 100644 --- a/examples/alerting_example/public/alert_types/index.ts +++ b/examples/alerting_example/public/alert_types/index.ts @@ -19,15 +19,15 @@ import { registerNavigation as registerPeopleInSpaceNavigation } from './astros'; import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; -import { SanitizedAlert } from '../../../../x-pack/plugins/alerting/common'; -import { PluginSetupContract as AlertingSetup } from '../../../../x-pack/plugins/alerting/public'; +import { SanitizedAlert } from '../../../../x-pack/plugins/alerts/common'; +import { PluginSetupContract as AlertingSetup } from '../../../../x-pack/plugins/alerts/public'; -export function registerNavigation(alerting: AlertingSetup) { +export function registerNavigation(alerts: AlertingSetup) { // register default navigation - alerting.registerDefaultNavigation( + alerts.registerDefaultNavigation( ALERTING_EXAMPLE_APP_ID, (alert: SanitizedAlert) => `/alert/${alert.id}` ); - registerPeopleInSpaceNavigation(alerting); + registerPeopleInSpaceNavigation(alerts); } diff --git a/examples/alerting_example/public/components/view_alert.tsx b/examples/alerting_example/public/components/view_alert.tsx index e418ed91096eb..75a515bfa1b25 100644 --- a/examples/alerting_example/public/components/view_alert.tsx +++ b/examples/alerting_example/public/components/view_alert.tsx @@ -36,7 +36,7 @@ import { Alert, AlertTaskState, BASE_ALERT_API_PATH, -} from '../../../../x-pack/plugins/alerting/common'; +} from '../../../../x-pack/plugins/alerts/common'; import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; type Props = RouteComponentProps & { diff --git a/examples/alerting_example/public/components/view_astros_alert.tsx b/examples/alerting_example/public/components/view_astros_alert.tsx index 296269180dd7b..19f235a3f3e4e 100644 --- a/examples/alerting_example/public/components/view_astros_alert.tsx +++ b/examples/alerting_example/public/components/view_astros_alert.tsx @@ -38,7 +38,7 @@ import { Alert, AlertTaskState, BASE_ALERT_API_PATH, -} from '../../../../x-pack/plugins/alerting/common'; +} from '../../../../x-pack/plugins/alerts/common'; import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants'; type Props = RouteComponentProps & { diff --git a/examples/alerting_example/public/plugin.tsx b/examples/alerting_example/public/plugin.tsx index e3748e3235f47..524ff18bd434e 100644 --- a/examples/alerting_example/public/plugin.tsx +++ b/examples/alerting_example/public/plugin.tsx @@ -18,7 +18,7 @@ */ import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; -import { PluginSetupContract as AlertingSetup } from '../../../x-pack/plugins/alerting/public'; +import { PluginSetupContract as AlertingSetup } from '../../../x-pack/plugins/alerts/public'; import { ChartsPluginStart } from '../../../src/plugins/charts/public'; import { TriggersAndActionsUIPublicPluginSetup } from '../../../x-pack/plugins/triggers_actions_ui/public'; import { DataPublicPluginStart } from '../../../src/plugins/data/public'; @@ -30,12 +30,12 @@ export type Setup = void; export type Start = void; export interface AlertingExamplePublicSetupDeps { - alerting: AlertingSetup; + alerts: AlertingSetup; triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; } export interface AlertingExamplePublicStartDeps { - alerting: AlertingSetup; + alerts: AlertingSetup; triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; charts: ChartsPluginStart; data: DataPublicPluginStart; @@ -44,7 +44,7 @@ export interface AlertingExamplePublicStartDeps { export class AlertingExamplePlugin implements Plugin { public setup( core: CoreSetup, - { alerting, triggers_actions_ui }: AlertingExamplePublicSetupDeps + { alerts, triggers_actions_ui }: AlertingExamplePublicSetupDeps ) { core.application.register({ id: 'AlertingExample', @@ -59,7 +59,7 @@ export class AlertingExamplePlugin implements Plugin { - public setup(core: CoreSetup, { alerting }: AlertingExampleDeps) { - alerting.registerType(alwaysFiringAlert); - alerting.registerType(peopleInSpaceAlert); + public setup(core: CoreSetup, { alerts }: AlertingExampleDeps) { + alerts.registerType(alwaysFiringAlert); + alerts.registerType(peopleInSpaceAlert); } public start() {} diff --git a/package.json b/package.json index 2dca52121d056..cc1f7eb6c1dd3 100644 --- a/package.json +++ b/package.json @@ -408,7 +408,7 @@ "chai": "3.5.0", "chance": "1.0.18", "cheerio": "0.22.0", - "chromedriver": "^81.0.0", + "chromedriver": "^83.0.0", "classnames": "2.2.6", "dedent": "^0.7.0", "delete-empty": "^2.0.0", diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index b7c9a63897bf9..7bd7a236a43aa 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -28,6 +28,7 @@ "cpy": "^8.0.0", "css-loader": "^3.4.2", "del": "^5.1.0", + "execa": "^4.0.2", "file-loader": "^4.2.0", "istanbul-instrumenter-loader": "^3.0.1", "jest-diff": "^25.1.0", diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.scss b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.scss new file mode 100644 index 0000000000000..563d20e99ce82 --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.scss @@ -0,0 +1,3 @@ +body { + color: green; +} diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts index 817c4796562e8..7ddd10f4a388f 100644 --- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts @@ -18,6 +18,7 @@ */ import './legacy/styles.scss'; +import './index.scss'; import { fooLibFn } from '../../foo/public/index'; export * from './lib'; export { fooLibFn }; diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index 8bf5eb72523ff..2814ab32017d2 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -55,8 +55,8 @@ OptimizerConfig { } `; -exports[`prepares assets for distribution: 1 async bundle 1`] = `"(window[\\"foo_bundle_jsonpfunction\\"]=window[\\"foo_bundle_jsonpfunction\\"]||[]).push([[1],[,function(module,__webpack_exports__,__webpack_require__){\\"use strict\\";__webpack_require__.r(__webpack_exports__);__webpack_require__.d(__webpack_exports__,\\"foo\\",(function(){return foo}));function foo(){}}]]);"`; +exports[`prepares assets for distribution: bar bundle 1`] = `"var __kbnBundles__=typeof __kbnBundles__===\\"object\\"?__kbnBundles__:{};__kbnBundles__[\\"plugin/bar\\"]=function(modules){function webpackJsonpCallback(data){var chunkIds=data[0];var moreModules=data[1];var moduleId,chunkId,i=0,resolves=[];for(;i value.split(REPO_ROOT).join('').replace(/\\/g, '/'), + test: (value: any) => typeof value === 'string' && value.includes(REPO_ROOT), +}); const log = new ToolingLog({ level: 'error', @@ -129,13 +132,14 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { const foo = config.bundles.find((b) => b.id === 'foo')!; expect(foo).toBeTruthy(); foo.cache.refresh(); - expect(foo.cache.getModuleCount()).toBe(4); + expect(foo.cache.getModuleCount()).toBe(5); expect(foo.cache.getReferencedFiles()).toMatchInlineSnapshot(` Array [ /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/async_import.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/ext.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/index.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/lib.ts, + /packages/kbn-ui-shared-deps/public_path_module_creator.js, ] `); @@ -143,14 +147,15 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { expect(bar).toBeTruthy(); bar.cache.refresh(); expect(bar.cache.getModuleCount()).toBe( - // code + styles + style/css-loader runtimes - 15 + // code + styles + style/css-loader runtimes + public path updater + 21 ); expect(bar.cache.getReferencedFiles()).toMatchInlineSnapshot(` Array [ /node_modules/css-loader/package.json, /node_modules/style-loader/package.json, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.scss, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/legacy/styles.scss, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/lib.ts, @@ -159,6 +164,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/index.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/lib.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/legacy/ui/public/icon.svg, + /packages/kbn-ui-shared-deps/public_path_module_creator.js, ] `); }); @@ -207,7 +213,7 @@ it('prepares assets for distribution', async () => { expectFileMatchesSnapshotWithCompression('plugins/foo/target/public/foo.plugin.js', 'foo bundle'); expectFileMatchesSnapshotWithCompression( 'plugins/foo/target/public/1.plugin.js', - '1 async bundle' + 'foo async bundle' ); expectFileMatchesSnapshotWithCompression('plugins/bar/target/public/bar.plugin.js', 'bar bundle'); }); @@ -217,6 +223,7 @@ it('prepares assets for distribution', async () => { */ const expectFileMatchesSnapshotWithCompression = (filePath: string, snapshotLabel: string) => { const raw = Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, filePath), 'utf8'); + expect(raw).toMatchSnapshot(snapshotLabel); // Verify the brotli variant matches diff --git a/packages/kbn-optimizer/src/optimizer/observe_worker.ts b/packages/kbn-optimizer/src/optimizer/observe_worker.ts index f5c944cefb76f..c929cf62d1bb0 100644 --- a/packages/kbn-optimizer/src/optimizer/observe_worker.ts +++ b/packages/kbn-optimizer/src/optimizer/observe_worker.ts @@ -17,10 +17,10 @@ * under the License. */ -import { fork, ChildProcess } from 'child_process'; import { Readable } from 'stream'; import { inspect } from 'util'; +import execa from 'execa'; import * as Rx from 'rxjs'; import { map, takeUntil } from 'rxjs/operators'; @@ -42,7 +42,7 @@ export interface WorkerStarted { export type WorkerStatus = WorkerStdio | WorkerStarted; interface ProcResource extends Rx.Unsubscribable { - proc: ChildProcess; + proc: execa.ExecaChildProcess; } const isNumeric = (input: any) => String(input).match(/^[0-9]+$/); @@ -70,20 +70,22 @@ function usingWorkerProc( config: OptimizerConfig, workerConfig: WorkerConfig, bundles: Bundle[], - fn: (proc: ChildProcess) => Rx.Observable + fn: (proc: execa.ExecaChildProcess) => Rx.Observable ) { return Rx.using( (): ProcResource => { const args = [JSON.stringify(workerConfig), JSON.stringify(bundles.map((b) => b.toSpec()))]; - const proc = fork(require.resolve('../worker/run_worker'), args, { - stdio: ['ignore', 'pipe', 'pipe', 'ipc'], - execArgv: [ + const proc = execa.node(require.resolve('../worker/run_worker'), args, { + nodeOptions: [ ...(inspectFlag && config.inspectWorkers ? [`${inspectFlag}=${inspectPortCounter++}`] : []), ...(config.maxWorkerCount <= 3 ? ['--max-old-space-size=2048'] : []), ], + buffer: false, + stderr: 'pipe', + stdout: 'pipe', }); return { diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 0c9a5b0a75687..763f1d515804f 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -17,6 +17,7 @@ * under the License. */ +import Fs from 'fs'; import Path from 'path'; import normalizePath from 'normalize-path'; @@ -86,12 +87,17 @@ function dynamicExternals(bundle: Bundle, context: string, request: string) { } export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { + const extensions = ['.js', '.ts', '.tsx', '.json']; + const entryExtension = extensions.find((ext) => + Fs.existsSync(Path.resolve(bundle.contextDir, bundle.entry) + ext) + ); + const commonConfig: webpack.Configuration = { node: { fs: 'empty' }, context: bundle.contextDir, cache: true, entry: { - [bundle.id]: bundle.entry, + [bundle.id]: `${bundle.entry}${entryExtension}`, }, devtool: worker.dist ? false : '#cheap-source-map', @@ -144,7 +150,7 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { rules: [ { - include: Path.join(bundle.contextDir, bundle.entry), + include: [`${Path.resolve(bundle.contextDir, bundle.entry)}${entryExtension}`], loader: UiSharedDeps.publicPathLoader, options: { key: bundle.id, @@ -292,7 +298,7 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { }, resolve: { - extensions: ['.js', '.ts', '.tsx', '.json'], + extensions, mainFields: ['browser', 'main'], alias: { tinymath: require.resolve('tinymath/lib/tinymath.es5.js'), diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index baaac3d8d4a86..21fff4d85ece6 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -34812,10 +34812,11 @@ const makeError = ({ const prefix = getErrorPrefix({timedOut, timeout, errorCode, signal, signalDescription, exitCode, isCanceled}); const execaMessage = `Command ${prefix}: ${command}`; - const shortMessage = error instanceof Error ? `${execaMessage}\n${error.message}` : execaMessage; + const isError = Object.prototype.toString.call(error) === '[object Error]'; + const shortMessage = isError ? `${execaMessage}\n${error.message}` : execaMessage; const message = [shortMessage, stderr, stdout].filter(Boolean).join('\n'); - if (error instanceof Error) { + if (isError) { error.originalMessage = error.message; error.message = message; } else { @@ -36263,25 +36264,24 @@ module.exports = function (/*streams...*/) { "use strict"; -const mergePromiseProperty = (spawned, promise, property) => { - // Starting the main `promise` is deferred to avoid consuming streams - const value = typeof promise === 'function' ? - (...args) => promise()[property](...args) : - promise[property].bind(promise); - Object.defineProperty(spawned, property, { - value, - writable: true, - enumerable: false, - configurable: true - }); -}; +const nativePromisePrototype = (async () => {})().constructor.prototype; +const descriptors = ['then', 'catch', 'finally'].map(property => [ + property, + Reflect.getOwnPropertyDescriptor(nativePromisePrototype, property) +]); // The return value is a mixin of `childProcess` and `Promise` const mergePromise = (spawned, promise) => { - mergePromiseProperty(spawned, promise, 'then'); - mergePromiseProperty(spawned, promise, 'catch'); - mergePromiseProperty(spawned, promise, 'finally'); + for (const [property, descriptor] of descriptors) { + // Starting the main `promise` is deferred to avoid consuming streams + const value = typeof promise === 'function' ? + (...args) => Reflect.apply(descriptor.value, promise(), args) : + descriptor.value.bind(promise); + + Reflect.defineProperty(spawned, property, {...descriptor, value}); + } + return spawned; }; diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index d34fe3624ab26..4e6bec92a65e4 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -35,6 +35,8 @@ "devDependencies": { "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0", + "loader-utils": "^1.2.3", + "val-loader": "^1.1.1", "css-loader": "^3.4.2", "del": "^5.1.0", "webpack": "^4.41.5" diff --git a/packages/kbn-ui-shared-deps/public_path_loader.js b/packages/kbn-ui-shared-deps/public_path_loader.js index fceebd6d6b3a1..cdebdcb4f0422 100644 --- a/packages/kbn-ui-shared-deps/public_path_loader.js +++ b/packages/kbn-ui-shared-deps/public_path_loader.js @@ -17,7 +17,15 @@ * under the License. */ +const Qs = require('querystring'); +const { stringifyRequest } = require('loader-utils'); + +const VAL_LOADER = require.resolve('val-loader'); +const MODULE_CREATOR = require.resolve('./public_path_module_creator'); + module.exports = function (source) { const options = this.query; - return `__webpack_public_path__ = window.__kbnPublicPath__['${options.key}'];${source}`; + const valOpts = Qs.stringify({ key: options.key }); + const req = `${VAL_LOADER}?${valOpts}!${MODULE_CREATOR}`; + return `import ${stringifyRequest(this, req)};${source}`; }; diff --git a/packages/kbn-ui-shared-deps/public_path_module_creator.js b/packages/kbn-ui-shared-deps/public_path_module_creator.js new file mode 100644 index 0000000000000..1cb9989432178 --- /dev/null +++ b/packages/kbn-ui-shared-deps/public_path_module_creator.js @@ -0,0 +1,24 @@ +/* + * 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. + */ + +module.exports = function ({ key }) { + return { + code: `__webpack_public_path__ = window.__kbnPublicPath__['${key}']`, + }; +}; diff --git a/rfcs/text/0003_handler_interface.md b/rfcs/text/0003_handler_interface.md index 51e78cf7c9f54..197f2a2012376 100644 --- a/rfcs/text/0003_handler_interface.md +++ b/rfcs/text/0003_handler_interface.md @@ -65,7 +65,7 @@ application.registerApp({ }); // Alerting -alerting.registerType({ +alerts.registerType({ id: 'myAlert', async execute(context, params, state) { const indexPatterns = await context.core.savedObjects.find('indexPattern'); diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index fc7e78f209022..67cd43f0647e4 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -36,7 +36,7 @@ import { ChromeDocTitle, DocTitleService } from './doc_title'; import { ChromeNavControls, NavControlsService } from './nav_controls'; import { ChromeNavLinks, NavLinksService } from './nav_links'; import { ChromeRecentlyAccessed, RecentlyAccessedService } from './recently_accessed'; -import { Header, LoadingIndicator } from './ui'; +import { Header } from './ui'; import { NavType } from './ui/header'; import { ChromeHelpExtensionMenuLink } from './ui/header/header_help_menu'; export { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle }; @@ -214,31 +214,29 @@ export class ChromeService { docTitle, getHeaderComponent: () => ( - - -
- +
), setAppTitle: (appTitle: string) => appTitle$.next(appTitle), diff --git a/src/core/public/chrome/nav_links/nav_link.ts b/src/core/public/chrome/nav_links/nav_link.ts index fb2972735c2b7..55b5c80526bab 100644 --- a/src/core/public/chrome/nav_links/nav_link.ts +++ b/src/core/public/chrome/nav_links/nav_link.ts @@ -62,7 +62,7 @@ export interface ChromeNavLink { /** * A EUI iconType that will be used for the app's icon. This icon - * takes precendence over the `icon` property. + * takes precedence over the `icon` property. */ readonly euiIconType?: string; @@ -72,6 +72,14 @@ export interface ChromeNavLink { */ readonly icon?: string; + /** + * Settled state between `url`, `baseUrl`, and `active` + * + * @internalRemarks + * This should be required once legacy apps are gone. + */ + readonly href?: string; + /** LEGACY FIELDS */ /** @@ -144,7 +152,7 @@ export interface ChromeNavLink { /** @public */ export type ChromeNavLinkUpdateableFields = Partial< - Pick + Pick >; export class NavLinkWrapper { @@ -162,7 +170,7 @@ export class NavLinkWrapper { public update(newProps: ChromeNavLinkUpdateableFields) { // Enforce limited properties at runtime for JS code - newProps = pick(newProps, ['active', 'disabled', 'hidden', 'url', 'subUrlBase']); + newProps = pick(newProps, ['active', 'disabled', 'hidden', 'url', 'subUrlBase', 'href']); return new NavLinkWrapper({ ...this.properties, ...newProps }); } } diff --git a/src/core/public/chrome/nav_links/to_nav_link.ts b/src/core/public/chrome/nav_links/to_nav_link.ts index 21814f5328dbf..b8f97f9ddc005 100644 --- a/src/core/public/chrome/nav_links/to_nav_link.ts +++ b/src/core/public/chrome/nav_links/to_nav_link.ts @@ -27,7 +27,12 @@ export function toNavLink( basePath: IBasePath ): NavLinkWrapper { const useAppStatus = app.navLinkStatus === AppNavLinkStatus.default; - const baseUrl = isLegacyApp(app) ? basePath.prepend(app.appUrl) : basePath.prepend(app.appRoute!); + const relativeBaseUrl = isLegacyApp(app) + ? basePath.prepend(app.appUrl) + : basePath.prepend(app.appRoute!); + const url = relativeToAbsolute(appendAppPath(relativeBaseUrl, app.defaultPath)); + const baseUrl = relativeToAbsolute(relativeBaseUrl); + return new NavLinkWrapper({ ...app, hidden: useAppStatus @@ -35,17 +40,27 @@ export function toNavLink( : app.navLinkStatus === AppNavLinkStatus.hidden, disabled: useAppStatus ? false : app.navLinkStatus === AppNavLinkStatus.disabled, legacy: isLegacyApp(app), - baseUrl: relativeToAbsolute(baseUrl), + baseUrl, ...(isLegacyApp(app) - ? {} + ? { + href: url && !url.startsWith(app.subUrlBase!) ? url : baseUrl, + } : { - url: relativeToAbsolute(appendAppPath(baseUrl, app.defaultPath)), + href: url, + url, }), }); } -function relativeToAbsolute(url: string) { - // convert all link urls to absolute urls +/** + * @param {string} url - a relative or root relative url. If a relative path is given then the + * absolute url returned will depend on the current page where this function is called from. For example + * if you are on page "http://www.mysite.com/shopping/kids" and you pass this function "adults", you would get + * back "http://www.mysite.com/shopping/adults". If you passed this function a root relative path, or one that + * starts with a "/", for example "/account/cart", you would get back "http://www.mysite.com/account/cart". + * @return {string} the relative url transformed into an absolute url + */ +export function relativeToAbsolute(url: string) { const a = document.createElement('a'); a.setAttribute('href', url); return a.href; diff --git a/src/core/public/chrome/recently_accessed/recently_accessed_service.ts b/src/core/public/chrome/recently_accessed/recently_accessed_service.ts index 27dbc288d18cb..86c7f3a1ef765 100644 --- a/src/core/public/chrome/recently_accessed/recently_accessed_service.ts +++ b/src/core/public/chrome/recently_accessed/recently_accessed_service.ts @@ -76,7 +76,7 @@ export interface ChromeRecentlyAccessed { * * @param link a relative URL to the resource (not including the {@link HttpStart.basePath | `http.basePath`}) * @param label the label to display in the UI - * @param id a unique string used to de-duplicate the recently accessed llist. + * @param id a unique string used to de-duplicate the recently accessed list. */ add(link: string, label: string, id: string): void; diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 866ea5f45d986..f5b17f8d214e9 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -2,140 +2,295 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
@@ -376,7 +531,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` aria-label="recent 2" class="euiListGroupItem__button" data-test-subj="collapsibleNavAppLink--recent" - href="recent 2" + href="http://localhost/recent%202" rel="noreferrer" title="recent 2" > @@ -465,7 +620,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` style="max-width: none;" >
  • @@ -1023,7 +1179,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` aria-label="recent 2" class="euiListGroupItem__button" data-test-subj="collapsibleNavAppLink--recent" - href="recent 2" + href="http://localhost/recent%202" rel="noreferrer" title="recent 2" > @@ -1112,7 +1268,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` style="max-width: none;" >
  • + +
    +
    +
    + +
  • +`; + +exports[`Header renders 2`] = ` +
    + +
    +
    +
    + +
    + +
    + +
    + +
    + + + +
    +
    + +
    + + + + + + + + + + +
    + +
    + + + + + + } + closePopover={[Function]} + data-test-subj="helpMenuButton" + display="inlineBlock" + hasArrow={true} + id="headerHelpMenu" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + repositionOnScroll={true} + > + +
    +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + + + + +
    +
    +`; + +exports[`Header renders 3`] = ` +
    + +
    +
    +
    + +
    + +
    + +
    + +
    + + + +
    +
    + +
    + + + + +
    + + + + +
    + + +
    + + + + + + + + + + +
    + +
    + + + + + + } + closePopover={[Function]} + data-test-subj="helpMenuButton" + display="inlineBlock" + hasArrow={true} + id="headerHelpMenu" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + repositionOnScroll={true} + > + +
    +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + + + + + + + +
    + +
    +
    +
    + + +
    + } + onActivation={[Function]} + onDeactivation={[Function]} + persistentFocus={false} + > + + +
    + } + onActivation={[Function]} + onDeactivation={[Function]} + persistentFocus={false} + /> + + +
    +
    + +
    + + + + + +
    +
    +`; + +exports[`Header renders 4`] = ` +
    + +
    +
    +
    + +
    + +
    + +
    + + +
    + + + +
    +
    +
    + +
    + + + + +
    + + + + +
    + + +
    + + + + + + + + + + +
    + +
    + + + + + + } + closePopover={[Function]} + data-test-subj="helpMenuButton" + display="inlineBlock" + hasArrow={true} + id="headerHelpMenu" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + repositionOnScroll={true} + > + +
    +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + + + + + + + + +
    +
    +`; diff --git a/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap index d089019915686..fdaa17c279a10 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap @@ -5,6 +5,7 @@ exports[`HeaderBreadcrumbs renders updates to the breadcrumbs$ observable 1`] = aria-current="page" className="euiBreadcrumb euiBreadcrumb--last" data-test-subj="breadcrumb first last" + title="First" > First @@ -39,6 +40,7 @@ Array [ aria-current="page" className="euiBreadcrumb euiBreadcrumb--last" data-test-subj="breadcrumb last" + title="Second" > Second , diff --git a/src/core/public/chrome/ui/header/_index.scss b/src/core/public/chrome/ui/header/_index.scss index 1b0438d748ff0..5c5e7f18b60a4 100644 --- a/src/core/public/chrome/ui/header/_index.scss +++ b/src/core/public/chrome/ui/header/_index.scss @@ -1,5 +1,3 @@ -@import './collapsible_nav'; - // TODO #64541 // Delete this block .chrHeaderWrapper:not(.headerWrapper) { diff --git a/src/core/public/chrome/ui/header/_collapsible_nav.scss b/src/core/public/chrome/ui/header/collapsible_nav.scss similarity index 100% rename from src/core/public/chrome/ui/header/_collapsible_nav.scss rename to src/core/public/chrome/ui/header/collapsible_nav.scss diff --git a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx index 527f0df598c7c..5a734d55445a2 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx @@ -19,11 +19,13 @@ import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; +import { BehaviorSubject } from 'rxjs'; import sinon from 'sinon'; -import { CollapsibleNav } from './collapsible_nav'; -import { DEFAULT_APP_CATEGORIES } from '../../..'; import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; -import { NavLink, RecentNavLink } from './nav_link'; +import { ChromeNavLink, DEFAULT_APP_CATEGORIES } from '../../..'; +import { httpServiceMock } from '../../../http/http_service.mock'; +import { ChromeRecentlyAccessedHistoryItem } from '../../recently_accessed'; +import { CollapsibleNav } from './collapsible_nav'; jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ htmlIdGenerator: () => () => 'mockId', @@ -31,40 +33,42 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ const { kibana, observability, security, management } = DEFAULT_APP_CATEGORIES; -function mockLink({ label = 'discover', category, onClick }: Partial) { +function mockLink({ title = 'discover', category }: Partial) { return { - key: label, - label, - href: label, - isActive: true, - onClick: onClick || (() => {}), + title, category, - 'data-test-subj': label, + id: title, + href: title, + baseUrl: '/', + legacy: false, + isActive: true, + 'data-test-subj': title, }; } -function mockRecentNavLink({ label = 'recent', onClick }: Partial) { +function mockRecentNavLink({ label = 'recent' }: Partial) { return { - href: label, label, - title: label, - 'aria-label': label, - onClick, + link: label, + id: label, }; } function mockProps() { return { - id: 'collapsible-nav', - homeHref: '/', + appId$: new BehaviorSubject('test'), + basePath: httpServiceMock.createSetupContract({ basePath: '/test' }).basePath, + id: 'collapsibe-nav', isLocked: false, isOpen: false, - navLinks: [], - recentNavLinks: [], + homeHref: '/', + legacyMode: false, + navLinks$: new BehaviorSubject([]), + recentlyAccessed$: new BehaviorSubject([]), storage: new StubBrowserStorage(), - onIsOpenUpdate: () => {}, onIsLockedUpdate: () => {}, - navigateToApp: () => {}, + closeNav: () => {}, + navigateToApp: () => Promise.resolve(), }; } @@ -103,14 +107,14 @@ describe('CollapsibleNav', () => { it('renders links grouped by category', () => { // just a test of category functionality, categories are not accurate const navLinks = [ - mockLink({ label: 'discover', category: kibana }), - mockLink({ label: 'siem', category: security }), - mockLink({ label: 'metrics', category: observability }), - mockLink({ label: 'monitoring', category: management }), - mockLink({ label: 'visualize', category: kibana }), - mockLink({ label: 'dashboard', category: kibana }), - mockLink({ label: 'canvas' }), // links should be able to be rendered top level as well - mockLink({ label: 'logs', category: observability }), + mockLink({ title: 'discover', category: kibana }), + mockLink({ title: 'siem', category: security }), + mockLink({ title: 'metrics', category: observability }), + mockLink({ title: 'monitoring', category: management }), + mockLink({ title: 'visualize', category: kibana }), + mockLink({ title: 'dashboard', category: kibana }), + mockLink({ title: 'canvas' }), // links should be able to be rendered top level as well + mockLink({ title: 'logs', category: observability }), ]; const recentNavLinks = [ mockRecentNavLink({ label: 'recent 1' }), @@ -120,8 +124,8 @@ describe('CollapsibleNav', () => { ); expect(component).toMatchSnapshot(); @@ -134,8 +138,8 @@ describe('CollapsibleNav', () => { ); expectShownNavLinksCount(component, 3); @@ -149,32 +153,34 @@ describe('CollapsibleNav', () => { }); it('closes the nav after clicking a link', () => { - const onClick = sinon.spy(); - const onIsOpenUpdate = sinon.spy(); - const navLinks = [mockLink({ category: kibana, onClick })]; - const recentNavLinks = [mockRecentNavLink({ onClick })]; + const onClose = sinon.spy(); + const navLinks = [mockLink({ category: kibana }), mockLink({ title: 'categoryless' })]; + const recentNavLinks = [mockRecentNavLink({})]; const component = mount( ); component.setProps({ - onIsOpenUpdate: (isOpen: boolean) => { - component.setProps({ isOpen }); - onIsOpenUpdate(); + closeNav: () => { + component.setProps({ isOpen: false }); + onClose(); }, }); component.find('[data-test-subj="collapsibleNavGroup-recentlyViewed"] a').simulate('click'); - expect(onClick.callCount).toEqual(1); - expect(onIsOpenUpdate.callCount).toEqual(1); + expect(onClose.callCount).toEqual(1); expectNavIsClosed(component); component.setProps({ isOpen: true }); component.find('[data-test-subj="collapsibleNavGroup-kibana"] a').simulate('click'); - expect(onClick.callCount).toEqual(2); - expect(onIsOpenUpdate.callCount).toEqual(2); + expect(onClose.callCount).toEqual(2); + expectNavIsClosed(component); + component.setProps({ isOpen: true }); + component.find('[data-test-subj="collapsibleNavGroup-noCategory"] a').simulate('click'); + expect(onClose.callCount).toEqual(3); + expectNavIsClosed(component); }); }); diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx index 8bca42db23517..9494e22920de8 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -17,6 +17,7 @@ * under the License. */ +import './collapsible_nav.scss'; import { EuiCollapsibleNav, EuiCollapsibleNavGroup, @@ -30,11 +31,16 @@ import { import { i18n } from '@kbn/i18n'; import { groupBy, sortBy } from 'lodash'; import React, { useRef } from 'react'; +import { useObservable } from 'react-use'; +import * as Rx from 'rxjs'; +import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem } from '../..'; import { AppCategory } from '../../../../types'; +import { InternalApplicationStart } from '../../../application/types'; +import { HttpStart } from '../../../http'; import { OnIsLockedUpdate } from './'; -import { NavLink, RecentNavLink } from './nav_link'; +import { createEuiListItem, createRecentNavLink } from './nav_link'; -function getAllCategories(allCategorizedLinks: Record) { +function getAllCategories(allCategorizedLinks: Record) { const allCategories = {} as Record; for (const [key, value] of Object.entries(allCategorizedLinks)) { @@ -45,7 +51,7 @@ function getAllCategories(allCategorizedLinks: Record) { } function getOrderedCategories( - mainCategories: Record, + mainCategories: Record, categoryDictionary: ReturnType ) { return sortBy( @@ -69,35 +75,53 @@ function setIsCategoryOpen(id: string, isOpen: boolean, storage: Storage) { } interface Props { + appId$: InternalApplicationStart['currentAppId$']; + basePath: HttpStart['basePath']; + id: string; isLocked: boolean; isOpen: boolean; - navLinks: NavLink[]; - recentNavLinks: RecentNavLink[]; homeHref: string; - id: string; + legacyMode: boolean; + navLinks$: Rx.Observable; + recentlyAccessed$: Rx.Observable; storage?: Storage; onIsLockedUpdate: OnIsLockedUpdate; - onIsOpenUpdate: (isOpen?: boolean) => void; - navigateToApp: (appId: string) => void; + closeNav: () => void; + navigateToApp: InternalApplicationStart['navigateToApp']; } export function CollapsibleNav({ + basePath, + id, isLocked, isOpen, - navLinks, - recentNavLinks, - onIsLockedUpdate, - onIsOpenUpdate, homeHref, - id, - navigateToApp, + legacyMode, storage = window.localStorage, + onIsLockedUpdate, + closeNav, + navigateToApp, + ...observables }: Props) { + const navLinks = useObservable(observables.navLinks$, []).filter((link) => !link.hidden); + const recentlyAccessed = useObservable(observables.recentlyAccessed$, []); + const appId = useObservable(observables.appId$, ''); const lockRef = useRef(null); const groupedNavLinks = groupBy(navLinks, (link) => link?.category?.id); const { undefined: unknowns = [], ...allCategorizedLinks } = groupedNavLinks; const categoryDictionary = getAllCategories(allCategorizedLinks); const orderedCategories = getOrderedCategories(allCategorizedLinks, categoryDictionary); + const readyForEUI = (link: ChromeNavLink, needsIcon: boolean = false) => { + return createEuiListItem({ + link, + legacyMode, + appId, + dataTestSubj: 'collapsibleNavAppLink', + navigateToApp, + onClick: closeNav, + ...(needsIcon && { basePath }), + }); + }; return ( {/* Pinned items */} @@ -127,7 +151,7 @@ export function CollapsibleNav({ iconType: 'home', href: homeHref, onClick: (event: React.MouseEvent) => { - onIsOpenUpdate(false); + closeNav(); if ( event.isDefaultPrevented() || event.altKey || @@ -159,21 +183,22 @@ export function CollapsibleNav({ onToggle={(isCategoryOpen) => setIsCategoryOpen('recentlyViewed', isCategoryOpen, storage)} data-test-subj="collapsibleNavGroup-recentlyViewed" > - {recentNavLinks.length > 0 ? ( + {recentlyAccessed.length > 0 ? ( {}, ...link }) => ({ - 'data-test-subj': 'collapsibleNavAppLink--recent', - onClick: (e: React.MouseEvent) => { - onIsOpenUpdate(false); - onClick(e); - }, - ...link, - }))} + listItems={recentlyAccessed.map((link) => { + // TODO #64541 + // Can remove icon from recent links completely + const { iconType, ...hydratedLink } = createRecentNavLink(link, navLinks, basePath); + + return { + ...hydratedLink, + 'data-test-subj': 'collapsibleNavAppLink--recent', + onClick: closeNav, + }; + })} maxWidth="none" color="subdued" gutterSize="none" @@ -195,21 +220,8 @@ export function CollapsibleNav({ {/* Kibana, Observability, Security, and Management sections */} - {orderedCategories.map((categoryName, i) => { + {orderedCategories.map((categoryName) => { const category = categoryDictionary[categoryName]!; - const links = allCategorizedLinks[categoryName].map( - ({ label, href, isActive, isDisabled, onClick }) => ({ - label, - href, - isActive, - isDisabled, - 'data-test-subj': 'collapsibleNavAppLink', - onClick: (e: React.MouseEvent) => { - onIsOpenUpdate(false); - onClick(e); - }, - }) - ); return ( readyForEUI(link))} maxWidth="none" color="subdued" gutterSize="none" @@ -237,23 +249,10 @@ export function CollapsibleNav({ })} {/* Things with no category (largely for custom plugins) */} - {unknowns.map(({ label, href, icon, isActive, isDisabled, onClick }, i) => ( - + {unknowns.map((link, i) => ( + - ) => { - onIsOpenUpdate(false); - onClick(e); - }} - /> + ))} diff --git a/src/core/public/chrome/ui/header/header.test.tsx b/src/core/public/chrome/ui/header/header.test.tsx new file mode 100644 index 0000000000000..13e1f6f086ae2 --- /dev/null +++ b/src/core/public/chrome/ui/header/header.test.tsx @@ -0,0 +1,106 @@ +/* + * 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 React from 'react'; +import { act } from 'react-dom/test-utils'; +import { BehaviorSubject } from 'rxjs'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { NavType } from '.'; +import { httpServiceMock } from '../../../http/http_service.mock'; +import { applicationServiceMock } from '../../../mocks'; +import { Header } from './header'; +import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + htmlIdGenerator: () => () => 'mockId', +})); + +function mockProps() { + const http = httpServiceMock.createSetupContract({ basePath: '/test' }); + const application = applicationServiceMock.createInternalStartContract(); + + return { + application, + kibanaVersion: '1.0.0', + appTitle$: new BehaviorSubject('test'), + badge$: new BehaviorSubject(undefined), + breadcrumbs$: new BehaviorSubject([]), + homeHref: '/', + isVisible$: new BehaviorSubject(true), + kibanaDocLink: '/docs', + navLinks$: new BehaviorSubject([]), + recentlyAccessed$: new BehaviorSubject([]), + forceAppSwitcherNavigation$: new BehaviorSubject(false), + helpExtension$: new BehaviorSubject(undefined), + helpSupportUrl$: new BehaviorSubject(''), + legacyMode: false, + navControlsLeft$: new BehaviorSubject([]), + navControlsRight$: new BehaviorSubject([]), + basePath: http.basePath, + isLocked$: new BehaviorSubject(false), + navType$: new BehaviorSubject('modern' as NavType), + loadingCount$: new BehaviorSubject(0), + onIsLockedUpdate: () => {}, + }; +} + +describe('Header', () => { + beforeAll(() => { + Object.defineProperty(window, 'localStorage', { + value: new StubBrowserStorage(), + }); + }); + + it('renders', () => { + const isVisible$ = new BehaviorSubject(false); + const breadcrumbs$ = new BehaviorSubject([{ text: 'test' }]); + const isLocked$ = new BehaviorSubject(false); + const navType$ = new BehaviorSubject('modern' as NavType); + const navLinks$ = new BehaviorSubject([ + { id: 'kibana', title: 'kibana', baseUrl: '', legacy: false }, + ]); + const recentlyAccessed$ = new BehaviorSubject([ + { link: '', label: 'dashboard', id: 'dashboard' }, + ]); + const component = mountWithIntl( +
    + ); + expect(component).toMatchSnapshot(); + + act(() => isVisible$.next(true)); + component.update(); + expect(component).toMatchSnapshot(); + + act(() => isLocked$.next(true)); + component.update(); + expect(component).toMatchSnapshot(); + + act(() => navType$.next('legacy' as NavType)); + component.update(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 6280d68587355..d24b342e0386b 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -28,9 +28,11 @@ import { htmlIdGenerator, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { Component, createRef } from 'react'; import classnames from 'classnames'; -import * as Rx from 'rxjs'; +import React, { createRef, useState } from 'react'; +import { useObservable } from 'react-use'; +import { Observable } from 'rxjs'; +import { LoadingIndicator } from '../'; import { ChromeBadge, ChromeBreadcrumb, @@ -41,192 +43,101 @@ import { import { InternalApplicationStart } from '../../../application/types'; import { HttpStart } from '../../../http'; import { ChromeHelpExtension } from '../../chrome_service'; -import { HeaderBadge } from './header_badge'; import { NavType, OnIsLockedUpdate } from './'; +import { CollapsibleNav } from './collapsible_nav'; +import { HeaderBadge } from './header_badge'; import { HeaderBreadcrumbs } from './header_breadcrumbs'; import { HeaderHelpMenu } from './header_help_menu'; -import { HeaderNavControls } from './header_nav_controls'; -import { createNavLink, createRecentNavLink } from './nav_link'; import { HeaderLogo } from './header_logo'; +import { HeaderNavControls } from './header_nav_controls'; import { NavDrawer } from './nav_drawer'; -import { CollapsibleNav } from './collapsible_nav'; export interface HeaderProps { kibanaVersion: string; application: InternalApplicationStart; - appTitle$: Rx.Observable; - badge$: Rx.Observable; - breadcrumbs$: Rx.Observable; + appTitle$: Observable; + badge$: Observable; + breadcrumbs$: Observable; homeHref: string; - isVisible$: Rx.Observable; + isVisible$: Observable; kibanaDocLink: string; - navLinks$: Rx.Observable; - recentlyAccessed$: Rx.Observable; - forceAppSwitcherNavigation$: Rx.Observable; - helpExtension$: Rx.Observable; - helpSupportUrl$: Rx.Observable; + navLinks$: Observable; + recentlyAccessed$: Observable; + forceAppSwitcherNavigation$: Observable; + helpExtension$: Observable; + helpSupportUrl$: Observable; legacyMode: boolean; - navControlsLeft$: Rx.Observable; - navControlsRight$: Rx.Observable; + navControlsLeft$: Observable; + navControlsRight$: Observable; basePath: HttpStart['basePath']; - isLocked$: Rx.Observable; - navType$: Rx.Observable; + isLocked$: Observable; + navType$: Observable; + loadingCount$: ReturnType; onIsLockedUpdate: OnIsLockedUpdate; } -interface State { - appTitle: string; - isVisible: boolean; - navLinks: ChromeNavLink[]; - recentlyAccessed: ChromeRecentlyAccessedHistoryItem[]; - forceNavigation: boolean; - navControlsLeft: readonly ChromeNavControl[]; - navControlsRight: readonly ChromeNavControl[]; - currentAppId: string | undefined; - isLocked: boolean; - navType: NavType; - isOpen: boolean; +function renderMenuTrigger(toggleOpen: () => void) { + return ( + + + + ); } -export class Header extends Component { - private subscription?: Rx.Subscription; - private navDrawerRef = createRef(); - private toggleCollapsibleNavRef = createRef(); - - constructor(props: HeaderProps) { - super(props); - - let isLocked = false; - props.isLocked$.subscribe((initialIsLocked) => (isLocked = initialIsLocked)); - - this.state = { - appTitle: 'Kibana', - isVisible: true, - navLinks: [], - recentlyAccessed: [], - forceNavigation: false, - navControlsLeft: [], - navControlsRight: [], - currentAppId: '', - isLocked, - navType: 'modern', - isOpen: false, - }; +export function Header({ + kibanaVersion, + kibanaDocLink, + legacyMode, + application, + basePath, + onIsLockedUpdate, + homeHref, + ...observables +}: HeaderProps) { + const isVisible = useObservable(observables.isVisible$, true); + const navType = useObservable(observables.navType$, 'modern'); + const isLocked = useObservable(observables.isLocked$, false); + const [isOpen, setIsOpen] = useState(false); + + if (!isVisible) { + return ; } - public componentDidMount() { - this.subscription = Rx.combineLatest( - this.props.appTitle$, - this.props.isVisible$, - this.props.forceAppSwitcherNavigation$, - this.props.navLinks$, - this.props.recentlyAccessed$, - // Types for combineLatest only handle up to 6 inferred types so we combine these separately. - Rx.combineLatest( - this.props.navControlsLeft$, - this.props.navControlsRight$, - this.props.application.currentAppId$, - this.props.isLocked$, - this.props.navType$ - ) - ).subscribe({ - next: ([ - appTitle, - isVisible, - forceNavigation, - navLinks, - recentlyAccessed, - [navControlsLeft, navControlsRight, currentAppId, isLocked, navType], - ]) => { - this.setState({ - appTitle, - isVisible, - forceNavigation, - navLinks: navLinks.filter((navLink) => !navLink.hidden), - recentlyAccessed, - navControlsLeft, - navControlsRight, - currentAppId, - isLocked, - navType, - }); - }, - }); - } - - public componentWillUnmount() { - if (this.subscription) { - this.subscription.unsubscribe(); + const navDrawerRef = createRef(); + const toggleCollapsibleNavRef = createRef(); + const navId = htmlIdGenerator()(); + const className = classnames( + 'chrHeaderWrapper', // TODO #64541 - delete this + 'hide-for-sharing', + { + 'chrHeaderWrapper--navIsLocked': isLocked, + headerWrapper: navType === 'modern', } - } - - public renderMenuTrigger() { - return ( - this.navDrawerRef.current?.toggleOpen()} - > - - - ); - } - - public render() { - const { appTitle, isVisible, navControlsLeft, navControlsRight } = this.state; - const { - badge$, - breadcrumbs$, - helpExtension$, - helpSupportUrl$, - kibanaDocLink, - kibanaVersion, - } = this.props; - const navLinks = this.state.navLinks.map((link) => - createNavLink( - link, - this.props.legacyMode, - this.state.currentAppId, - this.props.basePath, - this.props.application.navigateToApp - ) - ); - const recentNavLinks = this.state.recentlyAccessed.map((link) => - createRecentNavLink(link, this.state.navLinks, this.props.basePath) - ); + ); - if (!isVisible) { - return null; - } - - const className = classnames( - 'chrHeaderWrapper', // TODO #64541 - delete this - 'hide-for-sharing', - { - 'chrHeaderWrapper--navIsLocked': this.state.isLocked, - headerWrapper: this.state.navType === 'modern', - } - ); - const navId = htmlIdGenerator()(); - return ( + return ( + <> +
    - {this.state.navType === 'modern' ? ( + {navType === 'modern' ? ( { - this.setState({ isOpen: !this.state.isOpen }); - }} - aria-expanded={this.state.isOpen} - aria-pressed={this.state.isOpen} + onClick={() => setIsOpen(!isOpen)} + aria-expanded={isOpen} + aria-pressed={isOpen} aria-controls={navId} - ref={this.toggleCollapsibleNavRef} + ref={toggleCollapsibleNavRef} > @@ -236,71 +147,79 @@ export class Header extends Component { // Delete this block - {this.renderMenuTrigger()} + {renderMenuTrigger(() => navDrawerRef.current?.toggleOpen())} )} - + - + - + - + - {this.state.navType === 'modern' ? ( + {navType === 'modern' ? ( { - this.setState({ isOpen }); - if (this.toggleCollapsibleNavRef.current) { - this.toggleCollapsibleNavRef.current.focus(); + isLocked={isLocked} + navLinks$={observables.navLinks$} + recentlyAccessed$={observables.recentlyAccessed$} + isOpen={isOpen} + homeHref={homeHref} + basePath={basePath} + legacyMode={legacyMode} + navigateToApp={application.navigateToApp} + onIsLockedUpdate={onIsLockedUpdate} + closeNav={() => { + setIsOpen(false); + if (toggleCollapsibleNavRef.current) { + toggleCollapsibleNavRef.current.focus(); } }} - navigateToApp={this.props.application.navigateToApp} /> ) : ( // TODO #64541 // Delete this block )}
    - ); - } + + ); } diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx b/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx index 0398f162f9af9..7fe2c91087090 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.test.tsx @@ -19,26 +19,23 @@ import { mount } from 'enzyme'; import React from 'react'; -import * as Rx from 'rxjs'; - -import { ChromeBreadcrumb } from '../../chrome_service'; +import { act } from 'react-dom/test-utils'; +import { BehaviorSubject } from 'rxjs'; import { HeaderBreadcrumbs } from './header_breadcrumbs'; describe('HeaderBreadcrumbs', () => { it('renders updates to the breadcrumbs$ observable', () => { - const breadcrumbs$ = new Rx.Subject(); - const wrapper = mount(); - - breadcrumbs$.next([{ text: 'First' }]); - // Unfortunately, enzyme won't update the wrapper until we call update. - wrapper.update(); + const breadcrumbs$ = new BehaviorSubject([{ text: 'First' }]); + const wrapper = mount( + + ); expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot(); - breadcrumbs$.next([{ text: 'First' }, { text: 'Second' }]); + act(() => breadcrumbs$.next([{ text: 'First' }, { text: 'Second' }])); wrapper.update(); expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot(); - breadcrumbs$.next([]); + act(() => breadcrumbs$.next([])); wrapper.update(); expect(wrapper.find('.euiBreadcrumb')).toMatchSnapshot(); }); diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx index 54cfc7131cb2b..174c46981db53 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx @@ -17,88 +17,36 @@ * under the License. */ -import classNames from 'classnames'; -import React, { Component } from 'react'; -import * as Rx from 'rxjs'; - import { EuiHeaderBreadcrumbs } from '@elastic/eui'; +import classNames from 'classnames'; +import React from 'react'; +import { useObservable } from 'react-use'; +import { Observable } from 'rxjs'; import { ChromeBreadcrumb } from '../../chrome_service'; interface Props { - appTitle?: string; - breadcrumbs$: Rx.Observable; + appTitle$: Observable; + breadcrumbs$: Observable; } -interface State { - breadcrumbs: ChromeBreadcrumb[]; -} - -export class HeaderBreadcrumbs extends Component { - private subscription?: Rx.Subscription; - - constructor(props: Props) { - super(props); +export function HeaderBreadcrumbs({ appTitle$, breadcrumbs$ }: Props) { + const appTitle = useObservable(appTitle$, 'Kibana'); + const breadcrumbs = useObservable(breadcrumbs$, []); + let crumbs = breadcrumbs; - this.state = { breadcrumbs: [] }; + if (breadcrumbs.length === 0 && appTitle) { + crumbs = [{ text: appTitle }]; } - public componentDidMount() { - this.subscribe(); - } - - public componentDidUpdate(prevProps: Props) { - if (prevProps.breadcrumbs$ === this.props.breadcrumbs$) { - return; - } + crumbs = crumbs.map((breadcrumb, i) => ({ + ...breadcrumb, + 'data-test-subj': classNames( + 'breadcrumb', + breadcrumb['data-test-subj'], + i === 0 && 'first', + i === breadcrumbs.length - 1 && 'last' + ), + })); - this.unsubscribe(); - this.subscribe(); - } - - public componentWillUnmount() { - this.unsubscribe(); - } - - public render() { - return ( - - ); - } - - private subscribe() { - this.subscription = this.props.breadcrumbs$.subscribe((breadcrumbs) => { - this.setState({ - breadcrumbs, - }); - }); - } - - private unsubscribe() { - if (this.subscription) { - this.subscription.unsubscribe(); - delete this.subscription; - } - } - - private getBreadcrumbs() { - let breadcrumbs = this.state.breadcrumbs; - - if (breadcrumbs.length === 0 && this.props.appTitle) { - breadcrumbs = [{ text: this.props.appTitle }]; - } - - return breadcrumbs.map((breadcrumb, i) => ({ - ...breadcrumb, - 'data-test-subj': classNames( - 'breadcrumb', - breadcrumb['data-test-subj'], - i === 0 && 'first', - i === breadcrumbs.length - 1 && 'last' - ), - })); - } + return ; } diff --git a/src/core/public/chrome/ui/header/header_logo.tsx b/src/core/public/chrome/ui/header/header_logo.tsx index 147c7cf5dc4b1..9bec946b6b76e 100644 --- a/src/core/public/chrome/ui/header/header_logo.tsx +++ b/src/core/public/chrome/ui/header/header_logo.tsx @@ -17,11 +17,13 @@ * under the License. */ -import Url from 'url'; -import React from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiHeaderLogo } from '@elastic/eui'; -import { NavLink } from './nav_link'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useObservable } from 'react-use'; +import { Observable } from 'rxjs'; +import Url from 'url'; +import { ChromeNavLink } from '../..'; function findClosestAnchor(element: HTMLElement): HTMLAnchorElement | void { let current = element; @@ -41,7 +43,7 @@ function findClosestAnchor(element: HTMLElement): HTMLAnchorElement | void { function onClick( event: React.MouseEvent, forceNavigation: boolean, - navLinks: NavLink[], + navLinks: ChromeNavLink[], navigateToApp: (appId: string) => void ) { const anchor = findClosestAnchor((event as any).nativeEvent.target); @@ -50,7 +52,7 @@ function onClick( } const navLink = navLinks.find((item) => item.href === anchor.href); - if (navLink && navLink.isDisabled) { + if (navLink && navLink.disabled) { event.preventDefault(); return; } @@ -85,12 +87,15 @@ function onClick( interface Props { href: string; - navLinks: NavLink[]; - forceNavigation: boolean; + navLinks$: Observable; + forceNavigation$: Observable; navigateToApp: (appId: string) => void; } -export function HeaderLogo({ href, forceNavigation, navLinks, navigateToApp }: Props) { +export function HeaderLogo({ href, navigateToApp, ...observables }: Props) { + const forceNavigation = useObservable(observables.forceNavigation$, false); + const navLinks = useObservable(observables.navLinks$, []); + return ( ; side: 'left' | 'right'; } -export class HeaderNavControls extends Component { - public render() { - const { navControls } = this.props; - - if (!navControls) { - return null; - } +export function HeaderNavControls({ navControls$, side }: Props) { + const navControls = useObservable(navControls$, []); - return navControls.map(this.renderNavControl); + if (!navControls) { + return null; } // It should be performant to use the index as the key since these are unlikely // to change while Kibana is running. - private renderNavControl = (navControl: ChromeNavControl, index: number) => ( - - - + return ( + <> + {navControls.map((navControl: ChromeNavControl, index: number) => ( + + + + ))} + ); } diff --git a/src/core/public/chrome/ui/header/nav_drawer.tsx b/src/core/public/chrome/ui/header/nav_drawer.tsx index 17df8569f6307..ee4bff6cc0ac4 100644 --- a/src/core/public/chrome/ui/header/nav_drawer.tsx +++ b/src/core/public/chrome/ui/header/nav_drawer.tsx @@ -17,24 +17,39 @@ * under the License. */ -import React from 'react'; +import { EuiHorizontalRule, EuiNavDrawer, EuiNavDrawerGroup } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { EuiNavDrawer, EuiHorizontalRule, EuiNavDrawerGroup } from '@elastic/eui'; +import React from 'react'; +import { useObservable } from 'react-use'; +import { Observable } from 'rxjs'; +import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem, CoreStart } from '../../..'; +import { InternalApplicationStart } from '../../../application/types'; +import { HttpStart } from '../../../http'; import { OnIsLockedUpdate } from './'; -import { NavLink, RecentNavLink } from './nav_link'; +import { createEuiListItem, createRecentNavLink } from './nav_link'; import { RecentLinks } from './recent_links'; export interface Props { + appId$: InternalApplicationStart['currentAppId$']; + basePath: HttpStart['basePath']; isLocked?: boolean; + legacyMode: boolean; + navLinks$: Observable; + recentlyAccessed$: Observable; + navigateToApp: CoreStart['application']['navigateToApp']; onIsLockedUpdate?: OnIsLockedUpdate; - navLinks: NavLink[]; - recentNavLinks: RecentNavLink[]; } -function navDrawerRenderer( - { isLocked, onIsLockedUpdate, navLinks, recentNavLinks }: Props, +function NavDrawerRenderer( + { isLocked, onIsLockedUpdate, basePath, legacyMode, navigateToApp, ...observables }: Props, ref: React.Ref ) { + const appId = useObservable(observables.appId$, ''); + const navLinks = useObservable(observables.navLinks$, []).filter((link) => !link.hidden); + const recentNavLinks = useObservable(observables.recentlyAccessed$, []).map((link) => + createRecentNavLink(link, navLinks, basePath) + ); + return ( + createEuiListItem({ + link, + legacyMode, + appId, + basePath, + navigateToApp, + dataTestSubj: 'navDrawerAppsMenuLink', + }) + )} aria-label={i18n.translate('core.ui.primaryNavList.screenReaderLabel', { defaultMessage: 'Primary navigation links', })} @@ -58,4 +82,4 @@ function navDrawerRenderer( ); } -export const NavDrawer = React.forwardRef(navDrawerRenderer); +export const NavDrawer = React.forwardRef(NavDrawerRenderer); diff --git a/src/core/public/chrome/ui/header/nav_link.tsx b/src/core/public/chrome/ui/header/nav_link.tsx index c979bb8271e1b..c09b15fac9bdb 100644 --- a/src/core/public/chrome/ui/header/nav_link.tsx +++ b/src/core/public/chrome/ui/header/nav_link.tsx @@ -17,12 +17,12 @@ * under the License. */ -import React from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiImage } from '@elastic/eui'; -import { AppCategory } from 'src/core/types'; -import { ChromeNavLink, CoreStart, ChromeRecentlyAccessedHistoryItem } from '../../../'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem, CoreStart } from '../../..'; import { HttpStart } from '../../../http'; +import { relativeToAbsolute } from '../../nav_links/to_nav_link'; function isModifiedEvent(event: React.MouseEvent) { return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); @@ -32,62 +32,37 @@ function LinkIcon({ url }: { url: string }) { return ; } -export interface NavLink { - key: string; - label: string; - href: string; - isActive: boolean; - onClick(event: React.MouseEvent): void; - category?: AppCategory; - isDisabled?: boolean; - iconType?: string; - icon?: JSX.Element; - order?: number; - 'data-test-subj': string; +interface Props { + link: ChromeNavLink; + legacyMode: boolean; + appId: string | undefined; + basePath?: HttpStart['basePath']; + dataTestSubj: string; + onClick?: Function; + navigateToApp: CoreStart['application']['navigateToApp']; } -/** - * Create a link that's actually ready to be passed into EUI - * - * @param navLink - * @param legacyMode - * @param currentAppId - * @param basePath - * @param navigateToApp - */ -export function createNavLink( - navLink: ChromeNavLink, - legacyMode: boolean, - currentAppId: string | undefined, - basePath: HttpStart['basePath'], - navigateToApp: CoreStart['application']['navigateToApp'] -): NavLink { - const { - legacy, - url, - active, - baseUrl, - id, - title, - disabled, - euiIconType, - icon, - category, - order, - tooltip, - } = navLink; - let href = navLink.url ?? navLink.baseUrl; - - if (legacy) { - href = url && !active ? url : baseUrl; - } +// TODO #64541 +// Set return type to EuiListGroupItemProps +// Currently it's a subset of EuiListGroupItemProps+FlyoutMenuItem for CollapsibleNav and NavDrawer +// But FlyoutMenuItem isn't exported from EUI +export function createEuiListItem({ + link, + legacyMode, + appId, + basePath, + onClick = () => {}, + navigateToApp, + dataTestSubj, +}: Props) { + const { legacy, active, id, title, disabled, euiIconType, icon, tooltip, href } = link; return { - category, - key: id, label: tooltip ?? title, - href, // Use href and onClick to support "open in new tab" and SPA navigation in the same link - onClick(event) { + href, + /* Use href and onClick to support "open in new tab" and SPA navigation in the same link */ + onClick(event: React.MouseEvent) { + onClick(); if ( !legacyMode && // ignore when in legacy mode !legacy && // ignore links to legacy apps @@ -96,57 +71,31 @@ export function createNavLink( !isModifiedEvent(event) // ignore clicks with modifier keys ) { event.preventDefault(); - navigateToApp(navLink.id); + navigateToApp(id); } }, // Legacy apps use `active` property, NP apps should match the current app - isActive: active || currentAppId === id, + isActive: active || appId === id, isDisabled: disabled, - iconType: euiIconType, - icon: !euiIconType && icon ? : undefined, - order, - 'data-test-subj': 'navDrawerAppsMenuLink', + 'data-test-subj': dataTestSubj, + ...(basePath && { + iconType: euiIconType, + icon: !euiIconType && icon ? : undefined, + }), }; } -// Providing a buffer between the limit and the cut off index -// protects from truncating just the last couple (6) characters -const TRUNCATE_LIMIT: number = 64; -const TRUNCATE_AT: number = 58; - -function truncateRecentItemLabel(label: string): string { - if (label.length > TRUNCATE_LIMIT) { - label = `${label.substring(0, TRUNCATE_AT)}…`; - } - - return label; -} - -/** - * @param {string} url - a relative or root relative url. If a relative path is given then the - * absolute url returned will depend on the current page where this function is called from. For example - * if you are on page "http://www.mysite.com/shopping/kids" and you pass this function "adults", you would get - * back "http://www.mysite.com/shopping/adults". If you passed this function a root relative path, or one that - * starts with a "/", for example "/account/cart", you would get back "http://www.mysite.com/account/cart". - * @return {string} the relative url transformed into an absolute url - */ -function relativeToAbsolute(url: string) { - const a = document.createElement('a'); - a.setAttribute('href', url); - return a.href; -} - export interface RecentNavLink { href: string; label: string; title: string; 'aria-label': string; iconType?: string; - onClick?(event: React.MouseEvent): void; } /** * Add saved object type info to recently links + * TODO #64541 - set return type to EuiListGroupItemProps * * Recent nav links are similar to normal nav links but are missing some Kibana Platform magic and * because of legacy reasons have slightly different properties. @@ -176,7 +125,7 @@ export function createRecentNavLink( return { href, - label: truncateRecentItemLabel(label), + label, title: titleAndAriaLabel, 'aria-label': titleAndAriaLabel, iconType: navLink?.euiIconType, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 904e9d6721ebc..bae0f9a2281cf 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -287,6 +287,7 @@ export interface ChromeNavLink { readonly disableSubUrlTracking?: boolean; readonly euiIconType?: string; readonly hidden?: boolean; + readonly href?: string; readonly icon?: string; readonly id: string; // @internal @@ -315,7 +316,7 @@ export interface ChromeNavLinks { } // @public (undocumented) -export type ChromeNavLinkUpdateableFields = Partial>; +export type ChromeNavLinkUpdateableFields = Partial>; // @public export interface ChromeRecentlyAccessed { diff --git a/src/core/server/elasticsearch/elasticsearch_service.mock.ts b/src/core/server/elasticsearch/elasticsearch_service.mock.ts index a7d78b56ff3fd..55e60f5987604 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.mock.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.mock.ts @@ -23,12 +23,7 @@ import { IClusterClient, ICustomClusterClient } from './cluster_client'; import { IScopedClusterClient } from './scoped_cluster_client'; import { ElasticsearchConfig } from './elasticsearch_config'; import { ElasticsearchService } from './elasticsearch_service'; -import { - InternalElasticsearchServiceSetup, - ElasticsearchServiceSetup, - ElasticsearchServiceStart, - ElasticsearchStatusMeta, -} from './types'; +import { InternalElasticsearchServiceSetup, ElasticsearchStatusMeta } from './types'; import { NodesVersionCompatibility } from './version_check/ensure_es_version'; import { ServiceStatus, ServiceStatusLevels } from '../status'; @@ -51,32 +46,26 @@ function createClusterClientMock() { return client; } -type MockedElasticSearchServiceSetup = jest.Mocked< - ElasticsearchServiceSetup & { - adminClient: jest.Mocked; - dataClient: jest.Mocked; - } ->; +interface MockedElasticSearchServiceSetup { + legacy: { + createClient: jest.Mock; + client: jest.Mocked; + }; +} const createSetupContractMock = () => { const setupContract: MockedElasticSearchServiceSetup = { - createClient: jest.fn(), - adminClient: createClusterClientMock(), - dataClient: createClusterClientMock(), + legacy: { + createClient: jest.fn(), + client: createClusterClientMock(), + }, }; - setupContract.createClient.mockReturnValue(createCustomClusterClientMock()); - setupContract.adminClient.asScoped.mockReturnValue(createScopedClusterClientMock()); - setupContract.dataClient.asScoped.mockReturnValue(createScopedClusterClientMock()); + setupContract.legacy.createClient.mockReturnValue(createCustomClusterClientMock()); + setupContract.legacy.client.asScoped.mockReturnValue(createScopedClusterClientMock()); return setupContract; }; -type MockedElasticSearchServiceStart = { - legacy: jest.Mocked; -} & { - legacy: { - client: jest.Mocked; - }; -}; +type MockedElasticSearchServiceStart = MockedElasticSearchServiceSetup; const createStartContractMock = () => { const startContract: MockedElasticSearchServiceStart = { @@ -92,13 +81,11 @@ const createStartContractMock = () => { type MockedInternalElasticSearchServiceSetup = jest.Mocked< InternalElasticsearchServiceSetup & { - adminClient: jest.Mocked; - dataClient: jest.Mocked; + legacy: { client: jest.Mocked }; } >; const createInternalSetupContractMock = () => { const setupContract: MockedInternalElasticSearchServiceSetup = { - ...createSetupContractMock(), esNodesCompatibility$: new BehaviorSubject({ isCompatible: true, incompatibleNodes: [], @@ -111,10 +98,10 @@ const createInternalSetupContractMock = () => { }), legacy: { config$: new BehaviorSubject({} as ElasticsearchConfig), + ...createSetupContractMock().legacy, }, }; - setupContract.adminClient.asScoped.mockReturnValue(createScopedClusterClientMock()); - setupContract.dataClient.asScoped.mockReturnValue(createScopedClusterClientMock()); + setupContract.legacy.client.asScoped.mockReturnValue(createScopedClusterClientMock()); return setupContract; }; diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts index 26144eaaa4afa..e7dab3807733a 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts @@ -74,25 +74,16 @@ describe('#setup', () => { ); }); - it('returns data and admin client as a part of the contract', async () => { - const mockAdminClusterClientInstance = elasticsearchServiceMock.createClusterClient(); - const mockDataClusterClientInstance = elasticsearchServiceMock.createClusterClient(); - MockClusterClient.mockImplementationOnce( - () => mockAdminClusterClientInstance - ).mockImplementationOnce(() => mockDataClusterClientInstance); + it('returns elasticsearch client as a part of the contract', async () => { + const mockClusterClientInstance = elasticsearchServiceMock.createClusterClient(); + MockClusterClient.mockImplementationOnce(() => mockClusterClientInstance); const setupContract = await elasticsearchService.setup(deps); + const client = setupContract.legacy.client; - const adminClient = setupContract.adminClient; - const dataClient = setupContract.dataClient; - - expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); - await adminClient.callAsInternalUser('any'); - expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); - - expect(mockDataClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); - await dataClient.callAsInternalUser('any'); - expect(mockDataClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); + await client.callAsInternalUser('any'); + expect(mockClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); }); describe('#createClient', () => { @@ -103,7 +94,7 @@ describe('#setup', () => { MockClusterClient.mockImplementation(() => mockClusterClientInstance); const customConfig = { logQueries: true }; - const clusterClient = setupContract.createClient('some-custom-type', customConfig); + const clusterClient = setupContract.legacy.createClient('some-custom-type', customConfig); expect(clusterClient).toBe(mockClusterClientInstance); @@ -124,7 +115,7 @@ describe('#setup', () => { logQueries: true, ssl: { certificate: 'certificate-value' }, }; - setupContract.createClient('some-custom-type', customConfig); + setupContract.legacy.createClient('some-custom-type', customConfig); const config = MockClusterClient.mock.calls[0][0]; expect(config).toMatchInlineSnapshot(` @@ -149,7 +140,7 @@ describe('#setup', () => { // reset all mocks called during setup phase MockClusterClient.mockClear(); - setupContract.createClient('another-type'); + setupContract.legacy.createClient('another-type'); const config = MockClusterClient.mock.calls[0][0]; expect(config).toMatchInlineSnapshot(` @@ -195,7 +186,7 @@ describe('#setup', () => { logQueries: true, ssl: { certificate: 'certificate-value' }, }; - setupContract.createClient('some-custom-type', customConfig); + setupContract.legacy.createClient('some-custom-type', customConfig); const config = MockClusterClient.mock.calls[0][0]; expect(config).toMatchInlineSnapshot(` @@ -218,40 +209,34 @@ describe('#setup', () => { }); it('esNodeVersionCompatibility$ only starts polling when subscribed to', async (done) => { - const mockAdminClusterClientInstance = elasticsearchServiceMock.createClusterClient(); - const mockDataClusterClientInstance = elasticsearchServiceMock.createClusterClient(); - MockClusterClient.mockImplementationOnce( - () => mockAdminClusterClientInstance - ).mockImplementationOnce(() => mockDataClusterClientInstance); + const clusterClientInstance = elasticsearchServiceMock.createClusterClient(); + MockClusterClient.mockImplementationOnce(() => clusterClientInstance); - mockAdminClusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); + clusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); const setupContract = await elasticsearchService.setup(deps); await delay(10); - expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); + expect(clusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); setupContract.esNodesCompatibility$.subscribe(() => { - expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(clusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); done(); }); }); it('esNodeVersionCompatibility$ stops polling when unsubscribed from', async (done) => { - const mockAdminClusterClientInstance = elasticsearchServiceMock.createClusterClient(); - const mockDataClusterClientInstance = elasticsearchServiceMock.createClusterClient(); - MockClusterClient.mockImplementationOnce( - () => mockAdminClusterClientInstance - ).mockImplementationOnce(() => mockDataClusterClientInstance); + const mockClusterClientInstance = elasticsearchServiceMock.createClusterClient(); + MockClusterClient.mockImplementationOnce(() => mockClusterClientInstance); - mockAdminClusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); + mockClusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); const setupContract = await elasticsearchService.setup(deps); - expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); + expect(mockClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); const sub = setupContract.esNodesCompatibility$.subscribe(async () => { sub.unsubscribe(); await delay(100); - expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); done(); }); }); @@ -259,38 +244,31 @@ describe('#setup', () => { describe('#stop', () => { it('stops both admin and data clients', async () => { - const mockAdminClusterClientInstance = { close: jest.fn() }; - const mockDataClusterClientInstance = { close: jest.fn() }; - MockClusterClient.mockImplementationOnce( - () => mockAdminClusterClientInstance - ).mockImplementationOnce(() => mockDataClusterClientInstance); + const mockClusterClientInstance = { close: jest.fn() }; + MockClusterClient.mockImplementationOnce(() => mockClusterClientInstance); await elasticsearchService.setup(deps); await elasticsearchService.stop(); - expect(mockAdminClusterClientInstance.close).toHaveBeenCalledTimes(1); - expect(mockDataClusterClientInstance.close).toHaveBeenCalledTimes(1); + expect(mockClusterClientInstance.close).toHaveBeenCalledTimes(1); }); it('stops pollEsNodeVersions even if there are active subscriptions', async (done) => { expect.assertions(2); - const mockAdminClusterClientInstance = elasticsearchServiceMock.createCustomClusterClient(); - const mockDataClusterClientInstance = elasticsearchServiceMock.createCustomClusterClient(); + const mockClusterClientInstance = elasticsearchServiceMock.createCustomClusterClient(); - MockClusterClient.mockImplementationOnce( - () => mockAdminClusterClientInstance - ).mockImplementationOnce(() => mockDataClusterClientInstance); + MockClusterClient.mockImplementationOnce(() => mockClusterClientInstance); - mockAdminClusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); + mockClusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); const setupContract = await elasticsearchService.setup(deps); setupContract.esNodesCompatibility$.subscribe(async () => { - expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); await elasticsearchService.stop(); await delay(100); - expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); done(); }); }); diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts index ab9c9e11fedc8..26001bf83924f 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.ts @@ -50,8 +50,7 @@ import { calculateStatus$ } from './status'; /** @internal */ interface CoreClusterClients { config: ElasticsearchConfig; - adminClient: ClusterClient; - dataClient: ClusterClient; + client: ClusterClient; } interface SetupDeps { @@ -70,7 +69,7 @@ export class ElasticsearchService type: string, clientConfig?: Partial ) => ICustomClusterClient; - private adminClient?: IClusterClient; + private client?: IClusterClient; constructor(private readonly coreContext: CoreContext) { this.kibanaVersion = coreContext.env.packageInfo.version; @@ -95,21 +94,19 @@ export class ElasticsearchService switchMap( (config) => new Observable((subscriber) => { - this.log.debug(`Creating elasticsearch clients`); + this.log.debug('Creating elasticsearch client'); const coreClients = { config, - adminClient: this.createClusterClient('admin', config), - dataClient: this.createClusterClient('data', config, deps.http.getAuthHeaders), + client: this.createClusterClient('data', config, deps.http.getAuthHeaders), }; subscriber.next(coreClients); return () => { - this.log.debug(`Closing elasticsearch clients`); + this.log.debug('Closing elasticsearch client'); - coreClients.adminClient.close(); - coreClients.dataClient.close(); + coreClients.client.close(); }; }) ), @@ -120,54 +117,27 @@ export class ElasticsearchService const config = await this.config$.pipe(first()).toPromise(); - const adminClient$ = clients$.pipe(map((clients) => clients.adminClient)); - const dataClient$ = clients$.pipe(map((clients) => clients.dataClient)); + const client$ = clients$.pipe(map((clients) => clients.client)); - this.adminClient = { + const client = { async callAsInternalUser( endpoint: string, clientParams: Record = {}, options?: CallAPIOptions ) { - const client = await adminClient$.pipe(take(1)).toPromise(); - return await client.callAsInternalUser(endpoint, clientParams, options); - }, - asScoped: (request: ScopeableRequest) => { - return { - callAsInternalUser: this.adminClient!.callAsInternalUser, - async callAsCurrentUser( - endpoint: string, - clientParams: Record = {}, - options?: CallAPIOptions - ) { - const client = await adminClient$.pipe(take(1)).toPromise(); - return await client - .asScoped(request) - .callAsCurrentUser(endpoint, clientParams, options); - }, - }; - }, - }; - - const dataClient = { - async callAsInternalUser( - endpoint: string, - clientParams: Record = {}, - options?: CallAPIOptions - ) { - const client = await dataClient$.pipe(take(1)).toPromise(); - return await client.callAsInternalUser(endpoint, clientParams, options); + const _client = await client$.pipe(take(1)).toPromise(); + return await _client.callAsInternalUser(endpoint, clientParams, options); }, asScoped(request: ScopeableRequest) { return { - callAsInternalUser: dataClient.callAsInternalUser, + callAsInternalUser: client.callAsInternalUser, async callAsCurrentUser( endpoint: string, clientParams: Record = {}, options?: CallAPIOptions ) { - const client = await dataClient$.pipe(take(1)).toPromise(); - return await client + const _client = await client$.pipe(take(1)).toPromise(); + return await _client .asScoped(request) .callAsCurrentUser(endpoint, clientParams, options); }, @@ -175,8 +145,10 @@ export class ElasticsearchService }, }; + this.client = client; + const esNodesCompatibility$ = pollEsNodesVersion({ - callWithInternalUser: this.adminClient.callAsInternalUser, + callWithInternalUser: client.callAsInternalUser, log: this.log, ignoreVersionMismatch: config.ignoreVersionMismatch, esVersionCheckInterval: config.healthCheckDelay.asMilliseconds(), @@ -189,22 +161,22 @@ export class ElasticsearchService }; return { - legacy: { config$: clients$.pipe(map((clients) => clients.config)) }, + legacy: { + config$: clients$.pipe(map((clients) => clients.config)), + client, + createClient: this.createClient, + }, esNodesCompatibility$, - adminClient: this.adminClient, - dataClient, - createClient: this.createClient, status$: calculateStatus$(esNodesCompatibility$), }; } - public async start() { - if (typeof this.adminClient === 'undefined' || typeof this.createClient === 'undefined') { + if (typeof this.client === 'undefined' || typeof this.createClient === 'undefined') { throw new Error('ElasticsearchService needs to be setup before calling start'); } else { return { legacy: { - client: this.adminClient, + client: this.client, createClient: this.createClient, }, }; diff --git a/src/core/server/elasticsearch/types.ts b/src/core/server/elasticsearch/types.ts index 3d38935e9fbf0..6fef08fc298ff 100644 --- a/src/core/server/elasticsearch/types.ts +++ b/src/core/server/elasticsearch/types.ts @@ -30,62 +30,60 @@ import { ServiceStatus } from '../status'; export interface ElasticsearchServiceSetup { /** * @deprecated - * Use {@link ElasticsearchServiceStart.legacy | ElasticsearchServiceStart.legacy.createClient} instead. + * Use {@link ElasticsearchServiceStart.legacy} instead. * - * Create application specific Elasticsearch cluster API client with customized config. See {@link IClusterClient}. - * - * @param type Unique identifier of the client - * @param clientConfig A config consists of Elasticsearch JS client options and - * valid sub-set of Elasticsearch service config. - * We fill all the missing properties in the `clientConfig` using the default - * Elasticsearch config so that we don't depend on default values set and - * controlled by underlying Elasticsearch JS client. - * We don't run validation against the passed config and expect it to be valid. - * - * @example - * ```js - * const client = elasticsearch.createCluster('my-app-name', config); - * const data = await client.callAsInternalUser(); - * ``` - */ - readonly createClient: ( - type: string, - clientConfig?: Partial - ) => ICustomClusterClient; - - /** - * @deprecated - * Use {@link ElasticsearchServiceStart.legacy | ElasticsearchServiceStart.legacy.client} instead. - * - * A client for the `admin` cluster. All Elasticsearch config value changes are processed under the hood. - * See {@link IClusterClient}. - * - * @example - * ```js - * const client = core.elasticsearch.adminClient; - * ``` - */ - readonly adminClient: IClusterClient; + * */ + legacy: { + /** + * @deprecated + * Use {@link ElasticsearchServiceStart.legacy | ElasticsearchServiceStart.legacy.createClient} instead. + * + * Create application specific Elasticsearch cluster API client with customized config. See {@link IClusterClient}. + * + * @param type Unique identifier of the client + * @param clientConfig A config consists of Elasticsearch JS client options and + * valid sub-set of Elasticsearch service config. + * We fill all the missing properties in the `clientConfig` using the default + * Elasticsearch config so that we don't depend on default values set and + * controlled by underlying Elasticsearch JS client. + * We don't run validation against the passed config and expect it to be valid. + * + * @example + * ```js + * const client = elasticsearch.createCluster('my-app-name', config); + * const data = await client.callAsInternalUser(); + * ``` + */ + readonly createClient: ( + type: string, + clientConfig?: Partial + ) => ICustomClusterClient; - /** - * @deprecated - * Use {@link ElasticsearchServiceStart.legacy | ElasticsearchServiceStart.legacy.client} instead. - * - * A client for the `data` cluster. All Elasticsearch config value changes are processed under the hood. - * See {@link IClusterClient}. - * - * @example - * ```js - * const client = core.elasticsearch.dataClient; - * ``` - */ - readonly dataClient: IClusterClient; + /** + * @deprecated + * Use {@link ElasticsearchServiceStart.legacy | ElasticsearchServiceStart.legacy.client} instead. + * + * All Elasticsearch config value changes are processed under the hood. + * See {@link IClusterClient}. + * + * @example + * ```js + * const client = core.elasticsearch.legacy.client; + * ``` + */ + readonly client: IClusterClient; + }; } /** * @public */ export interface ElasticsearchServiceStart { + /** + * @deprecated + * Provided for the backward compatibility. + * Switch to the new elasticsearch client as soon as https://github.com/elastic/kibana/issues/35508 done. + * */ legacy: { /** * Create application specific Elasticsearch cluster API client with customized config. See {@link IClusterClient}. @@ -123,9 +121,9 @@ export interface ElasticsearchServiceStart { } /** @internal */ -export interface InternalElasticsearchServiceSetup extends ElasticsearchServiceSetup { +export interface InternalElasticsearchServiceSetup { // Required for the BWC with the legacy Kibana only. - readonly legacy: { + readonly legacy: ElasticsearchServiceSetup['legacy'] & { readonly config$: Observable; }; esNodesCompatibility$: Observable; diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts index 9583cca177619..ba39effa77016 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -383,8 +383,8 @@ describe('http service', () => { // client contains authHeaders for BWC with legacy platform. const [client] = clusterClientMock.mock.calls; - const [, , dataClientHeaders] = client; - expect(dataClientHeaders).toEqual(authHeaders); + const [, , clientHeaders] = client; + expect(clientHeaders).toEqual(authHeaders); }); it('passes request authorization header to Elasticsearch if registerAuth was not set', async () => { @@ -407,8 +407,8 @@ describe('http service', () => { .expect(200); const [client] = clusterClientMock.mock.calls; - const [, , dataClientHeaders] = client; - expect(dataClientHeaders).toEqual({ authorization: authorizationHeader }); + const [, , clientHeaders] = client; + expect(clientHeaders).toEqual({ authorization: authorizationHeader }); }); }); }); diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 96bb0c9a006b0..658c24f835020 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -338,10 +338,8 @@ export { * which uses the credentials of the incoming request * - {@link ISavedObjectTypeRegistry | savedObjects.typeRegistry} - Type registry containing * all the registered types. - * - {@link ScopedClusterClient | elasticsearch.dataClient} - Elasticsearch + * - {@link ScopedClusterClient | elasticsearch.legacy.client} - Elasticsearch * data client which uses the credentials of the incoming request - * - {@link ScopedClusterClient | elasticsearch.adminClient} - Elasticsearch - * admin client which uses the credentials of the incoming request * - {@link IUiSettingsClient | uiSettings.client} - uiSettings client * which uses the credentials of the incoming request * diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index df1ed3e100923..2ced8b4762406 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -279,9 +279,10 @@ export class LegacyService implements CoreService { capabilities: setupDeps.core.capabilities, context: setupDeps.core.context, elasticsearch: { - adminClient: setupDeps.core.elasticsearch.adminClient, - dataClient: setupDeps.core.elasticsearch.dataClient, - createClient: setupDeps.core.elasticsearch.createClient, + legacy: { + client: setupDeps.core.elasticsearch.legacy.client, + createClient: setupDeps.core.elasticsearch.legacy.createClient, + }, }, http: { createCookieSessionStorageFactory: setupDeps.core.http.createCookieSessionStorageFactory, @@ -352,7 +353,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/legacy/types.ts b/src/core/server/legacy/types.ts index 2567ca790e04f..98f8d874c7088 100644 --- a/src/core/server/legacy/types.ts +++ b/src/core/server/legacy/types.ts @@ -151,7 +151,7 @@ export type LegacyAppSpec = Partial & { * @internal * @deprecated */ -export type LegacyNavLink = Omit & { +export type LegacyNavLink = Omit & { order: number; }; diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index f0fd471abb9be..b6e9ffef6f3f1 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -101,6 +101,7 @@ function pluginInitializerContextMock(config: T = {} as T) { } type CoreSetupMockType = MockedKeys & { + elasticsearch: ReturnType; getStartServices: jest.MockedFunction>; }; diff --git a/src/core/server/path/index.ts b/src/core/server/path/index.ts index d482a32b32ae4..2e05e3856bd4c 100644 --- a/src/core/server/path/index.ts +++ b/src/core/server/path/index.ts @@ -28,7 +28,6 @@ const CONFIG_PATHS = [ process.env.KIBANA_PATH_CONF && join(process.env.KIBANA_PATH_CONF, 'kibana.yml'), process.env.CONFIG_PATH, // deprecated fromRoot('config/kibana.yml'), - '/etc/kibana/kibana.yml', ].filter(isString); const DATA_PATHS = [ diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index ab18a9cbbc062..7afb607192cae 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -147,9 +147,7 @@ export function createPluginSetupContext( createContextContainer: deps.context.createContextContainer, }, elasticsearch: { - adminClient: deps.elasticsearch.adminClient, - dataClient: deps.elasticsearch.dataClient, - createClient: deps.elasticsearch.createClient, + legacy: deps.elasticsearch.legacy, }, http: { createCookieSessionStorageFactory: deps.http.createCookieSessionStorageFactory, diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts index bff392e8761eb..9fba2728003d2 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -67,6 +67,13 @@ describe('SavedObjectsService', () => { }; }; + const createStartDeps = (pluginsInitialized: boolean = true) => { + return { + pluginsInitialized, + elasticsearch: elasticsearchServiceMock.createStart(), + }; + }; + afterEach(() => { jest.clearAllMocks(); }); @@ -83,7 +90,7 @@ describe('SavedObjectsService', () => { setup.setClientFactoryProvider(factoryProvider); - await soService.start({}); + await soService.start(createStartDeps()); expect(clientProviderInstanceMock.setClientFactory).toHaveBeenCalledWith(factory); }); @@ -117,7 +124,7 @@ describe('SavedObjectsService', () => { setup.addClientWrapper(1, 'A', wrapperA); setup.addClientWrapper(2, 'B', wrapperB); - await soService.start({}); + await soService.start(createStartDeps()); expect(clientProviderInstanceMock.addClientWrapperFactory).toHaveBeenCalledTimes(2); expect(clientProviderInstanceMock.addClientWrapperFactory).toHaveBeenCalledWith( @@ -159,9 +166,10 @@ describe('SavedObjectsService', () => { const soService = new SavedObjectsService(coreContext); const coreSetup = createSetupDeps(); + const coreStart = createStartDeps(); let i = 0; - coreSetup.elasticsearch.adminClient.callAsInternalUser = jest + coreStart.elasticsearch.legacy.client.callAsInternalUser = jest .fn() .mockImplementation(() => i++ <= 2 @@ -170,7 +178,7 @@ describe('SavedObjectsService', () => { ); await soService.setup(coreSetup); - await soService.start({}, 1); + await soService.start(coreStart, 1); return expect(KibanaMigratorMock.mock.calls[0][0].callCluster()).resolves.toMatch('success'); }); @@ -180,7 +188,7 @@ describe('SavedObjectsService', () => { const soService = new SavedObjectsService(coreContext); await soService.setup(createSetupDeps()); - await soService.start({ pluginsInitialized: false }); + await soService.start(createStartDeps(false)); expect(migratorInstanceMock.runMigrations).not.toHaveBeenCalled(); }); @@ -188,7 +196,7 @@ describe('SavedObjectsService', () => { const coreContext = createCoreContext({ skipMigration: true }); const soService = new SavedObjectsService(coreContext); await soService.setup(createSetupDeps()); - await soService.start({}); + await soService.start(createStartDeps()); expect(migratorInstanceMock.runMigrations).not.toHaveBeenCalled(); }); @@ -206,7 +214,7 @@ describe('SavedObjectsService', () => { kibanaVersion: '8.0.0', }); await soService.setup(setupDeps); - soService.start({}); + soService.start(createStartDeps()); expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(0); ((setupDeps.elasticsearch.esNodesCompatibility$ as any) as BehaviorSubject< NodesVersionCompatibility @@ -228,7 +236,7 @@ describe('SavedObjectsService', () => { await soService.setup(createSetupDeps()); expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(0); - const startContract = await soService.start({}); + const startContract = await soService.start(createStartDeps()); expect(startContract.migrator).toBe(migratorInstanceMock); expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(1); }); @@ -237,7 +245,7 @@ describe('SavedObjectsService', () => { const coreContext = createCoreContext({ skipMigration: false }); const soService = new SavedObjectsService(coreContext); const setup = await soService.setup(createSetupDeps()); - await soService.start({}); + await soService.start(createStartDeps()); expect(() => { setup.setClientFactoryProvider(jest.fn()); @@ -268,7 +276,7 @@ describe('SavedObjectsService', () => { const coreContext = createCoreContext({ skipMigration: false }); const soService = new SavedObjectsService(coreContext); await soService.setup(createSetupDeps()); - const { getTypeRegistry } = await soService.start({}); + const { getTypeRegistry } = await soService.start(createStartDeps()); expect(getTypeRegistry()).toBe(typeRegistryInstanceMock); }); @@ -280,18 +288,19 @@ describe('SavedObjectsService', () => { const soService = new SavedObjectsService(coreContext); const coreSetup = createSetupDeps(); await soService.setup(coreSetup); - const { createScopedRepository } = await soService.start({}); + const coreStart = createStartDeps(); + const { createScopedRepository } = await soService.start(coreStart); const req = {} as KibanaRequest; createScopedRepository(req); - expect(coreSetup.elasticsearch.adminClient.asScoped).toHaveBeenCalledWith(req); + expect(coreStart.elasticsearch.legacy.client.asScoped).toHaveBeenCalledWith(req); const [ { value: { callAsCurrentUser }, }, - ] = coreSetup.elasticsearch.adminClient.asScoped.mock.results; + ] = coreStart.elasticsearch.legacy.client.asScoped.mock.results; const [ [, , , callCluster, includedHiddenTypes], @@ -306,7 +315,8 @@ describe('SavedObjectsService', () => { const soService = new SavedObjectsService(coreContext); const coreSetup = createSetupDeps(); await soService.setup(coreSetup); - const { createScopedRepository } = await soService.start({}); + const coreStart = createStartDeps(); + const { createScopedRepository } = await soService.start(coreStart); const req = {} as KibanaRequest; createScopedRepository(req, ['someHiddenType']); @@ -325,7 +335,8 @@ describe('SavedObjectsService', () => { const soService = new SavedObjectsService(coreContext); const coreSetup = createSetupDeps(); await soService.setup(coreSetup); - const { createInternalRepository } = await soService.start({}); + const coreStart = createStartDeps(); + const { createInternalRepository } = await soService.start(coreStart); createInternalRepository(); @@ -333,8 +344,8 @@ describe('SavedObjectsService', () => { [, , , callCluster, includedHiddenTypes], ] = (SavedObjectsRepository.createRepository as jest.Mocked).mock.calls; - expect(coreSetup.elasticsearch.adminClient.callAsInternalUser).toBe(callCluster); - expect(callCluster).toBe(coreSetup.elasticsearch.adminClient.callAsInternalUser); + expect(coreStart.elasticsearch.legacy.client.callAsInternalUser).toBe(callCluster); + expect(callCluster).toBe(coreStart.elasticsearch.legacy.client.callAsInternalUser); expect(includedHiddenTypes).toEqual([]); }); @@ -343,7 +354,7 @@ describe('SavedObjectsService', () => { const soService = new SavedObjectsService(coreContext); const coreSetup = createSetupDeps(); await soService.setup(coreSetup); - const { createInternalRepository } = await soService.start({}); + const { createInternalRepository } = await soService.start(createStartDeps()); createInternalRepository(['someHiddenType']); diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index a822a92acb91a..48b1e12fc187e 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -29,7 +29,12 @@ import { import { KibanaMigrator, IKibanaMigrator } from './migrations'; import { CoreContext } from '../core_context'; import { LegacyServiceDiscoverPlugins } from '../legacy'; -import { InternalElasticsearchServiceSetup, APICaller } from '../elasticsearch'; +import { + APICaller, + ElasticsearchServiceStart, + IClusterClient, + InternalElasticsearchServiceSetup, +} from '../elasticsearch'; import { KibanaConfigType } from '../kibana_config'; import { migrationsRetryCallCluster } from '../elasticsearch/retry_call_cluster'; import { @@ -278,8 +283,8 @@ interface WrappedClientFactoryWrapper { } /** @internal */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SavedObjectsStartDeps { + elasticsearch: ElasticsearchServiceStart; pluginsInitialized?: boolean; } @@ -365,7 +370,7 @@ export class SavedObjectsService } public async start( - { pluginsInitialized = true }: SavedObjectsStartDeps, + { elasticsearch, pluginsInitialized = true }: SavedObjectsStartDeps, migrationsRetryDelay?: number ): Promise { if (!this.setupDeps || !this.config) { @@ -378,8 +383,14 @@ export class SavedObjectsService .atPath('kibana') .pipe(first()) .toPromise(); - const adminClient = this.setupDeps!.elasticsearch.adminClient; - const migrator = this.createMigrator(kibanaConfig, this.config.migration, migrationsRetryDelay); + const client = elasticsearch.legacy.client; + + const migrator = this.createMigrator( + kibanaConfig, + this.config.migration, + client, + migrationsRetryDelay + ); this.migrator$.next(migrator); @@ -435,9 +446,9 @@ export class SavedObjectsService const repositoryFactory: SavedObjectsRepositoryFactory = { createInternalRepository: (includedHiddenTypes?: string[]) => - createRepository(adminClient.callAsInternalUser, includedHiddenTypes), + createRepository(client.callAsInternalUser, includedHiddenTypes), createScopedRepository: (req: KibanaRequest, includedHiddenTypes?: string[]) => - createRepository(adminClient.asScoped(req).callAsCurrentUser, includedHiddenTypes), + createRepository(client.asScoped(req).callAsCurrentUser, includedHiddenTypes), }; const clientProvider = new SavedObjectsClientProvider({ @@ -473,10 +484,9 @@ export class SavedObjectsService private createMigrator( kibanaConfig: KibanaConfigType, savedObjectsConfig: SavedObjectsMigrationConfigType, + esClient: IClusterClient, migrationsRetryDelay?: number ): KibanaMigrator { - const adminClient = this.setupDeps!.elasticsearch.adminClient; - return new KibanaMigrator({ typeRegistry: this.typeRegistry, logger: this.logger, @@ -485,7 +495,7 @@ export class SavedObjectsService savedObjectValidations: this.validations, kibanaConfig, callCluster: migrationsRetryCallCluster( - adminClient.callAsInternalUser, + esClient.callAsInternalUser, this.logger, migrationsRetryDelay ), diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 858458bfe40de..eef071e9488bf 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -823,16 +823,15 @@ export class ElasticsearchErrorHelpers { // @public (undocumented) export interface ElasticsearchServiceSetup { // @deprecated (undocumented) - readonly adminClient: IClusterClient; - // @deprecated (undocumented) - readonly createClient: (type: string, clientConfig?: Partial) => ICustomClusterClient; - // @deprecated (undocumented) - readonly dataClient: IClusterClient; + legacy: { + readonly createClient: (type: string, clientConfig?: Partial) => ICustomClusterClient; + readonly client: IClusterClient; + }; } // @public (undocumented) export interface ElasticsearchServiceStart { - // (undocumented) + // @deprecated (undocumented) legacy: { readonly createClient: (type: string, clientConfig?: Partial) => ICustomClusterClient; readonly client: IClusterClient; diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 8226b4e3a57e0..ef12379c199e8 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -195,12 +195,13 @@ export class Server { public async start() { this.log.debug('starting server'); + const elasticsearchStart = await this.elasticsearch.start(); const savedObjectsStart = await this.savedObjects.start({ + elasticsearch: elasticsearchStart, pluginsInitialized: this.pluginsInitialized, }); const capabilitiesStart = this.capabilities.start(); const uiSettingsStart = await this.uiSettings.start(); - const elasticsearchStart = await this.elasticsearch.start(); this.coreStart = { capabilities: capabilitiesStart, @@ -247,21 +248,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/dev/build/tasks/os_packages/service_templates/sysv/etc/default/kibana b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/default/kibana index 7b411542986be..092dc6482fa1d 100644 --- a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/default/kibana +++ b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/default/kibana @@ -11,3 +11,5 @@ nice="" KILL_ON_STOP_TIMEOUT=0 BABEL_CACHE_PATH="/var/lib/kibana/optimize/.babel_register_cache.json" + +KIBANA_PATH_CONF="/etc/kibana" diff --git a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana index fc1b797fe8ed2..a17d15522b45e 100755 --- a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana +++ b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana @@ -23,6 +23,7 @@ pidfile="/var/run/$name.pid" [ -r /etc/default/$name ] && . /etc/default/$name [ -r /etc/sysconfig/$name ] && . /etc/sysconfig/$name +export KIBANA_PATH_CONF export NODE_OPTIONS [ -z "$nice" ] && nice=0 diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index d9d0528748dc0..343ff47199375 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -128,9 +128,17 @@ export GECKODRIVER_CDNURL="https://us-central1-elastic-kibana-184716.cloudfuncti export CHROMEDRIVER_CDNURL="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache" export CYPRESS_DOWNLOAD_MIRROR="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/cypress" - export CHECKS_REPORTER_ACTIVE=false +# This is mainly for release-manager builds, which run in an environment that doesn't have Chrome installed +if [[ "$(which google-chrome-stable)" || "$(which google-chrome)" ]]; then + echo "Chrome detected, setting DETECT_CHROMEDRIVER_VERSION=true" + export DETECT_CHROMEDRIVER_VERSION=true + export CHROMEDRIVER_FORCE_DOWNLOAD=true +else + echo "Chrome not detected, installing default chromedriver binary for the package version" +fi + ### only run on pr jobs for elastic/kibana, checks-reporter doesn't work for other repos if [[ "$ghprbPullId" && "$ghprbGhRepository" == 'elastic/kibana' ]] ; then export CHECKS_REPORTER_ACTIVE=true diff --git a/src/legacy/core_plugins/elasticsearch/index.js b/src/legacy/core_plugins/elasticsearch/index.js index a7d6810ac6158..eb502e97fb77c 100644 --- a/src/legacy/core_plugins/elasticsearch/index.js +++ b/src/legacy/core_plugins/elasticsearch/index.js @@ -34,9 +34,9 @@ export default function (kibana) { // All methods that ES plugin exposes are synchronous so we should get the first // value from all observables here to be able to synchronously return and create // cluster clients afterwards. - const { adminClient, dataClient } = server.newPlatform.setup.core.elasticsearch; - const adminCluster = new Cluster(adminClient); - const dataCluster = new Cluster(dataClient); + const { client } = server.newPlatform.setup.core.elasticsearch.legacy; + const adminCluster = new Cluster(client); + const dataCluster = new Cluster(client); const esConfig = await server.newPlatform.__internals.elasticsearch.legacy.config$ .pipe(first()) @@ -72,7 +72,7 @@ export default function (kibana) { } const cluster = new Cluster( - server.newPlatform.setup.core.elasticsearch.createClient(name, clientConfig) + server.newPlatform.setup.core.elasticsearch.legacy.createClient(name, clientConfig) ); clusters.set(name, cluster); 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/server/saved_objects/saved_objects_mixin.test.js b/src/legacy/server/saved_objects/saved_objects_mixin.test.js index 5b40cc4b5aa35..63e4a632ab5e0 100644 --- a/src/legacy/server/saved_objects/saved_objects_mixin.test.js +++ b/src/legacy/server/saved_objects/saved_objects_mixin.test.js @@ -129,13 +129,6 @@ describe('Saved Objects Mixin', () => { waitUntilReady: jest.fn(), }, }, - newPlatform: { - __internals: { - elasticsearch: { - adminClient: { callAsInternalUser: mockCallCluster }, - }, - }, - }, }; const coreStart = coreMock.createStart(); 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/console/server/lib/spec_definitions/json/overrides/update_by_query.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/update_by_query.json new file mode 100644 index 0000000000000..44819eda6e29e --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/overrides/update_by_query.json @@ -0,0 +1,17 @@ +{ + "update_by_query": { + "data_autocomplete_rules": { + "conflicts": "", + "query": { + "__scope_link": "GLOBAL.query" + }, + "script": { + "__template": { + "source": "", + "lang": "painless" + }, + "__scope_link": "GLOBAL.script" + } + } + } +} 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..142ec9c8c877e 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; @@ -1364,7 +1366,7 @@ export interface QueryState { // Warning: (ae-missing-release-tag) "QueryStringInput" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const QueryStringInput: React.FC>; +export const QueryStringInput: React.FC>; // @public (undocumented) export type QuerySuggestion = QuerySuggestionBasic | QuerySuggestionField; @@ -1576,8 +1578,8 @@ export const search: { // Warning: (ae-missing-release-tag) "SearchBar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "customSubmitButton" | "screenTitle" | "dataTestSubj" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "timeHistory" | "onFiltersUpdated">, any> & { - WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; +export const SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "customSubmitButton" | "screenTitle" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "timeHistory" | "onFiltersUpdated">, any> & { + WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; }; // Warning: (ae-forgotten-export) The symbol "SearchBarOwnProps" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index 8ae63682413e5..e555c40d25592 100644 --- a/src/plugins/telemetry/server/plugin.ts +++ b/src/plugins/telemetry/server/plugin.ts @@ -82,7 +82,7 @@ export class TelemetryPlugin implements Plugin { const config$ = this.config$; const isDev = this.isDev; - registerCollection(telemetryCollectionManager, elasticsearch.dataClient); + registerCollection(telemetryCollectionManager, elasticsearch.legacy.client); const router = http.createRouter(); registerRoutes({ diff --git a/src/plugins/vis_type_metric/public/components/metric_vis_component.tsx b/src/plugins/vis_type_metric/public/components/metric_vis_component.tsx index eb3986b6388fe..5e8a463748188 100644 --- a/src/plugins/vis_type_metric/public/components/metric_vis_component.tsx +++ b/src/plugins/vis_type_metric/public/components/metric_vis_component.tsx @@ -93,7 +93,7 @@ export class MetricVisComponent extends Component { return false; } - const [red, green, blue] = colors.slice(1).map(parseInt); + const [red, green, blue] = colors.slice(1).map((c) => parseInt(c, 10)); return isColorDark(red, green, blue); } diff --git a/test/accessibility/apps/discover.ts b/test/accessibility/apps/discover.ts index 38552f5ecdafe..7e905fbe89fbd 100644 --- a/test/accessibility/apps/discover.ts +++ b/test/accessibility/apps/discover.ts @@ -34,8 +34,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ['geo.src', 'IN'], ]; - // FLAKY: https://github.com/elastic/kibana/issues/62497 - describe.skip('Discover', () => { + describe('Discover', () => { before(async () => { await esArchiver.load('discover'); await esArchiver.loadIfNeeded('logstash_functional'); @@ -133,9 +132,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Context view test it('should open context view on a doc', async () => { - await docTable.clickRowToggle(); - // click the open action await retry.try(async () => { + await docTable.clickRowToggle(); + // click the open action const rowActions = await docTable.getRowActions(); if (!rowActions.length) { throw new Error('row actions empty, trying again'); diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh index ebc58c5e4e773..50a92a41e3932 100755 --- a/test/scripts/jenkins_xpack.sh +++ b/test/scripts/jenkins_xpack.sh @@ -21,6 +21,12 @@ if [[ -z "$CODE_COVERAGE" ]] ; then echo "" echo "" + echo " -> Running List cyclic dependency test" + cd "$XPACK_DIR" + checks-reporter-with-killswitch "X-Pack List cyclic dependency test" node plugins/lists/scripts/check_circular_deps + echo "" + echo "" + # echo " -> Running jest integration tests" # cd "$XPACK_DIR" # node scripts/jest_integration --ci --verbose diff --git a/vars/workers.groovy b/vars/workers.groovy index d2cc19787bc5f..387f62a625230 100644 --- a/vars/workers.groovy +++ b/vars/workers.groovy @@ -5,6 +5,8 @@ def label(size) { switch(size) { case 's': return 'linux && immutable' + case 's-highmem': + return 'tests-s' case 'l': return 'tests-l' case 'xl': @@ -114,7 +116,7 @@ def ci(Map params, Closure closure) { // Worker for running the current intake jobs. Just runs a single script after bootstrap. def intake(jobName, String script) { return { - ci(name: jobName, size: 's', ramDisk: false) { + ci(name: jobName, size: 's-highmem', ramDisk: true) { withEnv(["JOB=${jobName}"]) { runbld(script, "Execute ${jobName}") } diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 7ac27dd47ad64..a479b08ef9069 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -4,7 +4,7 @@ "xpack.actions": "plugins/actions", "xpack.advancedUiActions": "plugins/advanced_ui_actions", "xpack.uiActionsEnhanced": "examples/ui_actions_enhanced_examples", - "xpack.alerting": "plugins/alerting", + "xpack.alerts": "plugins/alerts", "xpack.alertingBuiltins": "plugins/alerting_builtins", "xpack.apm": ["legacy/plugins/apm", "plugins/apm"], "xpack.beatsManagement": ["legacy/plugins/beats_management", "plugins/beats_management"], diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index 3d8b45e7d1b83..a222e11d28f4a 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) { +export function createJestConfig({ kibanaDirectory, rootDir, xPackKibanaDirectory }) { const fileMockPath = `${kibanaDirectory}/src/dev/jest/mocks/file_mock.js`; return { - rootDir: xPackKibanaDirectory, + rootDir, roots: ['/plugins', '/legacy/plugins', '/legacy/server'], moduleFileExtensions: ['js', 'json', 'ts', 'tsx'], moduleNameMapper: { @@ -44,15 +44,15 @@ export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) { '!**/plugins/apm/e2e/**', ], coveragePathIgnorePatterns: ['.*\\.d\\.ts'], - coverageDirectory: '/../target/kibana-coverage/jest', + coverageDirectory: `${kibanaDirectory}/target/kibana-coverage/jest`, coverageReporters: !!process.env.CODE_COVERAGE ? ['json'] : ['html'], setupFiles: [ `${kibanaDirectory}/src/dev/jest/setup/babel_polyfill.js`, - `/dev-tools/jest/setup/polyfills.js`, - `/dev-tools/jest/setup/enzyme.js`, + `${xPackKibanaDirectory}/dev-tools/jest/setup/polyfills.js`, + `${xPackKibanaDirectory}/dev-tools/jest/setup/enzyme.js`, ], setupFilesAfterEnv: [ - `/dev-tools/jest/setup/setup_test.js`, + `${xPackKibanaDirectory}/dev-tools/jest/setup/setup_test.js`, `${kibanaDirectory}/src/dev/jest/setup/mocks.js`, `${kibanaDirectory}/src/dev/jest/setup/react_testing_library.js`, ], diff --git a/x-pack/dev-tools/jest/index.js b/x-pack/dev-tools/jest/index.js index f61c50f989503..2f831e33cdd11 100644 --- a/x-pack/dev-tools/jest/index.js +++ b/x-pack/dev-tools/jest/index.js @@ -14,6 +14,7 @@ export function runJest() { const config = JSON.stringify( createJestConfig({ kibanaDirectory: resolve(__dirname, '../../..'), + rootDir: resolve(__dirname, '../..'), xPackKibanaDirectory: resolve(__dirname, '../..'), }) ); 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/monitoring/index.ts b/x-pack/legacy/plugins/monitoring/index.ts index 1a0fecb9ef5b5..ee31a3037a0cb 100644 --- a/x-pack/legacy/plugins/monitoring/index.ts +++ b/x-pack/legacy/plugins/monitoring/index.ts @@ -15,7 +15,7 @@ import { KIBANA_ALERTING_ENABLED } from '../../../plugins/monitoring/common/cons */ const deps = ['kibana', 'elasticsearch', 'xpack_main']; if (KIBANA_ALERTING_ENABLED) { - deps.push(...['alerting', 'actions']); + deps.push(...['alerts', 'actions']); } export const monitoring = (kibana: any) => { return new kibana.Plugin({ diff --git a/x-pack/legacy/plugins/reporting/common/constants.ts b/x-pack/legacy/plugins/reporting/common/constants.ts index f30a7cc87f318..48483c79d1af2 100644 --- a/x-pack/legacy/plugins/reporting/common/constants.ts +++ b/x-pack/legacy/plugins/reporting/common/constants.ts @@ -27,6 +27,7 @@ export const WHITELISTED_JOB_CONTENT_TYPES = [ 'application/pdf', CONTENT_TYPE_CSV, 'image/png', + 'text/plain', ]; // See: diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/create_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/create_job.ts index 8320cd05aa2e7..c76b4afe727da 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/create_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/create_job.ts @@ -4,14 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { ReportingCore } from '../../../server'; import { cryptoFactory } from '../../../server/lib'; -import { - ConditionalHeaders, - CreateJobFactory, - ESQueueCreateJobFn, - RequestFacade, -} from '../../../server/types'; +import { CreateJobFactory, ESQueueCreateJobFn } from '../../../server/types'; import { JobParamsDiscoverCsv } from '../types'; export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(reporting: ReportingCore) { const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); + const setupDeps = reporting.getPluginSetupDeps(); return async function createJob( jobParams: JobParamsDiscoverCsv, - headers: ConditionalHeaders['headers'], - request: RequestFacade + context: RequestHandlerContext, + request: KibanaRequest ) { - const serializedEncryptedHeaders = await crypto.encrypt(headers); + const serializedEncryptedHeaders = await crypto.encrypt(request.headers); - const savedObjectsClient = request.getSavedObjectsClient(); + const savedObjectsClient = context.core.savedObjects.client; const indexPatternSavedObject = await savedObjectsClient.get( 'index-pattern', jobParams.indexPatternId! @@ -36,7 +33,7 @@ export const createJobFactory: CreateJobFactory clusterStub, + legacy: { + client: { + asScoped: () => clusterStub, + }, }, }; const mockUiSettingsClient = { diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts index 7d95c45d5d233..a6b2b0d0561d0 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts @@ -81,7 +81,7 @@ export const executeJobFactory: ExecuteJobFactory callAsCurrentUser(endpoint, clientParams, options); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job.ts index ed0e17454260f..d23f60d9c2480 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job.ts @@ -6,10 +6,11 @@ import { notFound, notImplemented } from 'boom'; import { get } from 'lodash'; +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../../common/constants'; import { ReportingCore } from '../../../../server'; import { cryptoFactory, LevelLogger } from '../../../../server/lib'; -import { CreateJobFactory, RequestFacade, TimeRangeParams } from '../../../../server/types'; +import { CreateJobFactory, TimeRangeParams } from '../../../../server/types'; import { JobDocPayloadPanelCsv, JobParamsPanelCsv, @@ -23,8 +24,9 @@ import { createJobSearch } from './create_job_search'; export type ImmediateCreateJobFn = ( jobParams: JobParamsType, - headers: Record, - req: RequestFacade + headers: KibanaRequest['headers'], + context: RequestHandlerContext, + req: KibanaRequest ) => Promise<{ type: string | null; title: string; @@ -46,21 +48,21 @@ export const createJobFactory: CreateJobFactory { const { savedObjectType, savedObjectId } = jobParams; const serializedEncryptedHeaders = await crypto.encrypt(headers); - const client = req.getSavedObjectsClient(); const { panel, title, visType }: VisData = await Promise.resolve() - .then(() => client.get(savedObjectType, savedObjectId)) + .then(() => context.core.savedObjects.client.get(savedObjectType, savedObjectId)) .then(async (savedObject: SavedObject) => { const { attributes, references } = savedObject; const { kibanaSavedObjectMeta: kibanaSavedObjectMetaJSON, } = attributes as SavedSearchObjectAttributesJSON; - const { timerange } = req.payload as { timerange: TimeRangeParams }; + const { timerange } = req.body as { timerange: TimeRangeParams }; if (!kibanaSavedObjectMetaJSON) { throw new Error('Could not parse saved object data!'); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts index 5761a98ed160c..4ef7b8514b363 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts @@ -5,15 +5,11 @@ */ import { i18n } from '@kbn/i18n'; +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { CONTENT_TYPE_CSV, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; import { ReportingCore } from '../../../server'; import { cryptoFactory, LevelLogger } from '../../../server/lib'; -import { - ExecuteJobFactory, - JobDocOutput, - JobDocPayload, - RequestFacade, -} from '../../../server/types'; +import { ExecuteJobFactory, JobDocOutput, JobDocPayload } from '../../../server/types'; import { CsvResultFromSearch } from '../../csv/types'; import { FakeRequest, JobDocPayloadPanelCsv, JobParamsPanelCsv, SearchPanel } from '../types'; import { createGenerateCsv } from './lib'; @@ -25,7 +21,8 @@ import { createGenerateCsv } from './lib'; export type ImmediateExecuteFn = ( jobId: null, job: JobDocPayload, - request: RequestFacade + context: RequestHandlerContext, + req: KibanaRequest ) => Promise; export const executeJobFactory: ExecuteJobFactory { // There will not be a jobID for "immediate" generation. // jobID is only for "queued" jobs @@ -58,10 +56,11 @@ export const executeJobFactory: ExecuteJobFactory; @@ -103,6 +102,7 @@ export const executeJobFactory: ExecuteJobFactory { }; export async function generateCsvSearch( - req: RequestFacade, reporting: ReportingCore, - logger: LevelLogger, + context: RequestHandlerContext, + req: KibanaRequest, searchPanel: SearchPanel, - jobParams: JobParamsDiscoverCsv + jobParams: JobParamsDiscoverCsv, + logger: LevelLogger ): Promise { - const savedObjectsClient = await reporting.getSavedObjectsClient( - KibanaRequest.from(req.getRawRequest()) - ); + const savedObjectsClient = context.core.savedObjects.client; const { indexPatternSavedObjectId, timerange } = searchPanel; - const savedSearchObjectAttr = searchPanel.attributes as SavedSearchObjectAttributes; + const savedSearchObjectAttr = searchPanel.attributes; const { indexPatternSavedObject } = await getDataSource( savedObjectsClient, indexPatternSavedObjectId @@ -153,9 +149,7 @@ export async function generateCsvSearch( const config = reporting.getConfig(); const elasticsearch = await reporting.getElasticsearchService(); - const { callAsCurrentUser } = elasticsearch.dataClient.asScoped( - KibanaRequest.from(req.getRawRequest()) - ); + const { callAsCurrentUser } = elasticsearch.legacy.client.asScoped(req); const callCluster = (...params: [string, object]) => callAsCurrentUser(...params); const uiSettings = await getUiSettings(uiConfig); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_filters.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_filters.ts index 071427f4dab64..4695bbd922581 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_filters.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_filters.ts @@ -22,7 +22,7 @@ export function getFilters( let timezone: string | null; if (indexPatternTimeField) { - if (!timerange) { + if (!timerange || !timerange.min || !timerange.max) { throw badRequest( `Time range params are required for index pattern [${indexPatternId}], using time field [${indexPatternTimeField}]` ); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_job_params_from_request.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_job_params_from_request.ts index 57d74ee0e1383..5aed02c10b961 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_job_params_from_request.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/get_job_params_from_request.ts @@ -4,15 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestFacade } from '../../../../server/types'; +import { KibanaRequest } from 'src/core/server'; import { JobParamsPanelCsv, JobParamsPostPayloadPanelCsv } from '../../types'; export function getJobParamsFromRequest( - request: RequestFacade, + request: KibanaRequest, opts: { isImmediate: boolean } ): JobParamsPanelCsv { - const { savedObjectType, savedObjectId } = request.params; - const { timerange, state } = request.payload as JobParamsPostPayloadPanelCsv; + const { savedObjectType, savedObjectId } = request.params as { + savedObjectType: string; + savedObjectId: string; + }; + const { timerange, state } = request.body as JobParamsPostPayloadPanelCsv; + const post = timerange || state ? { timerange, state } : undefined; return { diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/create_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/create_job/index.ts index b19513de29eee..ab492c21256eb 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/create_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/create_job/index.ts @@ -5,28 +5,23 @@ */ import { validateUrls } from '../../../../common/validate_urls'; -import { ReportingCore } from '../../../../server'; import { cryptoFactory } from '../../../../server/lib'; -import { - ConditionalHeaders, - CreateJobFactory, - ESQueueCreateJobFn, - RequestFacade, -} from '../../../../server/types'; +import { CreateJobFactory, ESQueueCreateJobFn } from '../../../../server/types'; import { JobParamsPNG } from '../../types'; export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(reporting: ReportingCore) { +>> = function createJobFactoryFn(reporting) { const config = reporting.getConfig(); + const setupDeps = reporting.getPluginSetupDeps(); const crypto = cryptoFactory(config.get('encryptionKey')); return async function createJob( - { objectType, title, relativeUrl, browserTimezone, layout }: JobParamsPNG, - headers: ConditionalHeaders['headers'], - request: RequestFacade + { objectType, title, relativeUrl, browserTimezone, layout }, + context, + req ) { - const serializedEncryptedHeaders = await crypto.encrypt(headers); + const serializedEncryptedHeaders = await crypto.encrypt(req.headers); validateUrls([relativeUrl]); @@ -37,7 +32,7 @@ export const createJobFactory: CreateJobFactory { mockReporting = await createMockReportingCore(mockReportingConfig); const mockElasticsearch = { - dataClient: { - asScoped: () => ({ callAsCurrentUser: jest.fn() }), + legacy: { + client: { + asScoped: () => ({ callAsCurrentUser: jest.fn() }), + }, }, }; const mockGetElasticsearch = jest.fn(); diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/index.ts index 6882c29f80baa..ef597cfb45f78 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/index.ts @@ -5,33 +5,28 @@ */ import { validateUrls } from '../../../../common/validate_urls'; -import { ReportingCore } from '../../../../server'; import { cryptoFactory } from '../../../../server/lib'; -import { - ConditionalHeaders, - CreateJobFactory, - ESQueueCreateJobFn, - RequestFacade, -} from '../../../../server/types'; +import { CreateJobFactory, ESQueueCreateJobFn } from '../../../../server/types'; import { JobParamsPDF } from '../../types'; export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(reporting: ReportingCore) { +>> = function createJobFactoryFn(reporting) { const config = reporting.getConfig(); + const setupDeps = reporting.getPluginSetupDeps(); const crypto = cryptoFactory(config.get('encryptionKey')); return async function createJobFn( { title, relativeUrls, browserTimezone, layout, objectType }: JobParamsPDF, - headers: ConditionalHeaders['headers'], - request: RequestFacade + context, + req ) { - const serializedEncryptedHeaders = await crypto.encrypt(headers); + const serializedEncryptedHeaders = await crypto.encrypt(req.headers); validateUrls(relativeUrls); return { - basePath: request.getBasePath(), + basePath: setupDeps.basePath(req), browserTimezone, forceNow: new Date().toISOString(), headers: serializedEncryptedHeaders, diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.ts index b081521fef8dd..fdf9c24730638 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.ts @@ -57,8 +57,10 @@ beforeEach(async () => { mockReporting = await createMockReportingCore(mockReportingConfig); const mockElasticsearch = { - dataClient: { - asScoped: () => ({ callAsCurrentUser: jest.fn() }), + legacy: { + client: { + asScoped: () => ({ callAsCurrentUser: jest.fn() }), + }, }, }; const mockGetElasticsearch = jest.fn(); diff --git a/x-pack/legacy/plugins/reporting/server/core.ts b/x-pack/legacy/plugins/reporting/server/core.ts index 8fb948a253c16..b89ef9e06b961 100644 --- a/x-pack/legacy/plugins/reporting/server/core.ts +++ b/x-pack/legacy/plugins/reporting/server/core.ts @@ -5,33 +5,35 @@ */ import * as Rx from 'rxjs'; -import { first, mapTo } from 'rxjs/operators'; +import { first, mapTo, map } from 'rxjs/operators'; import { ElasticsearchServiceSetup, KibanaRequest, - SavedObjectsClient, SavedObjectsServiceStart, UiSettingsServiceStart, + IRouter, + SavedObjectsClientContract, + BasePath, } from 'src/core/server'; -import { ReportingPluginSpecOptions } from '../'; -// @ts-ignore no module definition -import { mirrorPluginStatus } from '../../../server/lib/mirror_plugin_status'; -import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; -import { PLUGIN_ID } from '../common/constants'; +import { SecurityPluginSetup } from '../../../../plugins/security/server'; +import { LicensingPluginSetup } from '../../../../plugins/licensing/server'; import { screenshotsObservableFactory } from '../export_types/common/lib/screenshots'; -import { ServerFacade } from '../server/types'; +import { ScreenshotsObservableFn } from '../server/types'; import { ReportingConfig } from './'; import { HeadlessChromiumDriverFactory } from './browsers/chromium/driver_factory'; -import { checkLicenseFactory, getExportTypesRegistry, LevelLogger } from './lib'; +import { checkLicense, getExportTypesRegistry } from './lib'; import { ESQueueInstance } from './lib/create_queue'; import { EnqueueJobFn } from './lib/enqueue_job'; -import { registerRoutes } from './routes'; -import { ReportingSetupDeps } from './types'; -interface ReportingInternalSetup { +export interface ReportingInternalSetup { browserDriverFactory: HeadlessChromiumDriverFactory; elasticsearch: ElasticsearchServiceSetup; + licensing: LicensingPluginSetup; + basePath: BasePath['get']; + router: IRouter; + security: SecurityPluginSetup; } + interface ReportingInternalStart { enqueueJob: EnqueueJobFn; esqueue: ESQueueInstance; @@ -46,30 +48,10 @@ export class ReportingCore { private readonly pluginStart$ = new Rx.ReplaySubject(); private exportTypesRegistry = getExportTypesRegistry(); - constructor(private logger: LevelLogger, private config: ReportingConfig) {} - - legacySetup( - xpackMainPlugin: XPackMainPlugin, - reporting: ReportingPluginSpecOptions, - __LEGACY: ServerFacade, - plugins: ReportingSetupDeps - ) { - // legacy plugin status - mirrorPluginStatus(xpackMainPlugin, reporting); - - // legacy license check - const checkLicense = checkLicenseFactory(this.exportTypesRegistry); - (xpackMainPlugin as any).status.once('green', () => { - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results for this plugin - xpackMainPlugin.info.feature(PLUGIN_ID).registerLicenseCheckResultsGenerator(checkLicense); - }); - - // legacy routes - registerRoutes(this, __LEGACY, plugins, this.logger); - } + constructor(private config: ReportingConfig) {} public pluginSetup(reportingSetupDeps: ReportingInternalSetup) { + this.pluginSetupDeps = reportingSetupDeps; this.pluginSetup$.next(reportingSetupDeps); } @@ -96,23 +78,35 @@ export class ReportingCore { return (await this.getPluginStartDeps()).enqueueJob; } - public getConfig() { + public async getLicenseInfo() { + const { licensing } = this.getPluginSetupDeps(); + return await licensing.license$ + .pipe( + map((license) => checkLicense(this.getExportTypesRegistry(), license)), + first() + ) + .toPromise(); + } + + public getConfig(): ReportingConfig { return this.config; } - public async getScreenshotsObservable() { - const { browserDriverFactory } = await this.getPluginSetupDeps(); + + public getScreenshotsObservable(): ScreenshotsObservableFn { + const { browserDriverFactory } = this.getPluginSetupDeps(); return screenshotsObservableFactory(this.config.get('capture'), browserDriverFactory); } + public getPluginSetupDeps() { + if (!this.pluginSetupDeps) { + throw new Error(`"pluginSetupDeps" dependencies haven't initialized yet`); + } + return this.pluginSetupDeps; + } + /* * Outside dependencies */ - private async getPluginSetupDeps() { - if (this.pluginSetupDeps) { - return this.pluginSetupDeps; - } - return await this.pluginSetup$.pipe(first()).toPromise(); - } private async getPluginStartDeps() { if (this.pluginStartDeps) { @@ -122,15 +116,15 @@ export class ReportingCore { } public async getElasticsearchService() { - return (await this.getPluginSetupDeps()).elasticsearch; + return this.getPluginSetupDeps().elasticsearch; } public async getSavedObjectsClient(fakeRequest: KibanaRequest) { const { savedObjects } = await this.getPluginStartDeps(); - return savedObjects.getScopedClient(fakeRequest) as SavedObjectsClient; + return savedObjects.getScopedClient(fakeRequest) as SavedObjectsClientContract; } - public async getUiSettingsServiceFactory(savedObjectsClient: SavedObjectsClient) { + public async getUiSettingsServiceFactory(savedObjectsClient: SavedObjectsClientContract) { const { uiSettings: uiSettingsService } = await this.getPluginStartDeps(); const scopedUiSettingsService = uiSettingsService.asScopedToClient(savedObjectsClient); return scopedUiSettingsService; diff --git a/x-pack/legacy/plugins/reporting/server/legacy.ts b/x-pack/legacy/plugins/reporting/server/legacy.ts index 37272325b97d0..14abd53cc83d9 100644 --- a/x-pack/legacy/plugins/reporting/server/legacy.ts +++ b/x-pack/legacy/plugins/reporting/server/legacy.ts @@ -7,8 +7,8 @@ import { Legacy } from 'kibana'; import { take } from 'rxjs/operators'; import { PluginInitializerContext } from 'src/core/server'; -import { ReportingPluginSpecOptions } from '../'; import { LicensingPluginSetup } from '../../../../plugins/licensing/server'; +import { ReportingPluginSpecOptions } from '../'; import { PluginsSetup } from '../../../../plugins/reporting/server'; import { SecurityPluginSetup } from '../../../../plugins/security/server'; import { buildConfig } from './config'; @@ -42,6 +42,7 @@ export const legacyInit = async ( server.newPlatform.coreContext as PluginInitializerContext, buildConfig(coreSetup, server, reportingConfig) ); + await pluginInstance.setup(coreSetup, { elasticsearch: coreSetup.elasticsearch, licensing: server.newPlatform.setup.plugins.licensing as LicensingPluginSetup, diff --git a/x-pack/legacy/plugins/reporting/server/lib/__tests__/check_license.js b/x-pack/legacy/plugins/reporting/server/lib/__tests__/check_license.js deleted file mode 100644 index 294a0df56756e..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/lib/__tests__/check_license.js +++ /dev/null @@ -1,147 +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 expect from '@kbn/expect'; -import { set } from 'lodash'; -import { checkLicenseFactory } from '../check_license'; - -describe('check_license', function () { - let mockLicenseInfo; - let checkLicense; - - beforeEach(() => { - mockLicenseInfo = {}; - checkLicense = checkLicenseFactory({ - getAll: () => [ - { - id: 'test', - name: 'Test Export Type', - jobType: 'testJobType', - }, - ], - }); - }); - - describe('license information is not available', () => { - beforeEach(() => (mockLicenseInfo.isAvailable = () => false)); - - it('should set management.showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).management.showLinks).to.be(true); - }); - - it('should set test.showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).test.showLinks).to.be(true); - }); - - it('should set management.enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).management.enableLinks).to.be(false); - }); - - it('should set test.enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).test.enableLinks).to.be(false); - }); - - it('should set management.jobTypes to undefined', () => { - expect(checkLicense(mockLicenseInfo).management.jobTypes).to.be(undefined); - }); - }); - - describe('license information is available', () => { - beforeEach(() => { - mockLicenseInfo.isAvailable = () => true; - set(mockLicenseInfo, 'license.getType', () => 'basic'); - }); - - describe('& license is > basic', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isOneOf', () => true)); - - describe('& license is active', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true)); - - it('should set management.showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).management.showLinks).to.be(true); - }); - - it('should set test.showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).test.showLinks).to.be(true); - }); - - it('should set management.enableLinks to true', () => { - expect(checkLicense(mockLicenseInfo).management.enableLinks).to.be(true); - }); - - it('should set test.enableLinks to true', () => { - expect(checkLicense(mockLicenseInfo).test.enableLinks).to.be(true); - }); - - it('should set management.jobTypes to contain testJobType', () => { - expect(checkLicense(mockLicenseInfo).management.jobTypes).to.contain('testJobType'); - }); - }); - - describe('& license is expired', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false)); - - it('should set management.showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).management.showLinks).to.be(true); - }); - - it('should set test.showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).test.showLinks).to.be(true); - }); - - it('should set management.enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).management.enableLinks).to.be(false); - }); - - it('should set test.enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).test.enableLinks).to.be(false); - }); - - it('should set management.jobTypes to undefined', () => { - expect(checkLicense(mockLicenseInfo).management.jobTypes).to.be(undefined); - }); - }); - }); - - describe('& license is basic', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isOneOf', () => false)); - - describe('& license is active', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true)); - - it('should set management.showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).management.showLinks).to.be(false); - }); - - it('should set test.showLinks to false', () => { - expect(checkLicense(mockLicenseInfo).test.showLinks).to.be(false); - }); - - it('should set management.jobTypes to an empty array', () => { - expect(checkLicense(mockLicenseInfo).management.jobTypes).to.be.an(Array); - expect(checkLicense(mockLicenseInfo).management.jobTypes).to.have.length(0); - }); - }); - - describe('& license is expired', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false)); - - it('should set management.showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).management.showLinks).to.be(true); - }); - - it('should set test.showLinks to false', () => { - expect(checkLicense(mockLicenseInfo).test.showLinks).to.be(false); - }); - - it('should set management.jobTypes to undefined', () => { - expect(checkLicense(mockLicenseInfo).management.jobTypes).to.be(undefined); - }); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/reporting/server/lib/check_license.test.ts b/x-pack/legacy/plugins/reporting/server/lib/check_license.test.ts new file mode 100644 index 0000000000000..366a8d94286f1 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/lib/check_license.test.ts @@ -0,0 +1,192 @@ +/* + * 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 { ILicense } from '../../../../../plugins/licensing/server'; +import { ExportTypesRegistry } from './export_types_registry'; + +describe('check_license', () => { + let exportTypesRegistry: ExportTypesRegistry; + let license: ILicense; + + beforeEach(() => { + exportTypesRegistry = ({ + getAll: () => [], + } as unknown) as ExportTypesRegistry; + }); + + describe('license information is not ready', () => { + beforeEach(() => { + exportTypesRegistry = ({ + getAll: () => [{ id: 'csv' }], + } as unknown) as ExportTypesRegistry; + }); + + it('should set management.showLinks to true', () => { + expect(checkLicense(exportTypesRegistry, undefined).management.showLinks).toEqual(true); + }); + + it('should set csv.showLinks to true', () => { + expect(checkLicense(exportTypesRegistry, undefined).csv.showLinks).toEqual(true); + }); + + it('should set management.enableLinks to false', () => { + expect(checkLicense(exportTypesRegistry, undefined).management.enableLinks).toEqual(false); + }); + + it('should set csv.enableLinks to false', () => { + expect(checkLicense(exportTypesRegistry, undefined).csv.enableLinks).toEqual(false); + }); + + it('should set management.jobTypes to undefined', () => { + expect(checkLicense(exportTypesRegistry, undefined).management.jobTypes).toEqual(undefined); + }); + }); + + describe('license information is not available', () => { + beforeEach(() => { + license = { + type: undefined, + } as ILicense; + exportTypesRegistry = ({ + getAll: () => [{ id: 'csv' }], + } as unknown) as ExportTypesRegistry; + }); + + it('should set management.showLinks to true', () => { + expect(checkLicense(exportTypesRegistry, license).management.showLinks).toEqual(true); + }); + + it('should set csv.showLinks to true', () => { + expect(checkLicense(exportTypesRegistry, license).csv.showLinks).toEqual(true); + }); + + it('should set management.enableLinks to false', () => { + expect(checkLicense(exportTypesRegistry, license).management.enableLinks).toEqual(false); + }); + + it('should set csv.enableLinks to false', () => { + expect(checkLicense(exportTypesRegistry, license).csv.enableLinks).toEqual(false); + }); + + it('should set management.jobTypes to undefined', () => { + expect(checkLicense(exportTypesRegistry, license).management.jobTypes).toEqual(undefined); + }); + }); + + describe('license information is available', () => { + beforeEach(() => { + license = {} as ILicense; + }); + + describe('& license is > basic', () => { + beforeEach(() => { + license.type = 'gold'; + exportTypesRegistry = ({ + getAll: () => [{ id: 'pdf', validLicenses: ['gold'], jobType: 'printable_pdf' }], + } as unknown) as ExportTypesRegistry; + }); + + describe('& license is active', () => { + beforeEach(() => (license.isActive = true)); + + it('should set management.showLinks to true', () => { + expect(checkLicense(exportTypesRegistry, license).management.showLinks).toEqual(true); + }); + + it('should setpdf.showLinks to true', () => { + expect(checkLicense(exportTypesRegistry, license).pdf.showLinks).toEqual(true); + }); + + it('should set management.enableLinks to true', () => { + expect(checkLicense(exportTypesRegistry, license).management.enableLinks).toEqual(true); + }); + + it('should setpdf.enableLinks to true', () => { + expect(checkLicense(exportTypesRegistry, license).pdf.enableLinks).toEqual(true); + }); + + it('should set management.jobTypes to contain testJobType', () => { + expect(checkLicense(exportTypesRegistry, license).management.jobTypes).toContain( + 'printable_pdf' + ); + }); + }); + + describe('& license is expired', () => { + beforeEach(() => { + license.isActive = false; + }); + + it('should set management.showLinks to true', () => { + expect(checkLicense(exportTypesRegistry, license).management.showLinks).toEqual(true); + }); + + it('should set pdf.showLinks to true', () => { + expect(checkLicense(exportTypesRegistry, license).pdf.showLinks).toEqual(true); + }); + + it('should set management.enableLinks to false', () => { + expect(checkLicense(exportTypesRegistry, license).management.enableLinks).toEqual(false); + }); + + it('should set pdf.enableLinks to false', () => { + expect(checkLicense(exportTypesRegistry, license).pdf.enableLinks).toEqual(false); + }); + + it('should set management.jobTypes to undefined', () => { + expect(checkLicense(exportTypesRegistry, license).management.jobTypes).toEqual(undefined); + }); + }); + }); + + describe('& license is basic', () => { + beforeEach(() => { + license.type = 'basic'; + exportTypesRegistry = ({ + getAll: () => [{ id: 'pdf', validLicenses: ['gold'], jobType: 'printable_pdf' }], + } as unknown) as ExportTypesRegistry; + }); + + describe('& license is active', () => { + beforeEach(() => { + license.isActive = true; + }); + + it('should set management.showLinks to false', () => { + expect(checkLicense(exportTypesRegistry, license).management.showLinks).toEqual(false); + }); + + it('should set test.showLinks to false', () => { + expect(checkLicense(exportTypesRegistry, license).pdf.showLinks).toEqual(false); + }); + + it('should set management.jobTypes to an empty array', () => { + expect(checkLicense(exportTypesRegistry, license).management.jobTypes).toEqual([]); + expect(checkLicense(exportTypesRegistry, license).management.jobTypes).toHaveLength(0); + }); + }); + + describe('& license is expired', () => { + beforeEach(() => { + license.isActive = false; + }); + + it('should set management.showLinks to true', () => { + expect(checkLicense(exportTypesRegistry, license).management.showLinks).toEqual(true); + }); + + it('should set test.showLinks to false', () => { + expect(checkLicense(exportTypesRegistry, license).pdf.showLinks).toEqual(false); + }); + + it('should set management.jobTypes to undefined', () => { + expect(checkLicense(exportTypesRegistry, license).management.jobTypes).toEqual(undefined); + }); + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/reporting/server/lib/check_license.ts b/x-pack/legacy/plugins/reporting/server/lib/check_license.ts index 80cf315539441..1b4eeaa0bae3e 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/check_license.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/check_license.ts @@ -4,23 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { XPackInfo } from '../../../xpack_main/server/lib/xpack_info'; -import { XPackInfoLicense } from '../../../xpack_main/server/lib/xpack_info_license'; +import { ILicense } from '../../../../../plugins/licensing/server'; import { ExportTypeDefinition } from '../types'; import { ExportTypesRegistry } from './export_types_registry'; -interface LicenseCheckResult { +export interface LicenseCheckResult { showLinks: boolean; enableLinks: boolean; message?: string; + jobTypes?: string[]; } const messages = { getUnavailable: () => { return 'You cannot use Reporting because license information is not available at this time.'; }, - getExpired: (license: XPackInfoLicense) => { - return `You cannot use Reporting because your ${license.getType()} license has expired.`; + getExpired: (license: ILicense) => { + return `You cannot use Reporting because your ${license.type} license has expired.`; }, }; @@ -29,8 +29,8 @@ const makeManagementFeature = ( ) => { return { id: 'management', - checkLicense: (license: XPackInfoLicense | null) => { - if (!license) { + checkLicense: (license?: ILicense) => { + if (!license || !license.type) { return { showLinks: true, enableLinks: false, @@ -38,7 +38,7 @@ const makeManagementFeature = ( }; } - if (!license.isActive()) { + if (!license.isActive) { return { showLinks: true, enableLinks: false, @@ -47,7 +47,7 @@ const makeManagementFeature = ( } const validJobTypes = exportTypes - .filter((exportType) => license.isOneOf(exportType.validLicenses)) + .filter((exportType) => exportType.validLicenses.includes(license.type || '')) .map((exportType) => exportType.jobType); return { @@ -64,8 +64,8 @@ const makeExportTypeFeature = ( ) => { return { id: exportType.id, - checkLicense: (license: XPackInfoLicense | null) => { - if (!license) { + checkLicense: (license?: ILicense) => { + if (!license || !license.type) { return { showLinks: true, enableLinks: false, @@ -73,17 +73,15 @@ const makeExportTypeFeature = ( }; } - if (!license.isOneOf(exportType.validLicenses)) { + if (!exportType.validLicenses.includes(license.type)) { return { showLinks: false, enableLinks: false, - message: `Your ${license.getType()} license does not support ${ - exportType.name - } Reporting. Please upgrade your license.`, + message: `Your ${license.type} license does not support ${exportType.name} Reporting. Please upgrade your license.`, }; } - if (!license.isActive()) { + if (!license.isActive) { return { showLinks: true, enableLinks: false, @@ -99,18 +97,18 @@ const makeExportTypeFeature = ( }; }; -export function checkLicenseFactory(exportTypesRegistry: ExportTypesRegistry) { - return function checkLicense(xpackInfo: XPackInfo) { - const license = xpackInfo === null || !xpackInfo.isAvailable() ? null : xpackInfo.license; - const exportTypes = Array.from(exportTypesRegistry.getAll()); - const reportingFeatures = [ - ...exportTypes.map(makeExportTypeFeature), - makeManagementFeature(exportTypes), - ]; +export function checkLicense( + exportTypesRegistry: ExportTypesRegistry, + license: ILicense | undefined +) { + const exportTypes = Array.from(exportTypesRegistry.getAll()); + const reportingFeatures = [ + ...exportTypes.map(makeExportTypeFeature), + makeManagementFeature(exportTypes), + ]; - return reportingFeatures.reduce((result, feature) => { - result[feature.id] = feature.checkLicense(license); - return result; - }, {} as Record); - }; + return reportingFeatures.reduce((result, feature) => { + result[feature.id] = feature.checkLicense(license); + return result; + }, {} as Record); } diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts b/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts index 2cac4bd654487..d993a17c0b314 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts @@ -52,7 +52,7 @@ export async function createQueueFactory( interval: queueIndexInterval, timeout: queueTimeout, dateSeparator: '.', - client: elasticsearch.dataClient, + client: elasticsearch.legacy.client, logger: createTaggedLogger(logger, ['esqueue', 'queue-worker']), }; diff --git a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts index 8ffb99f7a14c8..6367c8a1da98a 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts @@ -5,8 +5,9 @@ */ import { EventEmitter } from 'events'; -import { get } from 'lodash'; -import { ConditionalHeaders, ESQueueCreateJobFn, RequestFacade } from '../../server/types'; +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; +import { AuthenticatedUser } from '../../../../../plugins/security/server'; +import { ESQueueCreateJobFn } from '../../server/types'; import { ReportingCore } from '../core'; // @ts-ignore import { events as esqueueEvents } from './esqueue'; @@ -29,9 +30,9 @@ export type Job = EventEmitter & { export type EnqueueJobFn = ( exportTypeId: string, jobParams: JobParamsType, - user: string, - headers: Record, - request: RequestFacade + user: AuthenticatedUser | null, + context: RequestHandlerContext, + request: KibanaRequest ) => Promise; export function enqueueJobFactory( @@ -42,18 +43,17 @@ export function enqueueJobFactory( const queueTimeout = config.get('queue', 'timeout'); const browserType = config.get('capture', 'browser', 'type'); const maxAttempts = config.get('capture', 'maxAttempts'); - const logger = parentLogger.clone(['queue-job']); return async function enqueueJob( exportTypeId: string, jobParams: JobParamsType, - user: string, - headers: ConditionalHeaders['headers'], - request: RequestFacade + user: AuthenticatedUser | null, + context: RequestHandlerContext, + request: KibanaRequest ): Promise { type CreateJobFn = ESQueueCreateJobFn; - + const username = user ? user.username : false; const esqueue = await reporting.getEsqueue(); const exportType = reporting.getExportTypesRegistry().getById(exportTypeId); @@ -62,11 +62,11 @@ export function enqueueJobFactory( } const createJob = exportType.createJobFactory(reporting, logger) as CreateJobFn; - const payload = await createJob(jobParams, headers, request); + const payload = await createJob(jobParams, context, request); const options = { timeout: queueTimeout, - created_by: get(user, 'username', false), + created_by: username, browser_type: browserType, max_attempts: maxAttempts, }; diff --git a/x-pack/legacy/plugins/reporting/server/lib/get_user.ts b/x-pack/legacy/plugins/reporting/server/lib/get_user.ts index 8e8b1c83d5a40..164ffc5742d04 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/get_user.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/get_user.ts @@ -4,16 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Legacy } from 'kibana'; +import { SecurityPluginSetup } from '../../../../../plugins/security/server'; import { KibanaRequest } from '../../../../../../src/core/server'; -import { ReportingSetupDeps } from '../types'; -import { LevelLogger } from './level_logger'; -export function getUserFactory(security: ReportingSetupDeps['security'], logger: LevelLogger) { - /* - * Legacy.Request because this is called from routing middleware - */ - return async (request: Legacy.Request) => { - return security?.authc.getCurrentUser(KibanaRequest.from(request)) ?? null; +export function getUserFactory(security?: SecurityPluginSetup) { + return (request: KibanaRequest) => { + return security?.authc.getCurrentUser(request) ?? null; }; } diff --git a/x-pack/legacy/plugins/reporting/server/lib/index.ts b/x-pack/legacy/plugins/reporting/server/lib/index.ts index 2a8fa45b6fcef..0e9c49b170887 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/index.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/index.ts @@ -5,7 +5,7 @@ */ export { LevelLogger } from './level_logger'; -export { checkLicenseFactory } from './check_license'; +export { checkLicense } from './check_license'; export { createQueueFactory } from './create_queue'; export { cryptoFactory } from './crypto'; export { enqueueJobFactory } from './enqueue_job'; diff --git a/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts b/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts index 1abf58c29b481..06c4a7714099e 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts @@ -5,10 +5,10 @@ */ import { i18n } from '@kbn/i18n'; -import Boom from 'boom'; import { errors as elasticsearchErrors } from 'elasticsearch'; import { ElasticsearchServiceSetup } from 'kibana/server'; import { get } from 'lodash'; +import { AuthenticatedUser } from '../../../../../plugins/security/server'; import { ReportingConfig } from '../'; import { JobSource } from '../types'; @@ -40,16 +40,14 @@ interface CountAggResult { count: number; } +const getUsername = (user: AuthenticatedUser | null) => (user ? user.username : false); + export function jobsQueryFactory( config: ReportingConfig, elasticsearch: ElasticsearchServiceSetup ) { const index = config.get('index'); - const { callAsInternalUser } = elasticsearch.adminClient; - - function getUsername(user: any) { - return get(user, 'username', false); - } + const { callAsInternalUser } = elasticsearch.legacy.client; function execQuery(queryType: string, body: QueryBody) { const defaultBody: Record = { @@ -82,9 +80,14 @@ export function jobsQueryFactory( } return { - list(jobTypes: string[], user: any, page = 0, size = defaultSize, jobIds: string[] | null) { + list( + jobTypes: string[], + user: AuthenticatedUser | null, + page = 0, + size = defaultSize, + jobIds: string[] | null + ) { const username = getUsername(user); - const body: QueryBody = { size, from: size * page, @@ -108,9 +111,8 @@ export function jobsQueryFactory( return getHits(execQuery('search', body)); }, - count(jobTypes: string[], user: any) { + count(jobTypes: string[], user: AuthenticatedUser | null) { const username = getUsername(user); - const body: QueryBody = { query: { constant_score: { @@ -129,9 +131,12 @@ export function jobsQueryFactory( }); }, - get(user: any, id: string, opts: GetOpts = {}): Promise | void> { + get( + user: AuthenticatedUser | null, + id: string, + opts: GetOpts = {} + ): Promise | void> { if (!id) return Promise.resolve(); - const username = getUsername(user); const body: QueryBody = { @@ -164,14 +169,12 @@ export function jobsQueryFactory( const query = { id, index: deleteIndex }; return callAsInternalUser('delete', query); } catch (error) { - const wrappedError = new Error( + throw new Error( i18n.translate('xpack.reporting.jobsQuery.deleteError', { defaultMessage: 'Could not delete the report: {error}', values: { error: error.message }, }) ); - - throw Boom.boomify(wrappedError, { statusCode: error.status }); } }, }; diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.test.js b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.test.js index 2551fd48b91f3..f358021560cff 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.test.js +++ b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.test.js @@ -12,14 +12,16 @@ const ONE_HUNDRED_MEGABYTES = 104857600; describe('Reporting: Validate Max Content Length', () => { const elasticsearch = { - dataClient: { - callAsInternalUser: () => ({ - defaults: { - http: { - max_content_length: '100mb', + legacy: { + client: { + callAsInternalUser: () => ({ + defaults: { + http: { + max_content_length: '100mb', + }, }, - }, - }), + }), + }, }, }; @@ -34,14 +36,16 @@ describe('Reporting: Validate Max Content Length', () => { it('should log warning messages when reporting has a higher max-size than elasticsearch', async () => { const config = { get: sinon.stub().returns(FIVE_HUNDRED_MEGABYTES) }; const elasticsearch = { - dataClient: { - callAsInternalUser: () => ({ - defaults: { - http: { - max_content_length: '100mb', + legacy: { + client: { + callAsInternalUser: () => ({ + defaults: { + http: { + max_content_length: '100mb', + }, }, - }, - }), + }), + }, }, }; diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts index f6acf72612e01..6d34937d9bd75 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts @@ -18,7 +18,7 @@ export async function validateMaxContentLength( elasticsearch: ElasticsearchServiceSetup, logger: LevelLogger ) { - const { callAsInternalUser } = elasticsearch.dataClient; + const { callAsInternalUser } = elasticsearch.legacy.client; const elasticClusterSettingsResponse = await callAsInternalUser('cluster.getSettings', { includeDefaults: true, diff --git a/x-pack/legacy/plugins/reporting/server/plugin.ts b/x-pack/legacy/plugins/reporting/server/plugin.ts index 78c2ce5b9b106..5a407ad3e4c4a 100644 --- a/x-pack/legacy/plugins/reporting/server/plugin.ts +++ b/x-pack/legacy/plugins/reporting/server/plugin.ts @@ -8,10 +8,13 @@ import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core import { createBrowserDriverFactory } from './browsers'; import { ReportingConfig } from './config'; import { ReportingCore } from './core'; +import { registerRoutes } from './routes'; import { createQueueFactory, enqueueJobFactory, LevelLogger, runValidations } from './lib'; import { setFieldFormats } from './services'; import { ReportingSetup, ReportingSetupDeps, ReportingStart, ReportingStartDeps } from './types'; import { registerReportingUsageCollector } from './usage'; +// @ts-ignore no module definition +import { mirrorPluginStatus } from '../../../server/lib/mirror_plugin_status'; export class ReportingPlugin implements Plugin { @@ -22,24 +25,34 @@ export class ReportingPlugin constructor(context: PluginInitializerContext, config: ReportingConfig) { this.config = config; this.logger = new LevelLogger(context.logger.get('reporting')); - this.reportingCore = new ReportingCore(this.logger, this.config); + this.reportingCore = new ReportingCore(this.config); } public async setup(core: CoreSetup, plugins: ReportingSetupDeps) { const { config } = this; - const { elasticsearch, __LEGACY } = plugins; + const { elasticsearch, __LEGACY, licensing, security } = plugins; + const router = core.http.createRouter(); + const basePath = core.http.basePath.get; + const { xpack_main: xpackMainLegacy, reporting: reportingLegacy } = __LEGACY.plugins; - const browserDriverFactory = await createBrowserDriverFactory(config, this.logger); // required for validations :( - runValidations(config, elasticsearch, browserDriverFactory, this.logger); + // legacy plugin status + mirrorPluginStatus(xpackMainLegacy, reportingLegacy); - const { xpack_main: xpackMainLegacy, reporting: reportingLegacy } = __LEGACY.plugins; - this.reportingCore.legacySetup(xpackMainLegacy, reportingLegacy, __LEGACY, plugins); + const browserDriverFactory = await createBrowserDriverFactory(config, this.logger); + const deps = { + browserDriverFactory, + elasticsearch, + licensing, + basePath, + router, + security, + }; - // Register a function with server to manage the collection of usage stats - registerReportingUsageCollector(this.reportingCore, plugins); + runValidations(config, elasticsearch, browserDriverFactory, this.logger); - // regsister setup internals - this.reportingCore.pluginSetup({ browserDriverFactory, elasticsearch }); + this.reportingCore.pluginSetup(deps); + registerReportingUsageCollector(this.reportingCore, plugins); + registerRoutes(this.reportingCore, this.logger); return {}; } diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts b/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts index 3f79d51382a81..2a12a64d67a35 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts @@ -4,73 +4,53 @@ * you may not use this file except in compliance with the Elastic License. */ -import boom from 'boom'; -import Joi from 'joi'; -import { Legacy } from 'kibana'; import rison from 'rison-node'; +import { schema } from '@kbn/config-schema'; +import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; +import { HandlerErrorFunction, HandlerFunction } from './types'; import { ReportingCore } from '../'; import { API_BASE_URL } from '../../common/constants'; -import { LevelLogger as Logger } from '../lib'; -import { ReportingSetupDeps, ServerFacade } from '../types'; -import { makeRequestFacade } from './lib/make_request_facade'; -import { - GetRouteConfigFactoryFn, - getRouteConfigFactoryReportingPre, - RouteConfigFactory, -} from './lib/route_config_factories'; -import { HandlerErrorFunction, HandlerFunction, ReportingResponseToolkit } from './types'; const BASE_GENERATE = `${API_BASE_URL}/generate`; export function registerGenerateFromJobParams( reporting: ReportingCore, - server: ServerFacade, - plugins: ReportingSetupDeps, handler: HandlerFunction, - handleError: HandlerErrorFunction, - logger: Logger + handleError: HandlerErrorFunction ) { - const config = reporting.getConfig(); - const getRouteConfig = () => { - const getOriginalRouteConfig: GetRouteConfigFactoryFn = getRouteConfigFactoryReportingPre( - config, - plugins, - logger - ); - const routeConfigFactory: RouteConfigFactory = getOriginalRouteConfig( - ({ params: { exportType } }) => exportType - ); + const setupDeps = reporting.getPluginSetupDeps(); + const userHandler = authorizedUserPreRoutingFactory(reporting); + const { router } = setupDeps; - return { - ...routeConfigFactory, + router.post( + { + path: `${BASE_GENERATE}/{exportType}`, validate: { - params: Joi.object({ - exportType: Joi.string().required(), - }).required(), - payload: Joi.object({ - jobParams: Joi.string().optional().default(null), - }).allow(null), // allow optional payload - query: Joi.object({ - jobParams: Joi.string().default(null), - }).default(), + params: schema.object({ + exportType: schema.string({ minLength: 2 }), + }), + body: schema.nullable( + schema.object({ + jobParams: schema.maybe(schema.string()), + }) + ), + query: schema.nullable( + schema.object({ + jobParams: schema.string({ + defaultValue: '', + }), + }) + ), }, - }; - }; - - // generate report - server.route({ - path: `${BASE_GENERATE}/{exportType}`, - method: 'POST', - options: getRouteConfig(), - handler: async (legacyRequest: Legacy.Request, h: ReportingResponseToolkit) => { - const request = makeRequestFacade(legacyRequest); + }, + userHandler(async (user, context, req, res) => { let jobParamsRison: string | null; - if (request.payload) { - const { jobParams: jobParamsPayload } = request.payload as { jobParams: string }; + if (req.body) { + const { jobParams: jobParamsPayload } = req.body as { jobParams: string }; jobParamsRison = jobParamsPayload; } else { - const { jobParams: queryJobParams } = request.query as { jobParams: string }; + const { jobParams: queryJobParams } = req.query as { jobParams: string }; if (queryJobParams) { jobParamsRison = queryJobParams; } else { @@ -79,37 +59,46 @@ export function registerGenerateFromJobParams( } if (!jobParamsRison) { - throw boom.badRequest('A jobParams RISON string is required'); + return res.customError({ + statusCode: 400, + body: 'A jobParams RISON string is required in the querystring or POST body', + }); } - const { exportType } = request.params; + const { exportType } = req.params as { exportType: string }; let jobParams; - let response; + try { jobParams = rison.decode(jobParamsRison) as object | null; if (!jobParams) { - throw new Error('missing jobParams!'); + return res.customError({ + statusCode: 400, + body: 'Missing jobParams!', + }); } } catch (err) { - throw boom.badRequest(`invalid rison: ${jobParamsRison}`); + return res.customError({ + statusCode: 400, + body: `invalid rison: ${jobParamsRison}`, + }); } + try { - response = await handler(exportType, jobParams, legacyRequest, h); + return await handler(user, exportType, jobParams, context, req, res); } catch (err) { - throw handleError(exportType, err); + return handleError(res, err); } - return response; - }, - }); + }) + ); // Get route to generation endpoint: show error about GET method to user - server.route({ - path: `${BASE_GENERATE}/{p*}`, - method: 'GET', - handler: () => { - const err = boom.methodNotAllowed('GET is not allowed'); - err.output.headers.allow = 'POST'; - return err; + router.get( + { + path: `${BASE_GENERATE}/{p*}`, + validate: false, }, - }); + (context, req, res) => { + return res.customError({ statusCode: 405, body: 'GET is not allowed' }); + } + ); } diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject.ts b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject.ts index 03a893d1abeb4..4bc143b911572 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject.ts @@ -4,21 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Legacy } from 'kibana'; +import { schema } from '@kbn/config-schema'; import { get } from 'lodash'; +import { HandlerErrorFunction, HandlerFunction, QueuedJobPayload } from './types'; import { ReportingCore } from '../'; import { API_BASE_GENERATE_V1, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../common/constants'; import { getJobParamsFromRequest } from '../../export_types/csv_from_savedobject/server/lib/get_job_params_from_request'; -import { LevelLogger as Logger } from '../lib'; -import { ReportingSetupDeps, ServerFacade } from '../types'; -import { makeRequestFacade } from './lib/make_request_facade'; -import { getRouteOptionsCsv } from './lib/route_config_factories'; -import { - HandlerErrorFunction, - HandlerFunction, - QueuedJobPayload, - ReportingResponseToolkit, -} from './types'; +import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; /* * This function registers API Endpoints for queuing Reporting jobs. The API inputs are: @@ -31,22 +23,31 @@ import { */ export function registerGenerateCsvFromSavedObject( reporting: ReportingCore, - server: ServerFacade, - plugins: ReportingSetupDeps, handleRoute: HandlerFunction, - handleRouteError: HandlerErrorFunction, - logger: Logger + handleRouteError: HandlerErrorFunction ) { - const config = reporting.getConfig(); - const routeOptions = getRouteOptionsCsv(config, plugins, logger); - - server.route({ - path: `${API_BASE_GENERATE_V1}/csv/saved-object/{savedObjectType}:{savedObjectId}`, - method: 'POST', - options: routeOptions, - handler: async (legacyRequest: Legacy.Request, h: ReportingResponseToolkit) => { - const requestFacade = makeRequestFacade(legacyRequest); - + const setupDeps = reporting.getPluginSetupDeps(); + const userHandler = authorizedUserPreRoutingFactory(reporting); + const { router } = setupDeps; + router.post( + { + path: `${API_BASE_GENERATE_V1}/csv/saved-object/{savedObjectType}:{savedObjectId}`, + validate: { + params: schema.object({ + savedObjectType: schema.string({ minLength: 2 }), + savedObjectId: schema.string({ minLength: 2 }), + }), + body: schema.object({ + state: schema.object({}), + timerange: schema.object({ + timezone: schema.string({ defaultValue: 'UTC' }), + min: schema.nullable(schema.oneOf([schema.number(), schema.string({ minLength: 5 })])), + max: schema.nullable(schema.oneOf([schema.number(), schema.string({ minLength: 5 })])), + }), + }), + }, + }, + userHandler(async (user, context, req, res) => { /* * 1. Build `jobParams` object: job data that execution will need to reference in various parts of the lifecycle * 2. Pass the jobParams and other common params to `handleRoute`, a shared function to enqueue the job with the params @@ -54,19 +55,31 @@ export function registerGenerateCsvFromSavedObject( */ let result: QueuedJobPayload; try { - const jobParams = getJobParamsFromRequest(requestFacade, { isImmediate: false }); - result = await handleRoute(CSV_FROM_SAVEDOBJECT_JOB_TYPE, jobParams, legacyRequest, h); // pass the original request because the handler will make the request facade on its own + const jobParams = getJobParamsFromRequest(req, { isImmediate: false }); + result = await handleRoute( + user, + CSV_FROM_SAVEDOBJECT_JOB_TYPE, + jobParams, + context, + req, + res + ); } catch (err) { - throw handleRouteError(CSV_FROM_SAVEDOBJECT_JOB_TYPE, err); + return handleRouteError(res, err); } if (get(result, 'source.job') == null) { - throw new Error( - `The Export handler is expected to return a result with job info! ${result}` - ); + return res.badRequest({ + body: `The Export handler is expected to return a result with job info! ${result}`, + }); } - return result; - }, - }); + return res.ok({ + body: result, + headers: { + 'content-type': 'application/json', + }, + }); + }) + ); } diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts index 22aebb05cdf49..8a6d4553dfa9c 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts @@ -4,22 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ResponseObject } from 'hapi'; -import { Legacy } from 'kibana'; +import { schema } from '@kbn/config-schema'; import { ReportingCore } from '../'; +import { HandlerErrorFunction } from './types'; import { API_BASE_GENERATE_V1 } from '../../common/constants'; import { createJobFactory, executeJobFactory } from '../../export_types/csv_from_savedobject'; import { getJobParamsFromRequest } from '../../export_types/csv_from_savedobject/server/lib/get_job_params_from_request'; import { JobDocPayloadPanelCsv } from '../../export_types/csv_from_savedobject/types'; import { LevelLogger as Logger } from '../lib'; -import { JobDocOutput, ReportingSetupDeps, ServerFacade } from '../types'; -import { makeRequestFacade } from './lib/make_request_facade'; -import { getRouteOptionsCsv } from './lib/route_config_factories'; -import { ReportingResponseToolkit } from './types'; - -type ResponseFacade = ResponseObject & { - isBoom: boolean; -}; +import { JobDocOutput } from '../types'; +import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; /* * This function registers API Endpoints for immediate Reporting jobs. The API inputs are: @@ -32,61 +26,77 @@ type ResponseFacade = ResponseObject & { */ export function registerGenerateCsvFromSavedObjectImmediate( reporting: ReportingCore, - server: ServerFacade, - plugins: ReportingSetupDeps, + handleError: HandlerErrorFunction, parentLogger: Logger ) { - const config = reporting.getConfig(); - const routeOptions = getRouteOptionsCsv(config, plugins, parentLogger); + const setupDeps = reporting.getPluginSetupDeps(); + const userHandler = authorizedUserPreRoutingFactory(reporting); + const { router } = setupDeps; /* * CSV export with the `immediate` option does not queue a job with Reporting's ESQueue to run the job async. Instead, this does: * - re-use the createJob function to build up es query config * - re-use the executeJob function to run the scan and scroll queries and capture the entire CSV in a result object. */ - server.route({ - path: `${API_BASE_GENERATE_V1}/immediate/csv/saved-object/{savedObjectType}:{savedObjectId}`, - method: 'POST', - options: routeOptions, - handler: async (legacyRequest: Legacy.Request, h: ReportingResponseToolkit) => { - const request = makeRequestFacade(legacyRequest); + router.post( + { + path: `${API_BASE_GENERATE_V1}/immediate/csv/saved-object/{savedObjectType}:{savedObjectId}`, + validate: { + params: schema.object({ + savedObjectType: schema.string({ minLength: 5 }), + savedObjectId: schema.string({ minLength: 5 }), + }), + body: schema.object({ + state: schema.object({}, { unknowns: 'allow' }), + timerange: schema.object({ + timezone: schema.string({ defaultValue: 'UTC' }), + min: schema.nullable(schema.oneOf([schema.number(), schema.string({ minLength: 5 })])), + max: schema.nullable(schema.oneOf([schema.number(), schema.string({ minLength: 5 })])), + }), + }), + }, + }, + userHandler(async (user, context, req, res) => { const logger = parentLogger.clone(['savedobject-csv']); - const jobParams = getJobParamsFromRequest(request, { isImmediate: true }); + const jobParams = getJobParamsFromRequest(req, { isImmediate: true }); const createJobFn = createJobFactory(reporting, logger); const executeJobFn = await executeJobFactory(reporting, logger); // FIXME: does not "need" to be async - const jobDocPayload: JobDocPayloadPanelCsv = await createJobFn( - jobParams, - request.headers, - request - ); - const { - content_type: jobOutputContentType, - content: jobOutputContent, - size: jobOutputSize, - }: JobDocOutput = await executeJobFn(null, jobDocPayload, request); - logger.info(`Job output size: ${jobOutputSize} bytes`); + try { + const jobDocPayload: JobDocPayloadPanelCsv = await createJobFn( + jobParams, + req.headers, + context, + req + ); + const { + content_type: jobOutputContentType, + content: jobOutputContent, + size: jobOutputSize, + }: JobDocOutput = await executeJobFn(null, jobDocPayload, context, req); - /* - * ESQueue worker function defaults `content` to null, even if the - * executeJob returned undefined. - * - * This converts null to undefined so the value can be sent to h.response() - */ - if (jobOutputContent === null) { - logger.warn('CSV Job Execution created empty content result'); - } - const response = h - .response(jobOutputContent ? jobOutputContent : undefined) - .type(jobOutputContentType); + logger.info(`Job output size: ${jobOutputSize} bytes`); - // Set header for buffer download, not streaming - const { isBoom } = response as ResponseFacade; - if (isBoom == null) { - response.header('accept-ranges', 'none'); - } + /* + * ESQueue worker function defaults `content` to null, even if the + * executeJob returned undefined. + * + * This converts null to undefined so the value can be sent to h.response() + */ + if (jobOutputContent === null) { + logger.warn('CSV Job Execution created empty content result'); + } - return response; - }, - }); + return res.ok({ + body: jobOutputContent || '', + headers: { + 'content-type': jobOutputContentType, + 'accept-ranges': 'none', + }, + }); + } catch (err) { + return handleError(res, err); + } + }) + ); } diff --git a/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts b/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts index d767d37a477ab..87ac71e250d0c 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts @@ -4,140 +4,164 @@ * you may not use this file except in compliance with the Elastic License. */ -import Hapi from 'hapi'; -import { ReportingConfig, ReportingCore } from '../'; -import { createMockReportingCore } from '../../test_helpers'; -import { LevelLogger as Logger } from '../lib'; -import { ReportingSetupDeps, ServerFacade } from '../types'; +import supertest from 'supertest'; +import { UnwrapPromise } from '@kbn/utility-types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { setupServer } from 'src/core/server/saved_objects/routes/integration_tests/test_utils'; import { registerJobGenerationRoutes } from './generation'; +import { createMockReportingCore } from '../../test_helpers'; +import { ReportingCore } from '..'; +import { ExportTypesRegistry } from '../lib/export_types_registry'; +import { ExportTypeDefinition } from '../types'; +import { LevelLogger } from '../lib'; +import { of } from 'rxjs'; + +type setupServerReturn = UnwrapPromise>; + +describe('POST /api/reporting/generate', () => { + let server: setupServerReturn['server']; + let httpSetup: setupServerReturn['httpSetup']; + let exportTypesRegistry: ExportTypesRegistry; + let core: ReportingCore; + + const config = { + get: jest.fn().mockImplementation((...args) => { + const key = args.join('.'); + switch (key) { + case 'queue.indexInterval': + return 10000; + case 'queue.timeout': + return 10000; + case 'index': + return '.reporting'; + case 'queue.pollEnabled': + return false; + default: + return; + } + }), + kbnConfig: { get: jest.fn() }, + }; + const mockLogger = ({ + error: jest.fn(), + debug: jest.fn(), + } as unknown) as jest.Mocked; + + beforeEach(async () => { + ({ server, httpSetup } = await setupServer()); + const mockDeps = ({ + elasticsearch: { + legacy: { + client: { callAsInternalUser: jest.fn() }, + }, + }, + security: { + authc: { + getCurrentUser: () => ({ + id: '123', + roles: ['superuser'], + username: 'Tom Riddle', + }), + }, + }, + router: httpSetup.createRouter(''), + licensing: { + license$: of({ + isActive: true, + isAvailable: true, + type: 'gold', + }), + }, + } as unknown) as any; + core = await createMockReportingCore(config, mockDeps); + exportTypesRegistry = new ExportTypesRegistry(); + exportTypesRegistry.register({ + id: 'printablePdf', + jobType: 'printable_pdf', + jobContentEncoding: 'base64', + jobContentExtension: 'pdf', + validLicenses: ['basic', 'gold'], + } as ExportTypeDefinition); + core.getExportTypesRegistry = () => exportTypesRegistry; + }); -jest.mock('./lib/authorized_user_pre_routing', () => ({ - authorizedUserPreRoutingFactory: () => () => ({}), -})); -jest.mock('./lib/reporting_feature_pre_routing', () => ({ - reportingFeaturePreRoutingFactory: () => () => () => ({ - jobTypes: ['unencodedJobType', 'base64EncodedJobType'], - }), -})); - -let mockServer: Hapi.Server; -let mockReportingPlugin: ReportingCore; -let mockReportingConfig: ReportingConfig; - -const mockLogger = ({ - error: jest.fn(), - debug: jest.fn(), -} as unknown) as Logger; - -beforeEach(async () => { - mockServer = new Hapi.Server({ - debug: false, - port: 8080, - routes: { log: { collect: true } }, + afterEach(async () => { + mockLogger.debug.mockReset(); + mockLogger.error.mockReset(); + await server.stop(); }); - mockReportingConfig = { get: jest.fn(), kbnConfig: { get: jest.fn() } }; - mockReportingPlugin = await createMockReportingCore(mockReportingConfig); - mockReportingPlugin.getEnqueueJob = async () => - jest.fn().mockImplementation(() => ({ toJSON: () => '{ "job": "data" }' })); -}); + it('returns 400 if there are no job params', async () => { + registerJobGenerationRoutes(core, mockLogger); -const mockPlugins = { - elasticsearch: { - adminClient: { callAsInternalUser: jest.fn() }, - }, - security: null, -}; - -const getErrorsFromRequest = (request: Hapi.Request) => { - // @ts-ignore error property doesn't exist on RequestLog - return request.logs.filter((log) => log.tags.includes('error')).map((log) => log.error); // NOTE: error stack is available -}; - -test(`returns 400 if there are no job params`, async () => { - registerJobGenerationRoutes( - mockReportingPlugin, - (mockServer as unknown) as ServerFacade, - (mockPlugins as unknown) as ReportingSetupDeps, - mockLogger - ); - - const options = { - method: 'POST', - url: '/api/reporting/generate/printablePdf', - }; + await server.start(); - const { payload, request } = await mockServer.inject(options); - expect(payload).toMatchInlineSnapshot( - `"{\\"statusCode\\":400,\\"error\\":\\"Bad Request\\",\\"message\\":\\"A jobParams RISON string is required\\"}"` - ); - - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toMatchInlineSnapshot(` - Array [ - [Error: A jobParams RISON string is required], - ] - `); -}); + await supertest(httpSetup.server.listener) + .post('/api/reporting/generate/printablePdf') + .expect(400) + .then(({ body }) => + expect(body.message).toMatchInlineSnapshot( + '"A jobParams RISON string is required in the querystring or POST body"' + ) + ); + }); -test(`returns 400 if job params is invalid`, async () => { - registerJobGenerationRoutes( - mockReportingPlugin, - (mockServer as unknown) as ServerFacade, - (mockPlugins as unknown) as ReportingSetupDeps, - mockLogger - ); - - const options = { - method: 'POST', - url: '/api/reporting/generate/printablePdf', - payload: { jobParams: `foo:` }, - }; + it('returns 400 if job params query is invalid', async () => { + registerJobGenerationRoutes(core, mockLogger); - const { payload, request } = await mockServer.inject(options); - expect(payload).toMatchInlineSnapshot( - `"{\\"statusCode\\":400,\\"error\\":\\"Bad Request\\",\\"message\\":\\"invalid rison: foo:\\"}"` - ); - - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toMatchInlineSnapshot(` - Array [ - [Error: invalid rison: foo:], - ] - `); -}); + await server.start(); -test(`returns 500 if job handler throws an error`, async () => { - mockReportingPlugin.getEnqueueJob = async () => - jest.fn().mockImplementation(() => ({ - toJSON: () => { - throw new Error('you found me'); - }, - })); - - registerJobGenerationRoutes( - mockReportingPlugin, - (mockServer as unknown) as ServerFacade, - (mockPlugins as unknown) as ReportingSetupDeps, - mockLogger - ); - - const options = { - method: 'POST', - url: '/api/reporting/generate/printablePdf', - payload: { jobParams: `abc` }, - }; + await supertest(httpSetup.server.listener) + .post('/api/reporting/generate/printablePdf?jobParams=foo:') + .expect(400) + .then(({ body }) => expect(body.message).toMatchInlineSnapshot('"invalid rison: foo:"')); + }); + + it('returns 400 if job params body is invalid', async () => { + registerJobGenerationRoutes(core, mockLogger); + + await server.start(); + + await supertest(httpSetup.server.listener) + .post('/api/reporting/generate/printablePdf') + .send({ jobParams: `foo:` }) + .expect(400) + .then(({ body }) => expect(body.message).toMatchInlineSnapshot('"invalid rison: foo:"')); + }); + + it('returns 400 export type is invalid', async () => { + registerJobGenerationRoutes(core, mockLogger); + + await server.start(); - const { payload, request } = await mockServer.inject(options); - expect(payload).toMatchInlineSnapshot( - `"{\\"statusCode\\":500,\\"error\\":\\"Internal Server Error\\",\\"message\\":\\"An internal server error occurred\\"}"` - ); - - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toMatchInlineSnapshot(` - Array [ - [Error: you found me], - [Error: you found me], - ] - `); + await supertest(httpSetup.server.listener) + .post('/api/reporting/generate/TonyHawksProSkater2') + .send({ jobParams: `abc` }) + .expect(400) + .then(({ body }) => + expect(body.message).toMatchInlineSnapshot('"Invalid export-type of TonyHawksProSkater2"') + ); + }); + + it('returns 400 if job handler throws an error', async () => { + const errorText = 'you found me'; + core.getEnqueueJob = async () => + jest.fn().mockImplementation(() => ({ + toJSON: () => { + throw new Error(errorText); + }, + })); + + registerJobGenerationRoutes(core, mockLogger); + + await server.start(); + + await supertest(httpSetup.server.listener) + .post('/api/reporting/generate/printablePdf') + .send({ jobParams: `abc` }) + .expect(400) + .then(({ body }) => { + expect(body.message).toMatchInlineSnapshot(`"${errorText}"`); + }); + }); }); diff --git a/x-pack/legacy/plugins/reporting/server/routes/generation.ts b/x-pack/legacy/plugins/reporting/server/routes/generation.ts index 56faa37d5fcbd..f2e616c0803a7 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generation.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generation.ts @@ -4,27 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import boom from 'boom'; +import Boom from 'boom'; import { errors as elasticsearchErrors } from 'elasticsearch'; -import { Legacy } from 'kibana'; +import { kibanaResponseFactory } from 'src/core/server'; import { ReportingCore } from '../'; import { API_BASE_URL } from '../../common/constants'; import { LevelLogger as Logger } from '../lib'; -import { ReportingSetupDeps, ServerFacade } from '../types'; import { registerGenerateFromJobParams } from './generate_from_jobparams'; import { registerGenerateCsvFromSavedObject } from './generate_from_savedobject'; import { registerGenerateCsvFromSavedObjectImmediate } from './generate_from_savedobject_immediate'; -import { makeRequestFacade } from './lib/make_request_facade'; -import { ReportingResponseToolkit } from './types'; +import { HandlerFunction } from './types'; const esErrors = elasticsearchErrors as Record; -export function registerJobGenerationRoutes( - reporting: ReportingCore, - server: ServerFacade, - plugins: ReportingSetupDeps, - logger: Logger -) { +export function registerJobGenerationRoutes(reporting: ReportingCore, logger: Logger) { const config = reporting.getConfig(); const downloadBaseUrl = config.kbnConfig.get('server', 'basePath') + `${API_BASE_URL}/jobs/download`; @@ -32,48 +25,71 @@ export function registerJobGenerationRoutes( /* * Generates enqueued job details to use in responses */ - async function handler( - exportTypeId: string, - jobParams: object, - legacyRequest: Legacy.Request, - h: ReportingResponseToolkit - ) { - const request = makeRequestFacade(legacyRequest); - const user = request.pre.user; - const headers = request.headers; + const handler: HandlerFunction = async (user, exportTypeId, jobParams, context, req, res) => { + const licenseInfo = await reporting.getLicenseInfo(); + const licenseResults = licenseInfo[exportTypeId]; + + if (!licenseResults) { + return res.badRequest({ body: `Invalid export-type of ${exportTypeId}` }); + } + + if (!licenseResults.enableLinks) { + return res.forbidden({ body: licenseResults.message }); + } const enqueueJob = await reporting.getEnqueueJob(); - const job = await enqueueJob(exportTypeId, jobParams, user, headers, request); + const job = await enqueueJob(exportTypeId, jobParams, user, context, req); // return the queue's job information const jobJson = job.toJSON(); - return h - .response({ + return res.ok({ + headers: { + 'content-type': 'application/json', + }, + body: { path: `${downloadBaseUrl}/${jobJson.id}`, job: jobJson, - }) - .type('application/json'); - } + }, + }); + }; + + function handleError(res: typeof kibanaResponseFactory, err: Error | Boom) { + if (err instanceof Boom) { + return res.customError({ + statusCode: err.output.statusCode, + body: err.output.payload.message, + }); + } - function handleError(exportTypeId: string, err: Error) { if (err instanceof esErrors['401']) { - return boom.unauthorized(`Sorry, you aren't authenticated`); + return res.unauthorized({ + body: `Sorry, you aren't authenticated`, + }); } + if (err instanceof esErrors['403']) { - return boom.forbidden(`Sorry, you are not authorized to create ${exportTypeId} reports`); + return res.forbidden({ + body: `Sorry, you are not authorized`, + }); } + if (err instanceof esErrors['404']) { - return boom.boomify(err, { statusCode: 404 }); + return res.notFound({ + body: err.message, + }); } - return err; + + return res.badRequest({ + body: err.message, + }); } - registerGenerateFromJobParams(reporting, server, plugins, handler, handleError, logger); + registerGenerateFromJobParams(reporting, handler, handleError); // Register beta panel-action download-related API's if (config.get('csv', 'enablePanelActionDownload')) { - registerGenerateCsvFromSavedObject(reporting, server, plugins, handler, handleError, logger); - registerGenerateCsvFromSavedObjectImmediate(reporting, server, plugins, logger); + registerGenerateCsvFromSavedObject(reporting, handler, handleError); + registerGenerateCsvFromSavedObjectImmediate(reporting, handleError, logger); } } diff --git a/x-pack/legacy/plugins/reporting/server/routes/index.ts b/x-pack/legacy/plugins/reporting/server/routes/index.ts index 556f4e12b077e..005d82086665c 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/index.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/index.ts @@ -4,18 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReportingCore } from '../'; import { LevelLogger as Logger } from '../lib'; -import { ReportingSetupDeps, ServerFacade } from '../types'; import { registerJobGenerationRoutes } from './generation'; import { registerJobInfoRoutes } from './jobs'; +import { ReportingCore } from '../core'; -export function registerRoutes( - reporting: ReportingCore, - server: ServerFacade, - plugins: ReportingSetupDeps, - logger: Logger -) { - registerJobGenerationRoutes(reporting, server, plugins, logger); - registerJobInfoRoutes(reporting, server, plugins, logger); +export function registerRoutes(reporting: ReportingCore, logger: Logger) { + registerJobGenerationRoutes(reporting, logger); + registerJobInfoRoutes(reporting); } diff --git a/x-pack/legacy/plugins/reporting/server/routes/jobs.test.ts b/x-pack/legacy/plugins/reporting/server/routes/jobs.test.ts index 4f597bcee858e..0911f48f82ca4 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/jobs.test.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/jobs.test.ts @@ -4,327 +4,347 @@ * you may not use this file except in compliance with the Elastic License. */ -import Hapi from 'hapi'; -import { ReportingConfig, ReportingCore } from '../'; -import { LevelLogger } from '../lib'; +import supertest from 'supertest'; +import { UnwrapPromise } from '@kbn/utility-types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { setupServer } from 'src/core/server/saved_objects/routes/integration_tests/test_utils'; +import { registerJobInfoRoutes } from './jobs'; import { createMockReportingCore } from '../../test_helpers'; +import { ReportingCore } from '..'; import { ExportTypesRegistry } from '../lib/export_types_registry'; -import { ExportTypeDefinition, ReportingSetupDeps } from '../types'; -import { registerJobInfoRoutes } from './jobs'; - -jest.mock('./lib/authorized_user_pre_routing', () => ({ - authorizedUserPreRoutingFactory: () => () => ({}), -})); -jest.mock('./lib/reporting_feature_pre_routing', () => ({ - reportingFeaturePreRoutingFactory: () => () => () => ({ - jobTypes: ['unencodedJobType', 'base64EncodedJobType'], - }), -})); - -let mockServer: any; -let exportTypesRegistry: ExportTypesRegistry; -let mockReportingPlugin: ReportingCore; -let mockReportingConfig: ReportingConfig; -const mockLogger = ({ - error: jest.fn(), - debug: jest.fn(), -} as unknown) as LevelLogger; - -beforeEach(async () => { - mockServer = new Hapi.Server({ debug: false, port: 8080, routes: { log: { collect: true } } }); - exportTypesRegistry = new ExportTypesRegistry(); - exportTypesRegistry.register({ - id: 'unencoded', - jobType: 'unencodedJobType', - jobContentExtension: 'csv', - } as ExportTypeDefinition); - exportTypesRegistry.register({ - id: 'base64Encoded', - jobType: 'base64EncodedJobType', - jobContentEncoding: 'base64', - jobContentExtension: 'pdf', - } as ExportTypeDefinition); - - mockReportingConfig = { get: jest.fn(), kbnConfig: { get: jest.fn() } }; - mockReportingPlugin = await createMockReportingCore(mockReportingConfig); - mockReportingPlugin.getExportTypesRegistry = () => exportTypesRegistry; -}); - -const mockPlugins = ({ - elasticsearch: { - adminClient: { callAsInternalUser: jest.fn() }, - }, - security: null, -} as unknown) as ReportingSetupDeps; - -const getHits = (...sources: any) => { - return { - hits: { - hits: sources.map((source: object) => ({ _source: source })), - }, - }; -}; - -const getErrorsFromRequest = (request: any) => - request.logs.filter((log: any) => log.tags.includes('error')).map((log: any) => log.error); - -test(`returns 404 if job not found`, async () => { - // @ts-ignore - mockPlugins.elasticsearch.adminClient = { - callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(getHits())), - }; - - registerJobInfoRoutes(mockReportingPlugin, mockServer, mockPlugins, mockLogger); - - const request = { - method: 'GET', - url: '/api/reporting/jobs/download/1', - }; - - const response = await mockServer.inject(request); - const { statusCode } = response; - expect(statusCode).toBe(404); -}); - -test(`returns 401 if not valid job type`, async () => { - // @ts-ignore - mockPlugins.elasticsearch.adminClient = { - callAsInternalUser: jest - .fn() - .mockReturnValue(Promise.resolve(getHits({ jobtype: 'invalidJobType' }))), - }; - - registerJobInfoRoutes(mockReportingPlugin, mockServer, mockPlugins, mockLogger); - - const request = { - method: 'GET', - url: '/api/reporting/jobs/download/1', +import { ExportTypeDefinition } from '../types'; +import { LevelLogger } from '../lib'; +import { ReportingInternalSetup } from '../core'; +import { of } from 'rxjs'; + +type setupServerReturn = UnwrapPromise>; + +describe('GET /api/reporting/jobs/download', () => { + let server: setupServerReturn['server']; + let httpSetup: setupServerReturn['httpSetup']; + let exportTypesRegistry: ExportTypesRegistry; + let core: ReportingCore; + + const config = { get: jest.fn(), kbnConfig: { get: jest.fn() } }; + const mockLogger = ({ + error: jest.fn(), + debug: jest.fn(), + } as unknown) as jest.Mocked; + + const getHits = (...sources: any) => { + return { + hits: { + hits: sources.map((source: object) => ({ _source: source })), + }, + }; }; - const { statusCode } = await mockServer.inject(request); - expect(statusCode).toBe(401); -}); - -describe(`when job is incomplete`, () => { - const getIncompleteResponse = async () => { + beforeEach(async () => { + ({ server, httpSetup } = await setupServer()); + core = await createMockReportingCore(config, ({ + elasticsearch: { + legacy: { client: { callAsInternalUser: jest.fn() } }, + }, + security: { + authc: { + getCurrentUser: () => ({ + id: '123', + roles: ['superuser'], + username: 'Tom Riddle', + }), + }, + }, + router: httpSetup.createRouter(''), + licensing: { + license$: of({ + isActive: true, + isAvailable: true, + type: 'gold', + }), + }, + } as unknown) as ReportingInternalSetup); // @ts-ignore - mockPlugins.elasticsearch.adminClient = { - callAsInternalUser: jest - .fn() - .mockReturnValue( - Promise.resolve(getHits({ jobtype: 'unencodedJobType', status: 'pending' })) - ), - }; + exportTypesRegistry = new ExportTypesRegistry(); + exportTypesRegistry.register({ + id: 'unencoded', + jobType: 'unencodedJobType', + jobContentExtension: 'csv', + validLicenses: ['basic', 'gold'], + } as ExportTypeDefinition); + exportTypesRegistry.register({ + id: 'base64Encoded', + jobType: 'base64EncodedJobType', + jobContentEncoding: 'base64', + jobContentExtension: 'pdf', + validLicenses: ['basic', 'gold'], + } as ExportTypeDefinition); + core.getExportTypesRegistry = () => exportTypesRegistry; + }); - registerJobInfoRoutes(mockReportingPlugin, mockServer, mockPlugins, mockLogger); + afterEach(async () => { + mockLogger.debug.mockReset(); + mockLogger.error.mockReset(); + await server.stop(); + }); - const request = { - method: 'GET', - url: '/api/reporting/jobs/download/1', + it('fails on malformed download IDs', async () => { + // @ts-ignore + core.pluginSetupDeps.elasticsearch.legacy.client = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(getHits())), }; + registerJobInfoRoutes(core); - return await mockServer.inject(request); - }; + await server.start(); - test(`sets statusCode to 503`, async () => { - const { statusCode } = await getIncompleteResponse(); - expect(statusCode).toBe(503); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/1') + .expect(400) + .then(({ body }) => + expect(body.message).toMatchInlineSnapshot( + '"[request params.docId]: value has length [1] but it must have a minimum length of [3]."' + ) + ); }); - test(`uses status as payload`, async () => { - const { payload } = await getIncompleteResponse(); - expect(payload).toBe('pending'); - }); + it('fails on unauthenticated users', async () => { + // @ts-ignore + core.pluginSetupDeps = ({ + // @ts-ignore + ...core.pluginSetupDeps, + security: { + authc: { + getCurrentUser: () => undefined, + }, + }, + } as unknown) as ReportingInternalSetup; + registerJobInfoRoutes(core); - test(`sets content-type header to application/json; charset=utf-8`, async () => { - const { headers } = await getIncompleteResponse(); - expect(headers['content-type']).toBe('application/json; charset=utf-8'); - }); + await server.start(); - test(`sets retry-after header to 30`, async () => { - const { headers } = await getIncompleteResponse(); - expect(headers['retry-after']).toBe(30); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dope') + .expect(401) + .then(({ body }) => + expect(body.message).toMatchInlineSnapshot(`"Sorry, you aren't authenticated"`) + ); }); -}); -describe(`when job is failed`, () => { - const getFailedResponse = async () => { - const hits = getHits({ - jobtype: 'unencodedJobType', - status: 'failed', - output: { content: 'job failure message' }, - }); + it('fails on users without the appropriate role', async () => { // @ts-ignore - mockPlugins.elasticsearch.adminClient = { - callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), - }; - - registerJobInfoRoutes(mockReportingPlugin, mockServer, mockPlugins, mockLogger); - - const request = { - method: 'GET', - url: '/api/reporting/jobs/download/1', - }; - - return await mockServer.inject(request); - }; - - test(`sets status code to 500`, async () => { - const { statusCode } = await getFailedResponse(); - expect(statusCode).toBe(500); - }); + core.pluginSetupDeps = ({ + // @ts-ignore + ...core.pluginSetupDeps, + security: { + authc: { + getCurrentUser: () => ({ + id: '123', + roles: ['peasant'], + username: 'Tom Riddle', + }), + }, + }, + } as unknown) as ReportingInternalSetup; + registerJobInfoRoutes(core); - test(`sets content-type header to application/json; charset=utf-8`, async () => { - const { headers } = await getFailedResponse(); - expect(headers['content-type']).toBe('application/json; charset=utf-8'); - }); + await server.start(); - test(`sets the payload.reason to the job content`, async () => { - const { payload } = await getFailedResponse(); - expect(JSON.parse(payload).reason).toBe('job failure message'); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dope') + .expect(403) + .then(({ body }) => + expect(body.message).toMatchInlineSnapshot(`"Sorry, you don't have access to Reporting"`) + ); }); -}); -describe(`when job is completed`, () => { - const getCompletedResponse = async ({ - jobType = 'unencodedJobType', - outputContent = 'job output content', - outputContentType = 'application/pdf', - title = '', - } = {}) => { - const hits = getHits({ - jobtype: jobType, - status: 'completed', - output: { content: outputContent, content_type: outputContentType }, - payload: { - title, - }, - }); + it('returns 404 if job not found', async () => { // @ts-ignore - mockPlugins.elasticsearch.adminClient = { - callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), + core.pluginSetupDeps.elasticsearch.legacy.client = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(getHits())), }; - registerJobInfoRoutes(mockReportingPlugin, mockServer, mockPlugins, mockLogger); + registerJobInfoRoutes(core); - const request = { - method: 'GET', - url: '/api/reporting/jobs/download/1', - }; - - return await mockServer.inject(request); - }; + await server.start(); - test(`sets statusCode to 200`, async () => { - const { statusCode, request } = await getCompletedResponse(); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); - expect(statusCode).toBe(200); + await supertest(httpSetup.server.listener).get('/api/reporting/jobs/download/poo').expect(404); }); - test(`doesn't encode output content for not-specified jobTypes`, async () => { - const { payload, request } = await getCompletedResponse({ - jobType: 'unencodedJobType', - outputContent: 'test', - }); + it('returns a 401 if not a valid job type', async () => { + // @ts-ignore + core.pluginSetupDeps.elasticsearch.legacy.client = { + callAsInternalUser: jest + .fn() + .mockReturnValue(Promise.resolve(getHits({ jobtype: 'invalidJobType' }))), + }; + registerJobInfoRoutes(core); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); + await server.start(); - expect(payload).toBe('test'); + await supertest(httpSetup.server.listener).get('/api/reporting/jobs/download/poo').expect(401); }); - test(`base64 encodes output content for configured jobTypes`, async () => { - const { payload, request } = await getCompletedResponse({ - jobType: 'base64EncodedJobType', - outputContent: 'test', - }); - - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); + it('when a job is incomplete', async () => { + // @ts-ignore + core.pluginSetupDeps.elasticsearch.legacy.client = { + callAsInternalUser: jest + .fn() + .mockReturnValue( + Promise.resolve(getHits({ jobtype: 'unencodedJobType', status: 'pending' })) + ), + }; + registerJobInfoRoutes(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dank') + .expect(503) + .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('Retry-After', '30') + .then(({ text }) => expect(text).toEqual('pending')); + }); - expect(payload).toBe(Buffer.from('test', 'base64').toString()); + it('when a job fails', async () => { + // @ts-ignore + core.pluginSetupDeps.elasticsearch.legacy.client = { + callAsInternalUser: jest.fn().mockReturnValue( + Promise.resolve( + getHits({ + jobtype: 'unencodedJobType', + status: 'failed', + output: { content: 'job failure message' }, + }) + ) + ), + }; + registerJobInfoRoutes(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dank') + .expect(500) + .expect('Content-Type', 'application/json; charset=utf-8') + .then(({ body }) => + expect(body.message).toEqual('Reporting generation failed: job failure message') + ); }); - test(`specifies text/csv; charset=utf-8 contentType header from the job output`, async () => { - const { headers, request } = await getCompletedResponse({ outputContentType: 'text/csv' }); + describe('successful downloads', () => { + const getCompleteHits = async ({ + jobType = 'unencodedJobType', + outputContent = 'job output content', + outputContentType = 'text/plain', + title = '', + } = {}) => { + return getHits({ + jobtype: jobType, + status: 'completed', + output: { content: outputContent, content_type: outputContentType }, + payload: { + title, + }, + }); + }; - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); + it('when a known job-type is complete', async () => { + const hits = getCompleteHits(); + // @ts-ignore + core.pluginSetupDeps.elasticsearch.legacy.client = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), + }; + registerJobInfoRoutes(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dank') + .expect(200) + .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('content-disposition', 'inline; filename="report.csv"'); + }); - expect(headers['content-type']).toBe('text/csv; charset=utf-8'); - }); + it('succeeds when security is not there or disabled', async () => { + const hits = getCompleteHits(); + // @ts-ignore + core.pluginSetupDeps.elasticsearch.legacy.client = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), + }; - test(`specifies default filename in content-disposition header if no title`, async () => { - const { headers, request } = await getCompletedResponse({}); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); - expect(headers['content-disposition']).toBe('inline; filename="report.csv"'); - }); + // @ts-ignore + core.pluginSetupDeps.security = null; - test(`specifies payload title in content-disposition header`, async () => { - const { headers, request } = await getCompletedResponse({ title: 'something' }); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); - expect(headers['content-disposition']).toBe('inline; filename="something.csv"'); - }); + registerJobInfoRoutes(core); - test(`specifies jobContentExtension in content-disposition header`, async () => { - const { headers, request } = await getCompletedResponse({ jobType: 'base64EncodedJobType' }); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); - expect(headers['content-disposition']).toBe('inline; filename="report.pdf"'); - }); + await server.start(); - test(`specifies application/pdf contentType header from the job output`, async () => { - const { headers, request } = await getCompletedResponse({ - outputContentType: 'application/pdf', + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dope') + .expect(200) + .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('content-disposition', 'inline; filename="report.csv"'); }); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toEqual([]); - expect(headers['content-type']).toBe('application/pdf'); - }); - describe(`when non-whitelisted contentType specified in job output`, () => { - test(`sets statusCode to 500`, async () => { - const { statusCode, request } = await getCompletedResponse({ - outputContentType: 'application/html', + it(`doesn't encode output-content for non-specified job-types`, async () => { + const hits = getCompleteHits({ + jobType: 'unencodedJobType', + outputContent: 'test', }); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toMatchInlineSnapshot(` - Array [ - [Error: Unsupported content-type of application/html specified by job output], - [Error: Unsupported content-type of application/html specified by job output], - ] - `); - expect(statusCode).toBe(500); + // @ts-ignore + core.pluginSetupDeps.elasticsearch.legacy.client = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), + }; + registerJobInfoRoutes(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dank') + .expect(200) + .expect('Content-Type', 'text/plain; charset=utf-8') + .then(({ text }) => expect(text).toEqual('test')); }); - test(`doesn't include job output content in payload`, async () => { - const { payload, request } = await getCompletedResponse({ - outputContentType: 'application/html', + it(`base64 encodes output content for configured jobTypes`, async () => { + const hits = getCompleteHits({ + jobType: 'base64EncodedJobType', + outputContent: 'test', + outputContentType: 'application/pdf', }); - expect(payload).toMatchInlineSnapshot( - `"{\\"statusCode\\":500,\\"error\\":\\"Internal Server Error\\",\\"message\\":\\"An internal server error occurred\\"}"` - ); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toMatchInlineSnapshot(` - Array [ - [Error: Unsupported content-type of application/html specified by job output], - [Error: Unsupported content-type of application/html specified by job output], - ] - `); + // @ts-ignore + core.pluginSetupDeps.elasticsearch.legacy.client = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), + }; + registerJobInfoRoutes(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dank') + .expect(200) + .expect('Content-Type', 'application/pdf') + .expect('content-disposition', 'inline; filename="report.pdf"') + .then(({ body }) => expect(Buffer.from(body).toString('base64')).toEqual('test')); }); - test(`logs error message about invalid content type`, async () => { - const { request } = await getCompletedResponse({ outputContentType: 'application/html' }); - const errorLogs = getErrorsFromRequest(request); - expect(errorLogs).toMatchInlineSnapshot(` - Array [ - [Error: Unsupported content-type of application/html specified by job output], - [Error: Unsupported content-type of application/html specified by job output], - ] - `); + it('refuses to return unknown content-types', async () => { + const hits = getCompleteHits({ + jobType: 'unencodedJobType', + outputContent: 'alert("all your base mine now");', + outputContentType: 'application/html', + }); + // @ts-ignore + core.pluginSetupDeps.elasticsearch.legacy.client = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), + }; + registerJobInfoRoutes(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dank') + .expect(400) + .then(({ body }) => { + expect(body).toEqual({ + error: 'Bad Request', + message: 'Unsupported content-type of application/html specified by job output', + statusCode: 400, + }); + }); }); }); }); diff --git a/x-pack/legacy/plugins/reporting/server/routes/jobs.ts b/x-pack/legacy/plugins/reporting/server/routes/jobs.ts index 59090961998af..8c35f79ec0fb4 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/jobs.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/jobs.ts @@ -5,24 +5,15 @@ */ import Boom from 'boom'; -import { ResponseObject } from 'hapi'; -import { Legacy } from 'kibana'; +import { schema } from '@kbn/config-schema'; import { ReportingCore } from '../'; import { API_BASE_URL } from '../../common/constants'; -import { LevelLogger as Logger } from '../lib'; import { jobsQueryFactory } from '../lib/jobs_query'; -import { JobDocOutput, JobSource, ReportingSetupDeps, ServerFacade } from '../types'; import { deleteJobResponseHandlerFactory, downloadJobResponseHandlerFactory, } from './lib/job_response_handler'; -import { makeRequestFacade } from './lib/make_request_facade'; -import { - getRouteConfigFactoryDeletePre, - getRouteConfigFactoryDownloadPre, - getRouteConfigFactoryManagementPre, -} from './lib/route_config_factories'; -import { ReportingResponseToolkit } from './types'; +import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; interface ListQuery { page: string; @@ -31,193 +22,192 @@ interface ListQuery { } const MAIN_ENTRY = `${API_BASE_URL}/jobs`; -function isResponse(response: Boom | ResponseObject): response is ResponseObject { - return !(response as Boom).isBoom; -} - -export function registerJobInfoRoutes( - reporting: ReportingCore, - server: ServerFacade, - plugins: ReportingSetupDeps, - logger: Logger -) { +export async function registerJobInfoRoutes(reporting: ReportingCore) { const config = reporting.getConfig(); - const { elasticsearch } = plugins; + const setupDeps = reporting.getPluginSetupDeps(); + const userHandler = authorizedUserPreRoutingFactory(reporting); + const { elasticsearch, router } = setupDeps; const jobsQuery = jobsQueryFactory(config, elasticsearch); - const getRouteConfig = getRouteConfigFactoryManagementPre(config, plugins, logger); // list jobs in the queue, paginated - server.route({ - path: `${MAIN_ENTRY}/list`, - method: 'GET', - options: getRouteConfig(), - handler: (legacyRequest: Legacy.Request) => { - const request = makeRequestFacade(legacyRequest); - const { page: queryPage, size: querySize, ids: queryIds } = request.query as ListQuery; + router.get( + { + path: `${MAIN_ENTRY}/list`, + validate: false, + }, + userHandler(async (user, context, req, res) => { + const { + management: { jobTypes = [] }, + } = await reporting.getLicenseInfo(); + const { + page: queryPage = '0', + size: querySize = '10', + ids: queryIds = null, + } = req.query as ListQuery; const page = parseInt(queryPage, 10) || 0; const size = Math.min(100, parseInt(querySize, 10) || 10); const jobIds = queryIds ? queryIds.split(',') : null; + const results = await jobsQuery.list(jobTypes, user, page, size, jobIds); - const results = jobsQuery.list( - request.pre.management.jobTypes, - request.pre.user, - page, - size, - jobIds - ); - return results; - }, - }); + return res.ok({ + body: results, + headers: { + 'content-type': 'application/json', + }, + }); + }) + ); // return the count of all jobs in the queue - server.route({ - path: `${MAIN_ENTRY}/count`, - method: 'GET', - options: getRouteConfig(), - handler: (legacyRequest: Legacy.Request) => { - const request = makeRequestFacade(legacyRequest); - const results = jobsQuery.count(request.pre.management.jobTypes, request.pre.user); - return results; + router.get( + { + path: `${MAIN_ENTRY}/count`, + validate: false, }, - }); + userHandler(async (user, context, req, res) => { + const { + management: { jobTypes = [] }, + } = await reporting.getLicenseInfo(); + + const count = await jobsQuery.count(jobTypes, user); + + return res.ok({ + body: count.toString(), + headers: { + 'content-type': 'text/plain', + }, + }); + }) + ); // return the raw output from a job - server.route({ - path: `${MAIN_ENTRY}/output/{docId}`, - method: 'GET', - options: getRouteConfig(), - handler: (legacyRequest: Legacy.Request) => { - const request = makeRequestFacade(legacyRequest); - const { docId } = request.params; - - return jobsQuery.get(request.pre.user, docId, { includeContent: true }).then( - (result): JobDocOutput => { - if (!result) { - throw Boom.notFound(); - } - const { - _source: { jobtype: jobType, output: jobOutput }, - } = result; - - if (!request.pre.management.jobTypes.includes(jobType)) { - throw Boom.unauthorized(`Sorry, you are not authorized to download ${jobType} reports`); - } - - return jobOutput; - } - ); + router.get( + { + path: `${MAIN_ENTRY}/output/{docId}`, + validate: { + params: schema.object({ + docId: schema.string({ minLength: 2 }), + }), + }, }, - }); + userHandler(async (user, context, req, res) => { + const { docId } = req.params as { docId: string }; + const { + management: { jobTypes = [] }, + } = await reporting.getLicenseInfo(); + + const result = await jobsQuery.get(user, docId, { includeContent: true }); + + if (!result) { + throw Boom.notFound(); + } + + const { + _source: { jobtype: jobType, output: jobOutput }, + } = result; + + if (!jobTypes.includes(jobType)) { + throw Boom.unauthorized(`Sorry, you are not authorized to download ${jobType} reports`); + } + + return res.ok({ + body: jobOutput, + headers: { + 'content-type': 'application/json', + }, + }); + }) + ); // return some info about the job - server.route({ - path: `${MAIN_ENTRY}/info/{docId}`, - method: 'GET', - options: getRouteConfig(), - handler: (legacyRequest: Legacy.Request) => { - const request = makeRequestFacade(legacyRequest); - const { docId } = request.params; - - return jobsQuery.get(request.pre.user, docId).then((result): JobSource['_source'] => { - if (!result) { - throw Boom.notFound(); - } - - const { _source: job } = result; - const { jobtype: jobType, payload: jobPayload } = job; - if (!request.pre.management.jobTypes.includes(jobType)) { - throw Boom.unauthorized(`Sorry, you are not authorized to view ${jobType} info`); - } - - return { + router.get( + { + path: `${MAIN_ENTRY}/info/{docId}`, + validate: { + params: schema.object({ + docId: schema.string({ minLength: 2 }), + }), + }, + }, + userHandler(async (user, context, req, res) => { + const { docId } = req.params as { docId: string }; + const { + management: { jobTypes = [] }, + } = await reporting.getLicenseInfo(); + + const result = await jobsQuery.get(user, docId); + + if (!result) { + throw Boom.notFound(); + } + + const { _source: job } = result; + const { jobtype: jobType, payload: jobPayload } = job; + + if (!jobTypes.includes(jobType)) { + throw Boom.unauthorized(`Sorry, you are not authorized to view ${jobType} info`); + } + + return res.ok({ + body: { ...job, payload: { ...jobPayload, headers: undefined, }, - }; + }, + headers: { + 'content-type': 'application/json', + }, }); - }, - }); + }) + ); // trigger a download of the output from a job const exportTypesRegistry = reporting.getExportTypesRegistry(); - const getRouteConfigDownload = getRouteConfigFactoryDownloadPre(config, plugins, logger); - const downloadResponseHandler = downloadJobResponseHandlerFactory(config, elasticsearch, exportTypesRegistry); // prettier-ignore - server.route({ - path: `${MAIN_ENTRY}/download/{docId}`, - method: 'GET', - options: getRouteConfigDownload(), - handler: async (legacyRequest: Legacy.Request, h: ReportingResponseToolkit) => { - const request = makeRequestFacade(legacyRequest); - const { docId } = request.params; - - let response = await downloadResponseHandler( - request.pre.management.jobTypes, - request.pre.user, - h, - { docId } - ); - - if (isResponse(response)) { - const { statusCode } = response; - - if (statusCode !== 200) { - if (statusCode === 500) { - logger.error(`Report ${docId} has failed: ${JSON.stringify(response.source)}`); - } else { - logger.debug( - `Report ${docId} has non-OK status: [${statusCode}] Reason: [${JSON.stringify( - response.source - )}]` - ); - } - } - - response = response.header('accept-ranges', 'none'); - } - - return response; + const downloadResponseHandler = downloadJobResponseHandlerFactory( + config, + elasticsearch, + exportTypesRegistry + ); + + router.get( + { + path: `${MAIN_ENTRY}/download/{docId}`, + validate: { + params: schema.object({ + docId: schema.string({ minLength: 3 }), + }), + }, }, - }); + userHandler(async (user, context, req, res) => { + const { docId } = req.params as { docId: string }; + const { + management: { jobTypes = [] }, + } = await reporting.getLicenseInfo(); + + return downloadResponseHandler(res, jobTypes, user, { docId }); + }) + ); // allow a report to be deleted - const getRouteConfigDelete = getRouteConfigFactoryDeletePre(config, plugins, logger); const deleteResponseHandler = deleteJobResponseHandlerFactory(config, elasticsearch); - server.route({ - path: `${MAIN_ENTRY}/delete/{docId}`, - method: 'DELETE', - options: getRouteConfigDelete(), - handler: async (legacyRequest: Legacy.Request, h: ReportingResponseToolkit) => { - const request = makeRequestFacade(legacyRequest); - const { docId } = request.params; - - let response = await deleteResponseHandler( - request.pre.management.jobTypes, - request.pre.user, - h, - { docId } - ); - - if (isResponse(response)) { - const { statusCode } = response; - - if (statusCode !== 200) { - if (statusCode === 500) { - logger.error(`Report ${docId} has failed: ${JSON.stringify(response.source)}`); - } else { - logger.debug( - `Report ${docId} has non-OK status: [${statusCode}] Reason: [${JSON.stringify( - response.source - )}]` - ); - } - } - - response = response.header('accept-ranges', 'none'); - } - - return response; + router.delete( + { + path: `${MAIN_ENTRY}/delete/{docId}`, + validate: { + params: schema.object({ + docId: schema.string({ minLength: 3 }), + }), + }, }, - }); + userHandler(async (user, context, req, res) => { + const { docId } = req.params as { docId: string }; + const { + management: { jobTypes = [] }, + } = await reporting.getLicenseInfo(); + + return deleteResponseHandler(res, jobTypes, user, { docId }); + }) + ); } diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.js b/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.js deleted file mode 100644 index 2c80965432cd2..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.js +++ /dev/null @@ -1,175 +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 { authorizedUserPreRoutingFactory } from './authorized_user_pre_routing'; - -describe('authorized_user_pre_routing', function () { - const createMockConfig = (mockConfig = {}) => { - return { - get: (...keys) => mockConfig[keys.join('.')], - kbnConfig: { get: (...keys) => mockConfig[keys.join('.')] }, - }; - }; - const createMockPlugins = (function () { - const getUserStub = jest.fn(); - - return function ({ - securityEnabled = true, - xpackInfoUndefined = false, - xpackInfoAvailable = true, - getCurrentUser = undefined, - user = undefined, - }) { - getUserStub.mockReset(); - getUserStub.mockResolvedValue(user); - return { - security: securityEnabled - ? { - authc: { getCurrentUser }, - } - : null, - __LEGACY: { - plugins: { - xpack_main: { - info: !xpackInfoUndefined && { - isAvailable: () => xpackInfoAvailable, - feature(featureName) { - if (featureName === 'security') { - return { - isEnabled: () => securityEnabled, - isAvailable: () => xpackInfoAvailable, - }; - } - }, - }, - }, - }, - }, - }; - }; - })(); - - const mockRequestRaw = { - body: {}, - events: {}, - headers: {}, - isSystemRequest: false, - params: {}, - query: {}, - route: { settings: { payload: 'abc' }, options: { authRequired: true, body: {}, tags: [] } }, - withoutSecretHeaders: true, - }; - const getMockRequest = () => ({ - ...mockRequestRaw, - raw: { req: mockRequestRaw }, - }); - - const getMockLogger = () => ({ - warn: jest.fn(), - error: (msg) => { - throw new Error(msg); - }, - }); - - it('should return with boom notFound when xpackInfo is undefined', async function () { - const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - createMockConfig(), - createMockPlugins({ xpackInfoUndefined: true }), - getMockLogger() - ); - const response = await authorizedUserPreRouting(getMockRequest()); - expect(response.isBoom).toBe(true); - expect(response.output.statusCode).toBe(404); - }); - - it(`should return with boom notFound when xpackInfo isn't available`, async function () { - const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - createMockConfig(), - createMockPlugins({ xpackInfoAvailable: false }), - getMockLogger() - ); - const response = await authorizedUserPreRouting(getMockRequest()); - expect(response.isBoom).toBe(true); - expect(response.output.statusCode).toBe(404); - }); - - it('should return with null user when security is disabled in Elasticsearch', async function () { - const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - createMockConfig(), - createMockPlugins({ securityEnabled: false }), - getMockLogger() - ); - const response = await authorizedUserPreRouting(getMockRequest()); - expect(response).toBe(null); - }); - - it('should return with boom unauthenticated when security is enabled but no authenticated user', async function () { - const mockPlugins = createMockPlugins({ - user: null, - config: { 'xpack.reporting.roles.allow': ['.reporting_user'] }, - }); - mockPlugins.security = { authc: { getCurrentUser: () => null } }; - - const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - createMockConfig(), - mockPlugins, - getMockLogger() - ); - const response = await authorizedUserPreRouting(getMockRequest()); - expect(response.isBoom).toBe(true); - expect(response.output.statusCode).toBe(401); - }); - - it(`should return with boom forbidden when security is enabled but user doesn't have allowed role`, async function () { - const mockConfig = createMockConfig({ 'roles.allow': ['.reporting_user'] }); - const mockPlugins = createMockPlugins({ - user: { roles: [] }, - getCurrentUser: () => ({ roles: ['something_else'] }), - }); - - const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - mockConfig, - mockPlugins, - getMockLogger() - ); - const response = await authorizedUserPreRouting(getMockRequest()); - expect(response.isBoom).toBe(true); - expect(response.output.statusCode).toBe(403); - }); - - it('should return with user when security is enabled and user has explicitly allowed role', async function () { - const user = { roles: ['.reporting_user', 'something_else'] }; - const mockConfig = createMockConfig({ 'roles.allow': ['.reporting_user'] }); - const mockPlugins = createMockPlugins({ - user, - getCurrentUser: () => ({ roles: ['.reporting_user', 'something_else'] }), - }); - - const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - mockConfig, - mockPlugins, - getMockLogger() - ); - const response = await authorizedUserPreRouting(getMockRequest()); - expect(response).toEqual(user); - }); - - it('should return with user when security is enabled and user has superuser role', async function () { - const user = { roles: ['superuser', 'something_else'] }; - const mockConfig = createMockConfig({ 'roles.allow': [] }); - const mockPlugins = createMockPlugins({ - getCurrentUser: () => ({ roles: ['superuser', 'something_else'] }), - }); - - const authorizedUserPreRouting = authorizedUserPreRoutingFactory( - mockConfig, - mockPlugins, - getMockLogger() - ); - const response = await authorizedUserPreRouting(getMockRequest()); - expect(response).toEqual(user); - }); -}); diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts new file mode 100644 index 0000000000000..4cb7af3d0d409 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts @@ -0,0 +1,134 @@ +/* + * 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 { KibanaRequest, RequestHandlerContext, KibanaResponseFactory } from 'kibana/server'; +import sinon from 'sinon'; +import { coreMock, httpServerMock } from 'src/core/server/mocks'; +import { ReportingConfig, ReportingCore } from '../../'; +import { createMockReportingCore } from '../../../test_helpers'; +import { authorizedUserPreRoutingFactory } from './authorized_user_pre_routing'; +import { ReportingInternalSetup } from '../../core'; + +let mockConfig: ReportingConfig; +let mockCore: ReportingCore; + +const getMockConfig = (mockConfigGet: sinon.SinonStub) => ({ + get: mockConfigGet, + kbnConfig: { get: mockConfigGet }, +}); + +const getMockContext = () => + (({ + core: coreMock.createRequestHandlerContext(), + } as unknown) as RequestHandlerContext); + +const getMockRequest = () => + ({ + url: { port: '5601', query: '', path: '/foo' }, + route: { path: '/foo', options: {} }, + } as KibanaRequest); + +const getMockResponseFactory = () => + (({ + ...httpServerMock.createResponseFactory(), + forbidden: (obj: unknown) => obj, + unauthorized: (obj: unknown) => obj, + } as unknown) as KibanaResponseFactory); + +beforeEach(async () => { + const mockConfigGet = sinon.stub().withArgs('roles', 'allow').returns(['reporting_user']); + mockConfig = getMockConfig(mockConfigGet); + mockCore = await createMockReportingCore(mockConfig); +}); + +describe('authorized_user_pre_routing', function () { + it('should return from handler with null user when security is disabled', async function () { + mockCore.getPluginSetupDeps = () => + (({ + // @ts-ignore + ...mockCore.pluginSetupDeps, + security: undefined, // disable security + } as unknown) as ReportingInternalSetup); + const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore); + const mockResponseFactory = httpServerMock.createResponseFactory() as KibanaResponseFactory; + + let handlerCalled = false; + authorizedUserPreRouting((user: unknown) => { + expect(user).toBe(null); // verify the user is a null value + handlerCalled = true; + return Promise.resolve({ status: 200, options: {} }); + })(getMockContext(), getMockRequest(), mockResponseFactory); + + expect(handlerCalled).toBe(true); + }); + + it('should return with 401 when security is enabled but no authenticated user', async function () { + mockCore.getPluginSetupDeps = () => + (({ + // @ts-ignore + ...mockCore.pluginSetupDeps, + security: { + authc: { getCurrentUser: () => null }, + }, + } as unknown) as ReportingInternalSetup); + const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore); + const mockHandler = () => { + throw new Error('Handler callback should not be called'); + }; + const requestHandler = authorizedUserPreRouting(mockHandler); + const mockResponseFactory = getMockResponseFactory(); + + expect(requestHandler(getMockContext(), getMockRequest(), mockResponseFactory)).toMatchObject({ + body: `Sorry, you aren't authenticated`, + }); + }); + + it(`should return with 403 when security is enabled but user doesn't have allowed role`, async function () { + mockCore.getPluginSetupDeps = () => + (({ + // @ts-ignore + ...mockCore.pluginSetupDeps, + security: { + authc: { getCurrentUser: () => ({ username: 'friendlyuser', roles: ['cowboy'] }) }, + }, + } as unknown) as ReportingInternalSetup); + const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore); + const mockResponseFactory = getMockResponseFactory(); + + const mockHandler = () => { + throw new Error('Handler callback should not be called'); + }; + expect( + authorizedUserPreRouting(mockHandler)(getMockContext(), getMockRequest(), mockResponseFactory) + ).toMatchObject({ body: `Sorry, you don't have access to Reporting` }); + }); + + it('should return from handler when security is enabled and user has explicitly allowed role', async function () { + mockCore.getPluginSetupDeps = () => + (({ + // @ts-ignore + ...mockCore.pluginSetupDeps, + security: { + authc: { + getCurrentUser: () => ({ username: 'friendlyuser', roles: ['reporting_user'] }), + }, + }, + } as unknown) as ReportingInternalSetup); + const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore); + const mockResponseFactory = getMockResponseFactory(); + + let handlerCalled = false; + authorizedUserPreRouting((user: unknown) => { + expect(user).toMatchObject({ roles: ['reporting_user'], username: 'friendlyuser' }); + handlerCalled = true; + return Promise.resolve({ status: 200, options: {} }); + })(getMockContext(), getMockRequest(), mockResponseFactory); + + expect(handlerCalled).toBe(true); + }); + + it('should return from handler when security is enabled and user has superuser role', async function () {}); +}); diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts index 0c4e75a53831e..87582ca3ca239 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts @@ -4,52 +4,48 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; -import { Legacy } from 'kibana'; +import { RequestHandler, RouteMethod } from 'src/core/server'; import { AuthenticatedUser } from '../../../../../../plugins/security/server'; -import { ReportingConfig } from '../../../server'; -import { LevelLogger as Logger } from '../../../server/lib'; -import { ReportingSetupDeps } from '../../../server/types'; import { getUserFactory } from '../../lib/get_user'; +import { ReportingCore } from '../../core'; +type ReportingUser = AuthenticatedUser | null; const superuserRole = 'superuser'; -export type PreRoutingFunction = ( - request: Legacy.Request -) => Promise | AuthenticatedUser | null>; +export type RequestHandlerUser = RequestHandler extends (...a: infer U) => infer R + ? (user: ReportingUser, ...a: U) => R + : never; export const authorizedUserPreRoutingFactory = function authorizedUserPreRoutingFn( - config: ReportingConfig, - plugins: ReportingSetupDeps, - logger: Logger + reporting: ReportingCore ) { - const getUser = getUserFactory(plugins.security, logger); - const { info: xpackInfo } = plugins.__LEGACY.plugins.xpack_main; - - return async function authorizedUserPreRouting(request: Legacy.Request) { - if (!xpackInfo || !xpackInfo.isAvailable()) { - logger.warn('Unable to authorize user before xpack info is available.', [ - 'authorizedUserPreRouting', - ]); - return Boom.notFound(); - } - - const security = xpackInfo.feature('security'); - if (!security.isEnabled() || !security.isAvailable()) { - return null; - } - - const user = await getUser(request); - - if (!user) { - return Boom.unauthorized(`Sorry, you aren't authenticated`); - } - - const authorizedRoles = [superuserRole, ...(config.get('roles', 'allow') as string[])]; - if (!user.roles.find((role) => authorizedRoles.includes(role))) { - return Boom.forbidden(`Sorry, you don't have access to Reporting`); - } - - return user; + const config = reporting.getConfig(); + const setupDeps = reporting.getPluginSetupDeps(); + const getUser = getUserFactory(setupDeps.security); + return (handler: RequestHandlerUser): RequestHandler => { + return (context, req, res) => { + let user: ReportingUser = null; + if (setupDeps.security) { + // find the authenticated user, or null if security is not enabled + user = getUser(req); + if (!user) { + // security is enabled but the user is null + return res.unauthorized({ body: `Sorry, you aren't authenticated` }); + } + } + + if (user) { + // check allowance with the configured set of roleas + "superuser" + const allowedRoles = config.get('roles', 'allow') || []; + const authorizedRoles = [superuserRole, ...allowedRoles]; + + if (!user.roles.find((role) => authorizedRoles.includes(role))) { + // user's roles do not allow + return res.forbidden({ body: `Sorry, you don't have access to Reporting` }); + } + } + + return handler(user, context, req, res); + }; }; }; diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts index 6a228c1915615..e16f5278c8cc7 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts @@ -12,15 +12,10 @@ import { statuses } from '../../lib/esqueue/constants/statuses'; import { ExportTypesRegistry } from '../../lib/export_types_registry'; import { ExportTypeDefinition, JobDocOutput, JobSource } from '../../types'; -interface ICustomHeaders { - [x: string]: any; -} - type ExportTypeType = ExportTypeDefinition; interface ErrorFromPayload { message: string; - reason: string | null; } // A camelCase version of JobDocOutput @@ -37,7 +32,7 @@ const getTitle = (exportType: ExportTypeType, title?: string): string => `${title || DEFAULT_TITLE}.${exportType.jobContentExtension}`; const getReportingHeaders = (output: JobDocOutput, exportType: ExportTypeType) => { - const metaDataHeaders: ICustomHeaders = {}; + const metaDataHeaders: Record = {}; if (exportType.jobType === CSV_JOB_TYPE) { const csvContainsFormulas = _.get(output, 'csv_contains_formulas', false); @@ -76,12 +71,13 @@ export function getDocumentPayloadFactory(exportTypesRegistry: ExportTypesRegist }; } + // @TODO: These should be semantic HTTP codes as 500/503's indicate + // error then these are really operating properly. function getFailure(output: JobDocOutput): Payload { return { statusCode: 500, content: { - message: 'Reporting generation failed', - reason: output.content, + message: `Reporting generation failed: ${output.content}`, }, contentType: 'application/json', headers: {}, @@ -92,7 +88,7 @@ export function getDocumentPayloadFactory(exportTypesRegistry: ExportTypesRegist return { statusCode: 503, content: status, - contentType: 'application/json', + contentType: 'text/plain', headers: { 'retry-after': 30 }, }; } diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts index 174ec15c81d8a..990af2d0aca80 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; -import { ResponseToolkit } from 'hapi'; -import { ElasticsearchServiceSetup } from 'kibana/server'; +import { ElasticsearchServiceSetup, kibanaResponseFactory } from 'kibana/server'; +import { AuthenticatedUser } from '../../../../../../plugins/security/server'; import { ReportingConfig } from '../../'; import { WHITELISTED_JOB_CONTENT_TYPES } from '../../../common/constants'; import { ExportTypesRegistry } from '../../lib/export_types_registry'; @@ -29,40 +28,43 @@ export function downloadJobResponseHandlerFactory( const jobsQuery = jobsQueryFactory(config, elasticsearch); const getDocumentPayload = getDocumentPayloadFactory(exportTypesRegistry); - return function jobResponseHandler( + return async function jobResponseHandler( + res: typeof kibanaResponseFactory, validJobTypes: string[], - user: any, - h: ResponseToolkit, + user: AuthenticatedUser | null, params: JobResponseHandlerParams, opts: JobResponseHandlerOpts = {} ) { const { docId } = params; - // TODO: async/await - return jobsQuery.get(user, docId, { includeContent: !opts.excludeContent }).then((doc) => { - if (!doc) return Boom.notFound(); - const { jobtype: jobType } = doc._source; - if (!validJobTypes.includes(jobType)) { - return Boom.unauthorized(`Sorry, you are not authorized to download ${jobType} reports`); - } + const doc = await jobsQuery.get(user, docId, { includeContent: !opts.excludeContent }); + if (!doc) { + return res.notFound(); + } - const output = getDocumentPayload(doc); + const { jobtype: jobType } = doc._source; - if (!WHITELISTED_JOB_CONTENT_TYPES.includes(output.contentType)) { - return Boom.badImplementation( - `Unsupported content-type of ${output.contentType} specified by job output` - ); - } + if (!validJobTypes.includes(jobType)) { + return res.unauthorized({ + body: `Sorry, you are not authorized to download ${jobType} reports`, + }); + } - const response = h.response(output.content).type(output.contentType).code(output.statusCode); + const response = getDocumentPayload(doc); - if (output.headers) { - Object.keys(output.headers).forEach((key) => { - response.header(key, output.headers[key]); - }); - } + if (!WHITELISTED_JOB_CONTENT_TYPES.includes(response.contentType)) { + return res.badRequest({ + body: `Unsupported content-type of ${response.contentType} specified by job output`, + }); + } - return response; // Hapi + return res.custom({ + body: typeof response.content === 'string' ? Buffer.from(response.content) : response.content, + statusCode: response.statusCode, + headers: { + ...response.headers, + 'content-type': response.contentType, + }, }); }; } @@ -74,26 +76,37 @@ export function deleteJobResponseHandlerFactory( const jobsQuery = jobsQueryFactory(config, elasticsearch); return async function deleteJobResponseHander( + res: typeof kibanaResponseFactory, validJobTypes: string[], - user: any, - h: ResponseToolkit, + user: AuthenticatedUser | null, params: JobResponseHandlerParams ) { const { docId } = params; const doc = await jobsQuery.get(user, docId, { includeContent: false }); - if (!doc) return Boom.notFound(); + + if (!doc) { + return res.notFound(); + } const { jobtype: jobType } = doc._source; + if (!validJobTypes.includes(jobType)) { - return Boom.unauthorized(`Sorry, you are not authorized to delete ${jobType} reports`); + return res.unauthorized({ + body: `Sorry, you are not authorized to delete ${jobType} reports`, + }); } try { const docIndex = doc._index; await jobsQuery.delete(docIndex, docId); - return h.response({ deleted: true }); + return res.ok({ + body: { deleted: true }, + }); } catch (error) { - return Boom.boomify(error, { statusCode: error.statusCode }); + return res.customError({ + statusCode: error.statusCode, + body: error.message, + }); } }; } diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.test.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.test.ts deleted file mode 100644 index 8cdb7b4c018d7..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.test.ts +++ /dev/null @@ -1,62 +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 { Legacy } from 'kibana'; -import { makeRequestFacade } from './make_request_facade'; - -describe('makeRequestFacade', () => { - test('creates a default object', () => { - const legacyRequest = ({ - getBasePath: () => 'basebase', - params: { - param1: 123, - }, - payload: { - payload1: 123, - }, - headers: { - user: 123, - }, - } as unknown) as Legacy.Request; - - expect(makeRequestFacade(legacyRequest)).toMatchInlineSnapshot(` - Object { - "getBasePath": [Function], - "getRawRequest": [Function], - "getSavedObjectsClient": undefined, - "headers": Object { - "user": 123, - }, - "params": Object { - "param1": 123, - }, - "payload": Object { - "payload1": 123, - }, - "pre": undefined, - "query": undefined, - "route": undefined, - } - `); - }); - - test('getRawRequest', () => { - const legacyRequest = ({ - getBasePath: () => 'basebase', - params: { - param1: 123, - }, - payload: { - payload1: 123, - }, - headers: { - user: 123, - }, - } as unknown) as Legacy.Request; - - expect(makeRequestFacade(legacyRequest).getRawRequest()).toBe(legacyRequest); - }); -}); diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.ts deleted file mode 100644 index 5dd62711f2565..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { RequestQuery } from 'hapi'; -import { Legacy } from 'kibana'; -import { - RequestFacade, - ReportingRequestPayload, - ReportingRequestPre, - ReportingRequestQuery, -} from '../../../server/types'; - -export function makeRequestFacade(request: Legacy.Request): RequestFacade { - // This condition is for unit tests - const getSavedObjectsClient = request.getSavedObjectsClient - ? request.getSavedObjectsClient.bind(request) - : request.getSavedObjectsClient; - return { - getSavedObjectsClient, - headers: request.headers, - params: request.params, - payload: (request.payload as object) as ReportingRequestPayload, - query: ((request.query as RequestQuery) as object) as ReportingRequestQuery, - pre: (request.pre as Record) as ReportingRequestPre, - getBasePath: request.getBasePath, - route: request.route, - getRawRequest: () => request, - }; -} diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.ts deleted file mode 100644 index f9c7571e25bac..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/reporting_feature_pre_routing.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; -import { Legacy } from 'kibana'; -import { ReportingConfig } from '../../'; -import { LevelLogger as Logger } from '../../lib'; -import { ReportingSetupDeps } from '../../types'; - -export type GetReportingFeatureIdFn = (request: Legacy.Request) => string; - -export const reportingFeaturePreRoutingFactory = function reportingFeaturePreRoutingFn( - config: ReportingConfig, - plugins: ReportingSetupDeps, - logger: Logger -) { - const xpackMainPlugin = plugins.__LEGACY.plugins.xpack_main; - const pluginId = 'reporting'; - - // License checking and enable/disable logic - return function reportingFeaturePreRouting(getReportingFeatureId: GetReportingFeatureIdFn) { - return function licensePreRouting(request: Legacy.Request) { - const licenseCheckResults = xpackMainPlugin.info.feature(pluginId).getLicenseCheckResults(); - const reportingFeatureId = getReportingFeatureId(request) as string; - const reportingFeature = licenseCheckResults[reportingFeatureId]; - if (!reportingFeature.showLinks || !reportingFeature.enableLinks) { - throw Boom.forbidden(reportingFeature.message); - } else { - return reportingFeature; - } - }; - }; -}; diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/route_config_factories.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/route_config_factories.ts deleted file mode 100644 index 0ee9db4678684..0000000000000 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/route_config_factories.ts +++ /dev/null @@ -1,130 +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 Joi from 'joi'; -import { ReportingConfig } from '../../'; -import { LevelLogger as Logger } from '../../lib'; -import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; -import { ReportingSetupDeps } from '../../types'; -import { authorizedUserPreRoutingFactory } from './authorized_user_pre_routing'; -import { - GetReportingFeatureIdFn, - reportingFeaturePreRoutingFactory, -} from './reporting_feature_pre_routing'; - -const API_TAG = 'api'; - -export interface RouteConfigFactory { - tags?: string[]; - pre: any[]; - response?: { - ranges: boolean; - }; -} - -export type GetRouteConfigFactoryFn = ( - getFeatureId?: GetReportingFeatureIdFn -) => RouteConfigFactory; - -export function getRouteConfigFactoryReportingPre( - config: ReportingConfig, - plugins: ReportingSetupDeps, - logger: Logger -): GetRouteConfigFactoryFn { - const authorizedUserPreRouting = authorizedUserPreRoutingFactory(config, plugins, logger); - const reportingFeaturePreRouting = reportingFeaturePreRoutingFactory(config, plugins, logger); - - return (getFeatureId?: GetReportingFeatureIdFn): RouteConfigFactory => { - const preRouting: any[] = [{ method: authorizedUserPreRouting, assign: 'user' }]; - if (getFeatureId) { - preRouting.push(reportingFeaturePreRouting(getFeatureId)); - } - - return { - tags: [API_TAG], - pre: preRouting, - }; - }; -} - -export function getRouteOptionsCsv( - config: ReportingConfig, - plugins: ReportingSetupDeps, - logger: Logger -) { - const getRouteConfig = getRouteConfigFactoryReportingPre(config, plugins, logger); - return { - ...getRouteConfig(() => CSV_FROM_SAVEDOBJECT_JOB_TYPE), - validate: { - params: Joi.object({ - savedObjectType: Joi.string().required(), - savedObjectId: Joi.string().required(), - }).required(), - payload: Joi.object({ - state: Joi.object().default({}), - timerange: Joi.object({ - timezone: Joi.string().default('UTC'), - min: Joi.date().required(), - max: Joi.date().required(), - }).optional(), - }), - }, - }; -} - -export function getRouteConfigFactoryManagementPre( - config: ReportingConfig, - plugins: ReportingSetupDeps, - logger: Logger -): GetRouteConfigFactoryFn { - const authorizedUserPreRouting = authorizedUserPreRoutingFactory(config, plugins, logger); - const reportingFeaturePreRouting = reportingFeaturePreRoutingFactory(config, plugins, logger); - const managementPreRouting = reportingFeaturePreRouting(() => 'management'); - - return (): RouteConfigFactory => { - return { - pre: [ - { method: authorizedUserPreRouting, assign: 'user' }, - { method: managementPreRouting, assign: 'management' }, - ], - }; - }; -} - -// NOTE: We're disabling range request for downloading the PDF. There's a bug in Firefox's PDF.js viewer -// (https://github.com/mozilla/pdf.js/issues/8958) where they're using a range request to retrieve the -// TOC at the end of the PDF, but it's sending multiple cookies and causing our auth to fail with a 401. -// Additionally, the range-request doesn't alleviate any performance issues on the server as the entire -// download is loaded into memory. -export function getRouteConfigFactoryDownloadPre( - config: ReportingConfig, - plugins: ReportingSetupDeps, - logger: Logger -): GetRouteConfigFactoryFn { - const getManagementRouteConfig = getRouteConfigFactoryManagementPre(config, plugins, logger); - return (): RouteConfigFactory => ({ - ...getManagementRouteConfig(), - tags: [API_TAG, 'download'], - response: { - ranges: false, - }, - }); -} - -export function getRouteConfigFactoryDeletePre( - config: ReportingConfig, - plugins: ReportingSetupDeps, - logger: Logger -): GetRouteConfigFactoryFn { - const getManagementRouteConfig = getRouteConfigFactoryManagementPre(config, plugins, logger); - return (): RouteConfigFactory => ({ - ...getManagementRouteConfig(), - tags: [API_TAG, 'delete'], - response: { - ranges: false, - }, - }); -} diff --git a/x-pack/legacy/plugins/reporting/server/routes/types.d.ts b/x-pack/legacy/plugins/reporting/server/routes/types.d.ts index 2ebe1ada418dc..afa3fd3358fc1 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/types.d.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/types.d.ts @@ -4,17 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Legacy } from 'kibana'; +import { KibanaResponseFactory, KibanaRequest, RequestHandlerContext } from 'src/core/server'; +import { AuthenticatedUser } from '../../../../../plugins/security/common/model/authenticated_user'; import { JobDocPayload } from '../types'; export type HandlerFunction = ( + user: AuthenticatedUser | null, exportType: string, jobParams: object, - request: Legacy.Request, - h: ReportingResponseToolkit + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory ) => any; -export type HandlerErrorFunction = (exportType: string, err: Error) => any; +export type HandlerErrorFunction = (res: KibanaResponseFactory, err: Error) => any; export interface QueuedJobPayload { error?: boolean; @@ -24,5 +27,3 @@ export interface QueuedJobPayload { }; }; } - -export type ReportingResponseToolkit = Legacy.ResponseToolkit; diff --git a/x-pack/legacy/plugins/reporting/server/types.ts b/x-pack/legacy/plugins/reporting/server/types.ts index bfab568fe9fb3..2ccc209c3ce50 100644 --- a/x-pack/legacy/plugins/reporting/server/types.ts +++ b/x-pack/legacy/plugins/reporting/server/types.ts @@ -5,6 +5,7 @@ */ import { Legacy } from 'kibana'; +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { ElasticsearchServiceSetup } from 'kibana/server'; import * as Rx from 'rxjs'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -53,8 +54,8 @@ export type ReportingRequestPayload = GenerateExportTypePayload | JobParamPostPa export interface TimeRangeParams { timezone: string; - min: Date | string | number; - max: Date | string | number; + min: Date | string | number | null; + max: Date | string | number | null; } export interface JobParamPostPayload { @@ -189,22 +190,10 @@ export interface LegacySetup { * Internal Types */ -export interface RequestFacade { - getBasePath: Legacy.Request['getBasePath']; - getSavedObjectsClient: Legacy.Request['getSavedObjectsClient']; - headers: Legacy.Request['headers']; - params: Legacy.Request['params']; - payload: JobParamPostPayload | GenerateExportTypePayload; - query: ReportingRequestQuery; - route: Legacy.Request['route']; - pre: ReportingRequestPre; - getRawRequest: () => Legacy.Request; -} - export type ESQueueCreateJobFn = ( jobParams: JobParamsType, - headers: Record, - request: RequestFacade + context: RequestHandlerContext, + request: KibanaRequest ) => Promise; export type ESQueueWorkerExecuteFn = ( diff --git a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_reportingplugin.ts b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_reportingplugin.ts index 286e072f1f8f6..f6dbccdfe3980 100644 --- a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_reportingplugin.ts @@ -12,17 +12,21 @@ jest.mock('../server/lib/create_queue'); jest.mock('../server/lib/enqueue_job'); jest.mock('../server/lib/validate'); +import { of } from 'rxjs'; import { EventEmitter } from 'events'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { coreMock } from 'src/core/server/mocks'; import { ReportingConfig, ReportingCore, ReportingPlugin } from '../server'; import { ReportingSetupDeps, ReportingStartDeps } from '../server/types'; +import { ReportingInternalSetup } from '../server/core'; const createMockSetupDeps = (setupMock?: any): ReportingSetupDeps => { return { elasticsearch: setupMock.elasticsearch, security: setupMock.security, - licensing: {} as any, + licensing: { + license$: of({ isAvailable: true, isActive: true, type: 'basic' }), + } as any, usageCollection: {} as any, __LEGACY: { plugins: { xpack_main: { status: new EventEmitter() } } } as any, }; @@ -49,8 +53,18 @@ const createMockReportingPlugin = async (config: ReportingConfig): Promise => { +export const createMockReportingCore = async ( + config: ReportingConfig, + setupDepsMock?: ReportingInternalSetup +): Promise => { config = config || {}; const plugin = await createMockReportingPlugin(config); - return plugin.getReportingCore(); + const core = plugin.getReportingCore(); + + if (setupDepsMock) { + // @ts-ignore overwriting private properties + core.pluginSetupDeps = setupDepsMock; + } + + return core; }; diff --git a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_server.ts b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_server.ts index 819636b714631..01b9f6cbd9cd6 100644 --- a/x-pack/legacy/plugins/reporting/test_helpers/create_mock_server.ts +++ b/x-pack/legacy/plugins/reporting/test_helpers/create_mock_server.ts @@ -4,9 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ServerFacade } from '../server/types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createHttpServer, createCoreContext } from 'src/core/server/http/test_utils'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { coreMock } from 'src/core/server/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ContextService } from 'src/core/server/context/context_service'; -export const createMockServer = (): ServerFacade => { - const mockServer = {}; - return mockServer as any; +const coreId = Symbol('reporting'); + +export const createMockServer = async () => { + const coreContext = createCoreContext({ coreId }); + const contextService = new ContextService(coreContext); + + const server = createHttpServer(coreContext); + const httpSetup = await server.setup({ + context: contextService.setup({ pluginDependencies: new Map() }), + }); + const handlerContext = coreMock.createRequestHandlerContext(); + + httpSetup.registerRouteHandlerContext(coreId, 'core', async (ctx, req, res) => { + return handlerContext; + }); + + return { + server, + httpSetup, + handlerContext, + }; }; 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..79c57e564b4e1 100644 --- a/x-pack/legacy/plugins/spaces/index.ts +++ b/x-pack/legacy/plugins/spaces/index.ts @@ -8,27 +8,22 @@ 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', configPrefix: 'xpack.spaces', publicDir: resolve(__dirname, 'public'), require: ['kibana', 'elasticsearch', 'xpack_main'], - + config(Joi: any) { + return Joi.object({ + enabled: Joi.boolean().default(true), + }) + .unknown() + .default(); + }, uiExports: { managementSections: [], apps: [], @@ -79,15 +74,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/actions/README.md b/x-pack/plugins/actions/README.md index 847172ae972fd..96d5f04ac088f 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -26,7 +26,7 @@ Table of Contents - [Executor](#executor) - [Example](#example) - [RESTful API](#restful-api) - - [`POST /api/action`: Create action](#post-apiaction-create-action) + - [`POST /api/actions/action`: Create action](#post-apiaction-create-action) - [`DELETE /api/actions/action/{id}`: Delete action](#delete-apiactionid-delete-action) - [`GET /api/actions`: Get all actions](#get-apiactiongetall-get-all-actions) - [`GET /api/actions/action/{id}`: Get action](#get-apiactionid-get-action) @@ -163,7 +163,7 @@ The built-in email action type provides a good example of creating an action typ Using an action type requires an action to be created that will contain and encrypt configuration for a given action type. See below for CRUD operations using the API. -### `POST /api/action`: Create action +### `POST /api/actions/action`: Create action Payload: diff --git a/x-pack/plugins/actions/server/actions_client.mock.ts b/x-pack/plugins/actions/server/actions_client.mock.ts index 64b43e1ab6bbc..a2b64e49f76e3 100644 --- a/x-pack/plugins/actions/server/actions_client.mock.ts +++ b/x-pack/plugins/actions/server/actions_client.mock.ts @@ -16,6 +16,7 @@ const createActionsClientMock = () => { delete: jest.fn(), update: jest.fn(), getAll: jest.fn(), + getBulk: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 0132cc8bdb01a..bf55a1c18d169 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -423,6 +423,74 @@ describe('getAll()', () => { }); }); +describe('getBulk()', () => { + test('calls getBulk savedObjectsClient with parameters', async () => { + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actionTypeId: 'test', + name: 'test', + config: { + foo: 'bar', + }, + }, + references: [], + }, + ], + }); + scopedClusterClient.callAsInternalUser.mockResolvedValueOnce({ + aggregations: { + '1': { doc_count: 6 }, + testPreconfigured: { doc_count: 2 }, + }, + }); + + actionsClient = new ActionsClient({ + actionTypeRegistry, + savedObjectsClient, + scopedClusterClient, + defaultKibanaIndex, + preconfiguredActions: [ + { + id: 'testPreconfigured', + actionTypeId: '.slack', + secrets: {}, + isPreconfigured: true, + name: 'test', + config: { + foo: 'bar', + }, + }, + ], + }); + const result = await actionsClient.getBulk(['1', 'testPreconfigured']); + expect(result).toEqual([ + { + actionTypeId: '.slack', + config: { + foo: 'bar', + }, + id: 'testPreconfigured', + isPreconfigured: true, + name: 'test', + secrets: {}, + }, + { + actionTypeId: 'test', + config: { + foo: 'bar', + }, + id: '1', + isPreconfigured: false, + name: 'test', + }, + ]); + }); +}); + describe('delete()', () => { test('calls savedObjectsClient with id', async () => { const expectedResult = Symbol(); diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index c9052cf53d948..48703f01f5509 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.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 Boom from 'boom'; import { IScopedClusterClient, SavedObjectsClientContract, @@ -193,6 +193,44 @@ export class ActionsClient { ); } + /** + * Get bulk actions with preconfigured list + */ + public async getBulk(ids: string[]): Promise { + const actionResults = new Array(); + for (const actionId of ids) { + const action = this.preconfiguredActions.find( + (preconfiguredAction) => preconfiguredAction.id === actionId + ); + if (action !== undefined) { + actionResults.push(action); + } + } + + // Fetch action objects in bulk + // Excluding preconfigured actions to avoid an not found error, which is already added + const actionSavedObjectsIds = [ + ...new Set( + ids.filter( + (actionId) => !actionResults.find((actionResult) => actionResult.id === actionId) + ) + ), + ]; + + const bulkGetOpts = actionSavedObjectsIds.map((id) => ({ id, type: 'action' })); + const bulkGetResult = await this.savedObjectsClient.bulkGet(bulkGetOpts); + + for (const action of bulkGetResult.saved_objects) { + if (action.error) { + throw Boom.badRequest( + `Failed to load action ${action.id} (${action.error.statusCode}): ${action.error.message}` + ); + } + actionResults.push(actionFromSavedObject(action)); + } + return actionResults; + } + /** * Delete action */ diff --git a/x-pack/plugins/alerting_builtins/README.md b/x-pack/plugins/alerting_builtins/README.md index 233984a1ff23f..2944247e4714c 100644 --- a/x-pack/plugins/alerting_builtins/README.md +++ b/x-pack/plugins/alerting_builtins/README.md @@ -1,7 +1,7 @@ # alerting_builtins plugin This plugin provides alertTypes shipped with Kibana for use with the -[the alerting plugin](../alerting/README.md). When enabled, it will register +[the alerts plugin](../alerts/README.md). When enabled, it will register the built-in alertTypes with the alerting plugin, register associated HTTP routes, etc. diff --git a/x-pack/plugins/alerting_builtins/kibana.json b/x-pack/plugins/alerting_builtins/kibana.json index 78de9a1ae0165..cc613d5247ef4 100644 --- a/x-pack/plugins/alerting_builtins/kibana.json +++ b/x-pack/plugins/alerting_builtins/kibana.json @@ -3,7 +3,7 @@ "server": true, "version": "8.0.0", "kibanaVersion": "kibana", - "requiredPlugins": ["alerting"], + "requiredPlugins": ["alerts"], "configPath": ["xpack", "alerting_builtins"], "ui": false } diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index.ts index 475efc87b443a..d9232195b0f52 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index.ts @@ -10,7 +10,7 @@ import { register as registerIndexThreshold } from './index_threshold'; interface RegisterBuiltInAlertTypesParams { service: Service; router: IRouter; - alerting: AlertingSetup; + alerts: AlertingSetup; baseRoute: string; } diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/action_context.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/action_context.ts index 15139ae34c93d..c3a132bc609d6 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/action_context.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/action_context.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { Params } from './alert_type_params'; -import { AlertExecutorOptions } from '../../../../alerting/server'; +import { AlertExecutorOptions } from '../../../../alerts/server'; // alert type context provided to actions diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/index.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/index.ts index fbe107054ce9d..9787ece323c59 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/index.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/index.ts @@ -23,14 +23,14 @@ export function getService() { interface RegisterParams { service: Service; router: IRouter; - alerting: AlertingSetup; + alerts: AlertingSetup; baseRoute: string; } export function register(params: RegisterParams) { - const { service, router, alerting, baseRoute } = params; + const { service, router, alerts, baseRoute } = params; - alerting.registerType(getAlertType(service)); + alerts.registerType(getAlertType(service)); const baseBuiltInRoute = `${baseRoute}/index_threshold`; registerRoutes({ service, router, baseRoute: baseBuiltInRoute }); diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/date_range_info.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/date_range_info.ts index 0a4accc983d79..fa991786a60b6 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/date_range_info.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/date_range_info.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { times } from 'lodash'; -import { parseDuration } from '../../../../../alerting/server'; +import { parseDuration } from '../../../../../alerts/server'; import { MAX_INTERVALS } from '../index'; // dates as numbers are epoch millis diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts index 40e6f187ce18f..a22395cb0961b 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; -import { parseDuration } from '../../../../../alerting/server'; +import { parseDuration } from '../../../../../alerts/server'; import { MAX_INTERVALS } from '../index'; import { CoreQueryParamsSchemaProperties, validateCoreQueryBody } from './core_query_types'; import { diff --git a/x-pack/plugins/alerting_builtins/server/plugin.test.ts b/x-pack/plugins/alerting_builtins/server/plugin.test.ts index f93041fa3c142..71a904dcbab3d 100644 --- a/x-pack/plugins/alerting_builtins/server/plugin.test.ts +++ b/x-pack/plugins/alerting_builtins/server/plugin.test.ts @@ -6,7 +6,7 @@ import { AlertingBuiltinsPlugin } from './plugin'; import { coreMock } from '../../../../src/core/server/mocks'; -import { alertsMock } from '../../../plugins/alerting/server/mocks'; +import { alertsMock } from '../../alerts/server/mocks'; describe('AlertingBuiltins Plugin', () => { describe('setup()', () => { @@ -22,7 +22,7 @@ describe('AlertingBuiltins Plugin', () => { it('should register built-in alert types', async () => { const alertingSetup = alertsMock.createSetup(); - await plugin.setup(coreSetup, { alerting: alertingSetup }); + await plugin.setup(coreSetup, { alerts: alertingSetup }); expect(alertingSetup.registerType).toHaveBeenCalledTimes(1); @@ -44,7 +44,7 @@ describe('AlertingBuiltins Plugin', () => { it('should return a service in the expected shape', async () => { const alertingSetup = alertsMock.createSetup(); - const service = await plugin.setup(coreSetup, { alerting: alertingSetup }); + const service = await plugin.setup(coreSetup, { alerts: alertingSetup }); expect(typeof service.indexThreshold.timeSeriesQuery).toBe('function'); }); diff --git a/x-pack/plugins/alerting_builtins/server/plugin.ts b/x-pack/plugins/alerting_builtins/server/plugin.ts index 9a9483f9c9dfa..12d1b080c7c63 100644 --- a/x-pack/plugins/alerting_builtins/server/plugin.ts +++ b/x-pack/plugins/alerting_builtins/server/plugin.ts @@ -22,11 +22,11 @@ export class AlertingBuiltinsPlugin implements Plugin { }; } - public async setup(core: CoreSetup, { alerting }: AlertingBuiltinsDeps): Promise { + public async setup(core: CoreSetup, { alerts }: AlertingBuiltinsDeps): Promise { registerBuiltInAlertTypes({ service: this.service, router: core.http.createRouter(), - alerting, + alerts, baseRoute: '/api/alerting_builtins', }); return this.service; diff --git a/x-pack/plugins/alerting_builtins/server/types.ts b/x-pack/plugins/alerting_builtins/server/types.ts index ff07b85fd3038..95d34371a6d1e 100644 --- a/x-pack/plugins/alerting_builtins/server/types.ts +++ b/x-pack/plugins/alerting_builtins/server/types.ts @@ -5,7 +5,7 @@ */ import { Logger, ScopedClusterClient } from '../../../../src/core/server'; -import { PluginSetupContract as AlertingSetup } from '../../alerting/server'; +import { PluginSetupContract as AlertingSetup } from '../../alerts/server'; import { getService as getServiceIndexThreshold } from './alert_types/index_threshold'; export { Logger, IRouter } from '../../../../src/core/server'; @@ -14,11 +14,11 @@ export { PluginSetupContract as AlertingSetup, AlertType, AlertExecutorOptions, -} from '../../alerting/server'; +} from '../../alerts/server'; // this plugin's dependendencies export interface AlertingBuiltinsDeps { - alerting: AlertingSetup; + alerts: AlertingSetup; } // external service exposed through plugin setup/start diff --git a/x-pack/plugins/alerting/README.md b/x-pack/plugins/alerts/README.md similarity index 91% rename from x-pack/plugins/alerting/README.md rename to x-pack/plugins/alerts/README.md index dfa2991895429..811478426a8d3 100644 --- a/x-pack/plugins/alerting/README.md +++ b/x-pack/plugins/alerts/README.md @@ -20,20 +20,20 @@ Table of Contents - [Example](#example) - [Alert Navigation](#alert-navigation) - [RESTful API](#restful-api) - - [`POST /api/alert`: Create alert](#post-apialert-create-alert) - - [`DELETE /api/alert/{id}`: Delete alert](#delete-apialertid-delete-alert) - - [`GET /api/alert/_find`: Find alerts](#get-apialertfind-find-alerts) - - [`GET /api/alert/{id}`: Get alert](#get-apialertid-get-alert) - - [`GET /api/alert/{id}/state`: Get alert state](#get-apialertidstate-get-alert-state) - - [`GET /api/alert/types`: List alert types](#get-apialerttypes-list-alert-types) - - [`PUT /api/alert/{id}`: Update alert](#put-apialertid-update-alert) - - [`POST /api/alert/{id}/_enable`: Enable an alert](#post-apialertidenable-enable-an-alert) - - [`POST /api/alert/{id}/_disable`: Disable an alert](#post-apialertiddisable-disable-an-alert) - - [`POST /api/alert/{id}/_mute_all`: Mute all alert instances](#post-apialertidmuteall-mute-all-alert-instances) - - [`POST /api/alert/{alertId}/alert_instance/{alertInstanceId}/_mute`: Mute alert instance](#post-apialertalertidalertinstancealertinstanceidmute-mute-alert-instance) - - [`POST /api/alert/{id}/_unmute_all`: Unmute all alert instances](#post-apialertidunmuteall-unmute-all-alert-instances) - - [`POST /api/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute`: Unmute an alert instance](#post-apialertalertidalertinstancealertinstanceidunmute-unmute-an-alert-instance) - - [`POST /api/alert/{id}/_update_api_key`: Update alert API key](#post-apialertidupdateapikey-update-alert-api-key) + - [`POST /api/alerts/alert`: Create alert](#post-apialert-create-alert) + - [`DELETE /api/alerts/alert/{id}`: Delete alert](#delete-apialertid-delete-alert) + - [`GET /api/alerts/_find`: Find alerts](#get-apialertfind-find-alerts) + - [`GET /api/alerts/alert/{id}`: Get alert](#get-apialertid-get-alert) + - [`GET /api/alerts/alert/{id}/state`: Get alert state](#get-apialertidstate-get-alert-state) + - [`GET /api/alerts/list_alert_types`: List alert types](#get-apialerttypes-list-alert-types) + - [`PUT /api/alerts/alert/{id}`: Update alert](#put-apialertid-update-alert) + - [`POST /api/alerts/alert/{id}/_enable`: Enable an alert](#post-apialertidenable-enable-an-alert) + - [`POST /api/alerts/alert/{id}/_disable`: Disable an alert](#post-apialertiddisable-disable-an-alert) + - [`POST /api/alerts/alert/{id}/_mute_all`: Mute all alert instances](#post-apialertidmuteall-mute-all-alert-instances) + - [`POST /api/alerts/alert/{alert_id}/alert_instance/{alert_instance_id}/_mute`: Mute alert instance](#post-apialertalertidalertinstancealertinstanceidmute-mute-alert-instance) + - [`POST /api/alerts/alert/{id}/_unmute_all`: Unmute all alert instances](#post-apialertidunmuteall-unmute-all-alert-instances) + - [`POST /api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute`: Unmute an alert instance](#post-apialertalertidalertinstancealertinstanceidunmute-unmute-an-alert-instance) + - [`POST /api/alerts/alert/{id}/_update_api_key`: Update alert API key](#post-apialertidupdateapikey-update-alert-api-key) - [Schedule Formats](#schedule-formats) - [Alert instance factory](#alert-instance-factory) - [Templating actions](#templating-actions) @@ -78,7 +78,7 @@ Note that the `manage_own_api_key` cluster privilege is not enough - it can be u ### Methods -**server.newPlatform.setup.plugins.alerting.registerType(options)** +**server.newPlatform.setup.plugins.alerts.registerType(options)** The following table describes the properties of the `options` object. @@ -139,7 +139,7 @@ This example receives server and threshold as parameters. It will read the CPU u ```typescript import { schema } from '@kbn/config-schema'; ... -server.newPlatform.setup.plugins.alerting.registerType({ +server.newPlatform.setup.plugins.alerts.registerType({ id: 'my-alert-type', name: 'My alert type', validate: { @@ -220,7 +220,7 @@ server.newPlatform.setup.plugins.alerting.registerType({ This example only receives threshold as a parameter. It will read the CPU usage of all the servers and schedule individual actions if the reading for a server is greater than the threshold. This is a better implementation than above as only one query is performed for all the servers instead of one query per server. ```typescript -server.newPlatform.setup.plugins.alerting.registerType({ +server.newPlatform.setup.plugins.alerts.registerType({ id: 'my-alert-type', name: 'My alert type', validate: { @@ -352,7 +352,7 @@ You can use the `registerNavigation` api to specify as many AlertType specific h Using an alert type requires you to create an alert that will contain parameters and actions for a given alert type. See below for CRUD operations using the API. -### `POST /api/alert`: Create alert +### `POST /api/alerts/alert`: Create alert Payload: @@ -367,7 +367,7 @@ Payload: |params|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object| |actions|Array of the following:
    - `group` (string): We support grouping actions in the scenario of escalations or different types of alert instances. If you don't need this, feel free to use `default` as a value.
    - `id` (string): The id of the action saved object to execute.
    - `params` (object): The map to the `params` the action type will receive. In order to help apply context to strings, we handle them as mustache templates and pass in a default set of context. (see templating actions).|array| -### `DELETE /api/alert/{id}`: Delete alert +### `DELETE /api/alerts/alert/{id}`: Delete alert Params: @@ -375,13 +375,13 @@ Params: |---|---|---| |id|The id of the alert you're trying to delete.|string| -### `GET /api/alert/_find`: Find alerts +### `GET /api/alerts/_find`: Find alerts Params: See the saved objects API documentation for find. All the properties are the same except you cannot pass in `type`. -### `GET /api/alert/{id}`: Get alert +### `GET /api/alerts/alert/{id}`: Get alert Params: @@ -389,7 +389,7 @@ Params: |---|---|---| |id|The id of the alert you're trying to get.|string| -### `GET /api/alert/{id}/state`: Get alert state +### `GET /api/alerts/alert/{id}/state`: Get alert state Params: @@ -397,11 +397,11 @@ Params: |---|---|---| |id|The id of the alert whose state you're trying to get.|string| -### `GET /api/alert/types`: List alert types +### `GET /api/alerts/list_alert_types`: List alert types No parameters. -### `PUT /api/alert/{id}`: Update alert +### `PUT /api/alerts/alert/{id}`: Update alert Params: @@ -420,7 +420,7 @@ Payload: |params|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object| |actions|Array of the following:
    - `group` (string): We support grouping actions in the scenario of escalations or different types of alert instances. If you don't need this, feel free to use `default` as a value.
    - `id` (string): The id of the action saved object to execute.
    - `params` (object): There map to the `params` the action type will receive. In order to help apply context to strings, we handle them as mustache templates and pass in a default set of context. (see templating actions).|array| -### `POST /api/alert/{id}/_enable`: Enable an alert +### `POST /api/alerts/alert/{id}/_enable`: Enable an alert Params: @@ -428,7 +428,7 @@ Params: |---|---|---| |id|The id of the alert you're trying to enable.|string| -### `POST /api/alert/{id}/_disable`: Disable an alert +### `POST /api/alerts/alert/{id}/_disable`: Disable an alert Params: @@ -436,7 +436,7 @@ Params: |---|---|---| |id|The id of the alert you're trying to disable.|string| -### `POST /api/alert/{id}/_mute_all`: Mute all alert instances +### `POST /api/alerts/alert/{id}/_mute_all`: Mute all alert instances Params: @@ -444,7 +444,7 @@ Params: |---|---|---| |id|The id of the alert you're trying to mute all alert instances for.|string| -### `POST /api/alert/{alertId}/alert_instance/{alertInstanceId}/_mute`: Mute alert instance +### `POST /api/alerts/alert/{alert_id}/alert_instance/{alert_instance_id}/_mute`: Mute alert instance Params: @@ -453,7 +453,7 @@ Params: |alertId|The id of the alert you're trying to mute an instance for.|string| |alertInstanceId|The instance id of the alert instance you're trying to mute.|string| -### `POST /api/alert/{id}/_unmute_all`: Unmute all alert instances +### `POST /api/alerts/alert/{id}/_unmute_all`: Unmute all alert instances Params: @@ -461,7 +461,7 @@ Params: |---|---|---| |id|The id of the alert you're trying to unmute all alert instances for.|string| -### `POST /api/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute`: Unmute an alert instance +### `POST /api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute`: Unmute an alert instance Params: @@ -470,7 +470,7 @@ Params: |alertId|The id of the alert you're trying to unmute an instance for.|string| |alertInstanceId|The instance id of the alert instance you're trying to unmute.|string| -### `POST /api/alert/{id}/_update_api_key`: Update alert API key +### `POST /api/alerts/alert/{id}/_update_api_key`: Update alert API key |Property|Description|Type| |---|---|---| diff --git a/x-pack/plugins/alerting/common/alert.ts b/x-pack/plugins/alerts/common/alert.ts similarity index 100% rename from x-pack/plugins/alerting/common/alert.ts rename to x-pack/plugins/alerts/common/alert.ts diff --git a/x-pack/plugins/alerting/common/alert_instance.ts b/x-pack/plugins/alerts/common/alert_instance.ts similarity index 100% rename from x-pack/plugins/alerting/common/alert_instance.ts rename to x-pack/plugins/alerts/common/alert_instance.ts diff --git a/x-pack/plugins/alerting/common/alert_navigation.ts b/x-pack/plugins/alerts/common/alert_navigation.ts similarity index 100% rename from x-pack/plugins/alerting/common/alert_navigation.ts rename to x-pack/plugins/alerts/common/alert_navigation.ts diff --git a/x-pack/plugins/alerting/common/alert_task_instance.ts b/x-pack/plugins/alerts/common/alert_task_instance.ts similarity index 100% rename from x-pack/plugins/alerting/common/alert_task_instance.ts rename to x-pack/plugins/alerts/common/alert_task_instance.ts diff --git a/x-pack/plugins/alerting/common/alert_type.ts b/x-pack/plugins/alerts/common/alert_type.ts similarity index 100% rename from x-pack/plugins/alerting/common/alert_type.ts rename to x-pack/plugins/alerts/common/alert_type.ts diff --git a/x-pack/plugins/alerting/common/date_from_string.test.ts b/x-pack/plugins/alerts/common/date_from_string.test.ts similarity index 100% rename from x-pack/plugins/alerting/common/date_from_string.test.ts rename to x-pack/plugins/alerts/common/date_from_string.test.ts diff --git a/x-pack/plugins/alerting/common/date_from_string.ts b/x-pack/plugins/alerts/common/date_from_string.ts similarity index 100% rename from x-pack/plugins/alerting/common/date_from_string.ts rename to x-pack/plugins/alerts/common/date_from_string.ts diff --git a/x-pack/plugins/alerting/common/index.ts b/x-pack/plugins/alerts/common/index.ts similarity index 92% rename from x-pack/plugins/alerting/common/index.ts rename to x-pack/plugins/alerts/common/index.ts index 2574e73dd4f9a..88a8da5a3e575 100644 --- a/x-pack/plugins/alerting/common/index.ts +++ b/x-pack/plugins/alerts/common/index.ts @@ -20,4 +20,4 @@ export interface AlertingFrameworkHealth { hasPermanentEncryptionKey: boolean; } -export const BASE_ALERT_API_PATH = '/api/alert'; +export const BASE_ALERT_API_PATH = '/api/alerts'; diff --git a/x-pack/plugins/alerting/common/parse_duration.test.ts b/x-pack/plugins/alerts/common/parse_duration.test.ts similarity index 100% rename from x-pack/plugins/alerting/common/parse_duration.test.ts rename to x-pack/plugins/alerts/common/parse_duration.test.ts diff --git a/x-pack/plugins/alerting/common/parse_duration.ts b/x-pack/plugins/alerts/common/parse_duration.ts similarity index 100% rename from x-pack/plugins/alerting/common/parse_duration.ts rename to x-pack/plugins/alerts/common/parse_duration.ts diff --git a/x-pack/plugins/alerting/kibana.json b/x-pack/plugins/alerts/kibana.json similarity index 80% rename from x-pack/plugins/alerting/kibana.json rename to x-pack/plugins/alerts/kibana.json index 59c4bb2221b0a..3509f79dbbe4d 100644 --- a/x-pack/plugins/alerting/kibana.json +++ b/x-pack/plugins/alerts/kibana.json @@ -1,10 +1,10 @@ { - "id": "alerting", + "id": "alerts", "server": true, "ui": true, "version": "8.0.0", "kibanaVersion": "kibana", - "configPath": ["xpack", "alerting"], + "configPath": ["xpack", "alerts"], "requiredPlugins": ["licensing", "taskManager", "encryptedSavedObjects", "actions", "eventLog"], "optionalPlugins": ["usageCollection", "spaces", "security"] } diff --git a/x-pack/plugins/alerting/public/alert_api.test.ts b/x-pack/plugins/alerts/public/alert_api.test.ts similarity index 92% rename from x-pack/plugins/alerting/public/alert_api.test.ts rename to x-pack/plugins/alerts/public/alert_api.test.ts index 1149e6fc249a9..45b9f5ba8fe2e 100644 --- a/x-pack/plugins/alerting/public/alert_api.test.ts +++ b/x-pack/plugins/alerts/public/alert_api.test.ts @@ -31,7 +31,7 @@ describe('loadAlertTypes', () => { expect(result).toEqual(resolvedValue); expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "/api/alert/types", + "/api/alerts/list_alert_types", ] `); }); @@ -53,7 +53,7 @@ describe('loadAlertType', () => { expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "/api/alert/types", + "/api/alerts/list_alert_types", ] `); }); @@ -111,7 +111,7 @@ describe('loadAlert', () => { http.get.mockResolvedValueOnce(resolvedValue); expect(await loadAlert({ http, alertId })).toEqual(resolvedValue); - expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}`); + expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}`); }); }); @@ -130,7 +130,7 @@ describe('loadAlertState', () => { http.get.mockResolvedValueOnce(resolvedValue); expect(await loadAlertState({ http, alertId })).toEqual(resolvedValue); - expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}/state`); + expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}/state`); }); test('should parse AlertInstances', async () => { @@ -167,7 +167,7 @@ describe('loadAlertState', () => { }, }, }); - expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}/state`); + expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}/state`); }); test('should handle empty response from api', async () => { @@ -175,6 +175,6 @@ describe('loadAlertState', () => { http.get.mockResolvedValueOnce(''); expect(await loadAlertState({ http, alertId })).toEqual({}); - expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}/state`); + expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}/state`); }); }); diff --git a/x-pack/plugins/alerting/public/alert_api.ts b/x-pack/plugins/alerts/public/alert_api.ts similarity index 84% rename from x-pack/plugins/alerting/public/alert_api.ts rename to x-pack/plugins/alerts/public/alert_api.ts index ee9432885d671..5b7cd2128f386 100644 --- a/x-pack/plugins/alerting/public/alert_api.ts +++ b/x-pack/plugins/alerts/public/alert_api.ts @@ -16,7 +16,7 @@ import { BASE_ALERT_API_PATH, alertStateSchema } from '../common'; import { Alert, AlertType, AlertTaskState } from '../common'; export async function loadAlertTypes({ http }: { http: HttpSetup }): Promise { - return await http.get(`${BASE_ALERT_API_PATH}/types`); + return await http.get(`${BASE_ALERT_API_PATH}/list_alert_types`); } export async function loadAlertType({ @@ -27,11 +27,11 @@ export async function loadAlertType({ id: AlertType['id']; }): Promise { const maybeAlertType = findFirst((type) => type.id === id)( - await http.get(`${BASE_ALERT_API_PATH}/types`) + await http.get(`${BASE_ALERT_API_PATH}/list_alert_types`) ); if (isNone(maybeAlertType)) { throw new Error( - i18n.translate('xpack.alerting.loadAlertType.missingAlertTypeError', { + i18n.translate('xpack.alerts.loadAlertType.missingAlertTypeError', { defaultMessage: 'Alert type "{id}" is not registered.', values: { id, @@ -49,7 +49,7 @@ export async function loadAlert({ http: HttpSetup; alertId: string; }): Promise { - return await http.get(`${BASE_ALERT_API_PATH}/${alertId}`); + return await http.get(`${BASE_ALERT_API_PATH}/alert/${alertId}`); } type EmptyHttpResponse = ''; @@ -61,7 +61,7 @@ export async function loadAlertState({ alertId: string; }): Promise { return await http - .get(`${BASE_ALERT_API_PATH}/${alertId}/state`) + .get(`${BASE_ALERT_API_PATH}/alert/${alertId}/state`) .then((state: AlertTaskState | EmptyHttpResponse) => (state ? state : {})) .then((state: AlertTaskState) => { return pipe( diff --git a/x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.mock.ts b/x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.mock.ts similarity index 100% rename from x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.mock.ts rename to x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.mock.ts diff --git a/x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.test.ts b/x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.test.ts similarity index 100% rename from x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.test.ts rename to x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.test.ts diff --git a/x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.ts b/x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.ts similarity index 90% rename from x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.ts rename to x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.ts index f30629789b4ed..933ed442523c8 100644 --- a/x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.ts +++ b/x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.ts @@ -36,7 +36,7 @@ export class AlertNavigationRegistry { public registerDefault(consumer: string, handler: AlertNavigationHandler) { if (this.hasDefaultHandler(consumer)) { throw new Error( - i18n.translate('xpack.alerting.alertNavigationRegistry.register.duplicateDefaultError', { + i18n.translate('xpack.alerts.alertNavigationRegistry.register.duplicateDefaultError', { defaultMessage: 'Default Navigation within "{consumer}" is already registered.', values: { consumer, @@ -54,7 +54,7 @@ export class AlertNavigationRegistry { public register(consumer: string, alertType: AlertType, handler: AlertNavigationHandler) { if (this.hasTypedHandler(consumer, alertType)) { throw new Error( - i18n.translate('xpack.alerting.alertNavigationRegistry.register.duplicateNavigationError', { + i18n.translate('xpack.alerts.alertNavigationRegistry.register.duplicateNavigationError', { defaultMessage: 'Navigation for Alert type "{alertType}" within "{consumer}" is already registered.', values: { @@ -78,7 +78,7 @@ export class AlertNavigationRegistry { } throw new Error( - i18n.translate('xpack.alerting.alertNavigationRegistry.get.missingNavigationError', { + i18n.translate('xpack.alerts.alertNavigationRegistry.get.missingNavigationError', { defaultMessage: 'Navigation for Alert type "{alertType}" within "{consumer}" is not registered.', values: { diff --git a/x-pack/plugins/alerting/public/alert_navigation_registry/index.ts b/x-pack/plugins/alerts/public/alert_navigation_registry/index.ts similarity index 100% rename from x-pack/plugins/alerting/public/alert_navigation_registry/index.ts rename to x-pack/plugins/alerts/public/alert_navigation_registry/index.ts diff --git a/x-pack/plugins/alerting/public/alert_navigation_registry/types.ts b/x-pack/plugins/alerts/public/alert_navigation_registry/types.ts similarity index 100% rename from x-pack/plugins/alerting/public/alert_navigation_registry/types.ts rename to x-pack/plugins/alerts/public/alert_navigation_registry/types.ts diff --git a/x-pack/plugins/alerting/public/index.ts b/x-pack/plugins/alerts/public/index.ts similarity index 100% rename from x-pack/plugins/alerting/public/index.ts rename to x-pack/plugins/alerts/public/index.ts diff --git a/x-pack/plugins/alerting/public/mocks.ts b/x-pack/plugins/alerts/public/mocks.ts similarity index 100% rename from x-pack/plugins/alerting/public/mocks.ts rename to x-pack/plugins/alerts/public/mocks.ts diff --git a/x-pack/plugins/alerting/public/plugin.ts b/x-pack/plugins/alerts/public/plugin.ts similarity index 100% rename from x-pack/plugins/alerting/public/plugin.ts rename to x-pack/plugins/alerts/public/plugin.ts diff --git a/x-pack/plugins/alerting/server/alert_instance/alert_instance.test.ts b/x-pack/plugins/alerts/server/alert_instance/alert_instance.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/alert_instance/alert_instance.test.ts rename to x-pack/plugins/alerts/server/alert_instance/alert_instance.test.ts diff --git a/x-pack/plugins/alerting/server/alert_instance/alert_instance.ts b/x-pack/plugins/alerts/server/alert_instance/alert_instance.ts similarity index 100% rename from x-pack/plugins/alerting/server/alert_instance/alert_instance.ts rename to x-pack/plugins/alerts/server/alert_instance/alert_instance.ts diff --git a/x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.test.ts b/x-pack/plugins/alerts/server/alert_instance/create_alert_instance_factory.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.test.ts rename to x-pack/plugins/alerts/server/alert_instance/create_alert_instance_factory.test.ts diff --git a/x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.ts b/x-pack/plugins/alerts/server/alert_instance/create_alert_instance_factory.ts similarity index 100% rename from x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.ts rename to x-pack/plugins/alerts/server/alert_instance/create_alert_instance_factory.ts diff --git a/x-pack/plugins/alerting/server/alert_instance/index.ts b/x-pack/plugins/alerts/server/alert_instance/index.ts similarity index 100% rename from x-pack/plugins/alerting/server/alert_instance/index.ts rename to x-pack/plugins/alerts/server/alert_instance/index.ts diff --git a/x-pack/plugins/alerting/server/alert_type_registry.mock.ts b/x-pack/plugins/alerts/server/alert_type_registry.mock.ts similarity index 100% rename from x-pack/plugins/alerting/server/alert_type_registry.mock.ts rename to x-pack/plugins/alerts/server/alert_type_registry.mock.ts diff --git a/x-pack/plugins/alerting/server/alert_type_registry.test.ts b/x-pack/plugins/alerts/server/alert_type_registry.test.ts similarity index 98% rename from x-pack/plugins/alerting/server/alert_type_registry.test.ts rename to x-pack/plugins/alerts/server/alert_type_registry.test.ts index e78e5ab7932c2..6d7cf621ab0ca 100644 --- a/x-pack/plugins/alerting/server/alert_type_registry.test.ts +++ b/x-pack/plugins/alerts/server/alert_type_registry.test.ts @@ -7,7 +7,7 @@ import { TaskRunnerFactory } from './task_runner'; import { AlertTypeRegistry } from './alert_type_registry'; import { AlertType } from './types'; -import { taskManagerMock } from '../../../plugins/task_manager/server/task_manager.mock'; +import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; const taskManager = taskManagerMock.setup(); const alertTypeRegistryParams = { diff --git a/x-pack/plugins/alerting/server/alert_type_registry.ts b/x-pack/plugins/alerts/server/alert_type_registry.ts similarity index 89% rename from x-pack/plugins/alerting/server/alert_type_registry.ts rename to x-pack/plugins/alerts/server/alert_type_registry.ts index 0163cb71166e8..8f36afe062aa5 100644 --- a/x-pack/plugins/alerting/server/alert_type_registry.ts +++ b/x-pack/plugins/alerts/server/alert_type_registry.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; import { i18n } from '@kbn/i18n'; -import { RunContext, TaskManagerSetupContract } from '../../../plugins/task_manager/server'; +import { RunContext, TaskManagerSetupContract } from '../../task_manager/server'; import { TaskRunnerFactory } from './task_runner'; import { AlertType } from './types'; @@ -32,7 +32,7 @@ export class AlertTypeRegistry { public register(alertType: AlertType) { if (this.has(alertType.id)) { throw new Error( - i18n.translate('xpack.alerting.alertTypeRegistry.register.duplicateAlertTypeError', { + i18n.translate('xpack.alerts.alertTypeRegistry.register.duplicateAlertTypeError', { defaultMessage: 'Alert type "{id}" is already registered.', values: { id: alertType.id, @@ -55,7 +55,7 @@ export class AlertTypeRegistry { public get(id: string): AlertType { if (!this.has(id)) { throw Boom.badRequest( - i18n.translate('xpack.alerting.alertTypeRegistry.get.missingAlertTypeError', { + i18n.translate('xpack.alerts.alertTypeRegistry.get.missingAlertTypeError', { defaultMessage: 'Alert type "{id}" is not registered.', values: { id, diff --git a/x-pack/plugins/alerting/server/alerts_client.mock.ts b/x-pack/plugins/alerts/server/alerts_client.mock.ts similarity index 100% rename from x-pack/plugins/alerting/server/alerts_client.mock.ts rename to x-pack/plugins/alerts/server/alerts_client.mock.ts diff --git a/x-pack/plugins/alerting/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts similarity index 98% rename from x-pack/plugins/alerting/server/alerts_client.test.ts rename to x-pack/plugins/alerts/server/alerts_client.test.ts index fa86c27651136..9685f58b8fb31 100644 --- a/x-pack/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -7,12 +7,13 @@ import uuid from 'uuid'; import { schema } from '@kbn/config-schema'; import { AlertsClient, CreateOptions } from './alerts_client'; import { savedObjectsClientMock, loggingServiceMock } from '../../../../src/core/server/mocks'; -import { taskManagerMock } from '../../../plugins/task_manager/server/task_manager.mock'; +import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; import { alertTypeRegistryMock } from './alert_type_registry.mock'; -import { TaskStatus } from '../../../plugins/task_manager/server'; +import { TaskStatus } from '../../task_manager/server'; import { IntervalSchedule } from './types'; import { resolvable } from './test_utils'; -import { encryptedSavedObjectsMock } from '../../../plugins/encrypted_saved_objects/server/mocks'; +import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; +import { actionsClientMock } from '../../actions/server/mocks'; const taskManager = taskManagerMock.start(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -30,7 +31,7 @@ const alertsClientParams = { invalidateAPIKey: jest.fn(), logger: loggingServiceMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, - preconfiguredActions: [], + getActionsClient: jest.fn(), }; beforeEach(() => { @@ -42,6 +43,34 @@ beforeEach(() => { }); alertsClientParams.getUserName.mockResolvedValue('elastic'); taskManager.runNow.mockResolvedValue({ id: '' }); + const actionsClient = actionsClientMock.create(); + actionsClient.getBulk.mockResolvedValueOnce([ + { + id: '1', + isPreconfigured: false, + actionTypeId: 'test', + name: 'test', + config: { + foo: 'bar', + }, + }, + { + id: '2', + isPreconfigured: false, + actionTypeId: 'test2', + name: 'test2', + config: { + foo: 'bar', + }, + }, + { + id: 'testPreconfigured', + actionTypeId: '.slack', + isPreconfigured: true, + name: 'test', + }, + ]); + alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); }); const mockedDate = new Date('2019-02-12T21:01:22.479Z'); @@ -97,18 +126,6 @@ describe('create()', () => { test('creates an alert', async () => { const data = getMockData(); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actionTypeId: 'test', - }, - references: [], - }, - ], - }); savedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -297,26 +314,6 @@ describe('create()', () => { }, ], }); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actionTypeId: 'test', - }, - references: [], - }, - { - id: '2', - type: 'action', - attributes: { - actionTypeId: 'test2', - }, - references: [], - }, - ], - }); savedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -435,16 +432,6 @@ describe('create()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, } `); - expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith([ - { - id: '1', - type: 'action', - }, - { - id: '2', - type: 'action', - }, - ]); }); test('creates a disabled alert', async () => { @@ -549,7 +536,9 @@ describe('create()', () => { test('throws error if loading actions fails', async () => { const data = getMockData(); - savedObjectsClient.bulkGet.mockRejectedValueOnce(new Error('Test Error')); + const actionsClient = actionsClientMock.create(); + actionsClient.getBulk.mockRejectedValueOnce(new Error('Test Error')); + alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( `"Test Error"` ); @@ -1688,7 +1677,7 @@ describe('find()', () => { }, ], }); - const result = await alertsClient.find(); + const result = await alertsClient.find({ options: {} }); expect(result).toMatchInlineSnapshot(` Object { "data": Array [ @@ -1903,26 +1892,6 @@ describe('update()', () => { }); test('updates given parameters', async () => { - savedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actionTypeId: 'test', - }, - references: [], - }, - { - id: '2', - type: 'action', - attributes: { - actionTypeId: 'test2', - }, - references: [], - }, - ], - }); savedObjectsClient.update.mockResolvedValueOnce({ id: '1', type: 'alert', diff --git a/x-pack/plugins/alerting/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts similarity index 87% rename from x-pack/plugins/alerting/server/alerts_client.ts rename to x-pack/plugins/alerts/server/alerts_client.ts index e43939e2f44c3..6b091a5a4503b 100644 --- a/x-pack/plugins/alerting/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -13,7 +13,7 @@ import { SavedObjectReference, SavedObject, } from 'src/core/server'; -import { PreConfiguredAction } from '../../actions/server'; +import { ActionsClient } from '../../actions/server'; import { Alert, PartialAlert, @@ -24,16 +24,15 @@ import { IntervalSchedule, SanitizedAlert, AlertTaskState, - RawAlertAction, } from './types'; import { validateAlertTypeParams } from './lib'; import { InvalidateAPIKeyParams, GrantAPIKeyResult as SecurityPluginGrantAPIKeyResult, InvalidateAPIKeyResult as SecurityPluginInvalidateAPIKeyResult, -} from '../../../plugins/security/server'; -import { EncryptedSavedObjectsClient } from '../../../plugins/encrypted_saved_objects/server'; -import { TaskManagerStartContract } from '../../../plugins/task_manager/server'; +} from '../../security/server'; +import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server'; +import { TaskManagerStartContract } from '../../task_manager/server'; import { taskInstanceToAlertTaskInstance } from './task_runner/alert_task_instance'; import { deleteTaskIfItExists } from './lib/delete_task_if_it_exists'; @@ -56,25 +55,32 @@ interface ConstructorOptions { getUserName: () => Promise; createAPIKey: () => Promise; invalidateAPIKey: (params: InvalidateAPIKeyParams) => Promise; - preconfiguredActions: PreConfiguredAction[]; + getActionsClient: () => Promise; } -export interface FindOptions { - options?: { - perPage?: number; - page?: number; - search?: string; - defaultSearchOperator?: 'AND' | 'OR'; - searchFields?: string[]; - sortField?: string; - sortOrder?: string; - hasReference?: { - type: string; - id: string; - }; - fields?: string[]; - filter?: string; +export interface MuteOptions extends IndexType { + alertId: string; + alertInstanceId: string; +} + +export interface FindOptions extends IndexType { + perPage?: number; + page?: number; + search?: string; + defaultSearchOperator?: 'AND' | 'OR'; + searchFields?: string[]; + sortField?: string; + sortOrder?: string; + hasReference?: { + type: string; + id: string; }; + fields?: string[]; + filter?: string; +} + +interface IndexType { + [key: string]: unknown; } export interface FindResult { @@ -127,7 +133,7 @@ export class AlertsClient { private readonly invalidateAPIKey: ( params: InvalidateAPIKeyParams ) => Promise; - private preconfiguredActions: PreConfiguredAction[]; + private readonly getActionsClient: () => Promise; encryptedSavedObjectsClient: EncryptedSavedObjectsClient; constructor({ @@ -141,7 +147,7 @@ export class AlertsClient { createAPIKey, invalidateAPIKey, encryptedSavedObjectsClient, - preconfiguredActions, + getActionsClient, }: ConstructorOptions) { this.logger = logger; this.getUserName = getUserName; @@ -153,7 +159,7 @@ export class AlertsClient { this.createAPIKey = createAPIKey; this.invalidateAPIKey = invalidateAPIKey; this.encryptedSavedObjectsClient = encryptedSavedObjectsClient; - this.preconfiguredActions = preconfiguredActions; + this.getActionsClient = getActionsClient; } public async create({ data, options }: CreateOptions): Promise { @@ -226,7 +232,7 @@ export class AlertsClient { } } - public async find({ options = {} }: FindOptions = {}): Promise { + public async find({ options = {} }: { options: FindOptions }): Promise { const { page, per_page: perPage, @@ -534,13 +540,7 @@ export class AlertsClient { }); } - public async muteInstance({ - alertId, - alertInstanceId, - }: { - alertId: string; - alertInstanceId: string; - }) { + public async muteInstance({ alertId, alertInstanceId }: MuteOptions) { const { attributes, version } = await this.savedObjectsClient.get('alert', alertId); const mutedInstanceIds = attributes.mutedInstanceIds || []; if (!attributes.muteAll && !mutedInstanceIds.includes(alertInstanceId)) { @@ -600,7 +600,7 @@ export class AlertsClient { actions: RawAlert['actions'], references: SavedObjectReference[] ) { - return actions.map((action, i) => { + return actions.map((action) => { const reference = references.find((ref) => ref.name === action.actionRef); if (!reference) { throw new Error(`Reference ${action.actionRef} not found`); @@ -653,7 +653,7 @@ export class AlertsClient { ); if (invalidActionGroups.length) { throw Boom.badRequest( - i18n.translate('xpack.alerting.alertsClient.validateActions.invalidGroups', { + i18n.translate('xpack.alerts.alertsClient.validateActions.invalidGroups', { defaultMessage: 'Invalid action groups: {groups}', values: { groups: invalidActionGroups.join(', '), @@ -666,58 +666,31 @@ export class AlertsClient { private async denormalizeActions( alertActions: NormalizedAlertAction[] ): Promise<{ actions: RawAlert['actions']; references: SavedObjectReference[] }> { - const actionMap = new Map(); - // map preconfigured actions - for (const alertAction of alertActions) { - const action = this.preconfiguredActions.find( - (preconfiguredAction) => preconfiguredAction.id === alertAction.id - ); - if (action !== undefined) { - actionMap.set(action.id, action); - } - } - // Fetch action objects in bulk - // Excluding preconfigured actions to avoid an not found error, which is already mapped - const actionIds = [ - ...new Set( - alertActions - .filter((alertAction) => !actionMap.has(alertAction.id)) - .map((alertAction) => alertAction.id) - ), - ]; - if (actionIds.length > 0) { - const bulkGetOpts = actionIds.map((id) => ({ id, type: 'action' })); - const bulkGetResult = await this.savedObjectsClient.bulkGet(bulkGetOpts); - - for (const action of bulkGetResult.saved_objects) { - if (action.error) { - throw Boom.badRequest( - `Failed to load action ${action.id} (${action.error.statusCode}): ${action.error.message}` - ); - } - actionMap.set(action.id, action); - } - } - // Extract references and set actionTypeId + const actionsClient = await this.getActionsClient(); + const actionIds = [...new Set(alertActions.map((alertAction) => alertAction.id))]; + const actionResults = await actionsClient.getBulk(actionIds); const references: SavedObjectReference[] = []; const actions = alertActions.map(({ id, ...alertAction }, i) => { - const actionRef = `action_${i}`; - references.push({ - id, - name: actionRef, - type: 'action', - }); - const actionMapValue = actionMap.get(id); - // if action is a save object, than actionTypeId should be under attributes property - // if action is a preconfigured, than actionTypeId is the action property - const actionTypeId = actionIds.find((actionId) => actionId === id) - ? (actionMapValue as SavedObject>).attributes.actionTypeId - : (actionMapValue as RawAlertAction).actionTypeId; - return { - ...alertAction, - actionRef, - actionTypeId, - }; + const actionResultValue = actionResults.find((action) => action.id === id); + if (actionResultValue) { + const actionRef = `action_${i}`; + references.push({ + id, + name: actionRef, + type: 'action', + }); + return { + ...alertAction, + actionRef, + actionTypeId: actionResultValue.actionTypeId, + }; + } else { + return { + ...alertAction, + actionRef: '', + actionTypeId: '', + }; + } }); return { actions, diff --git a/x-pack/plugins/alerting/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts similarity index 87% rename from x-pack/plugins/alerting/server/alerts_client_factory.test.ts rename to x-pack/plugins/alerts/server/alerts_client_factory.test.ts index cc792d11c890d..50dafba00a7e4 100644 --- a/x-pack/plugins/alerting/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts @@ -7,12 +7,13 @@ import { Request } from 'hapi'; import { AlertsClientFactory, AlertsClientFactoryOpts } from './alerts_client_factory'; import { alertTypeRegistryMock } from './alert_type_registry.mock'; -import { taskManagerMock } from '../../../plugins/task_manager/server/task_manager.mock'; +import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; import { KibanaRequest } from '../../../../src/core/server'; import { loggingServiceMock, savedObjectsClientMock } from '../../../../src/core/server/mocks'; -import { encryptedSavedObjectsMock } from '../../../plugins/encrypted_saved_objects/server/mocks'; -import { AuthenticatedUser } from '../../../plugins/security/public'; -import { securityMock } from '../../../plugins/security/server/mocks'; +import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; +import { AuthenticatedUser } from '../../security/public'; +import { securityMock } from '../../security/server/mocks'; +import { actionsMock } from '../../actions/server/mocks'; jest.mock('./alerts_client'); @@ -25,7 +26,7 @@ const alertsClientFactoryParams: jest.Mocked = { getSpaceId: jest.fn(), spaceIdToNamespace: jest.fn(), encryptedSavedObjectsClient: encryptedSavedObjectsMock.createClient(), - preconfiguredActions: [], + actions: actionsMock.createStart(), }; const fakeRequest = ({ headers: {}, @@ -65,7 +66,7 @@ test('creates an alerts client with proper constructor arguments', async () => { createAPIKey: expect.any(Function), invalidateAPIKey: expect.any(Function), encryptedSavedObjectsClient: alertsClientFactoryParams.encryptedSavedObjectsClient, - preconfiguredActions: [], + getActionsClient: expect.any(Function), }); }); @@ -95,6 +96,16 @@ test('getUserName() returns a name when security is enabled', async () => { expect(userNameResult).toEqual('bob'); }); +test('getActionsClient() returns ActionsClient', async () => { + const factory = new AlertsClientFactory(); + factory.initialize(alertsClientFactoryParams); + factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient); + const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; + + const actionsClient = await constructorCall.getActionsClient(); + expect(actionsClient).not.toBe(null); +}); + test('createAPIKey() returns { apiKeysEnabled: false } when security is disabled', async () => { const factory = new AlertsClientFactory(); factory.initialize(alertsClientFactoryParams); diff --git a/x-pack/plugins/alerting/server/alerts_client_factory.ts b/x-pack/plugins/alerts/server/alerts_client_factory.ts similarity index 87% rename from x-pack/plugins/alerting/server/alerts_client_factory.ts rename to x-pack/plugins/alerts/server/alerts_client_factory.ts index 913b4e2e81fe1..af546f965d7df 100644 --- a/x-pack/plugins/alerting/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PreConfiguredAction } from '../../actions/server'; +import { PluginStartContract as ActionsPluginStartContract } from '../../actions/server'; import { AlertsClient } from './alerts_client'; import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types'; import { KibanaRequest, Logger, SavedObjectsClientContract } from '../../../../src/core/server'; -import { InvalidateAPIKeyParams, SecurityPluginSetup } from '../../../plugins/security/server'; -import { EncryptedSavedObjectsClient } from '../../../plugins/encrypted_saved_objects/server'; -import { TaskManagerStartContract } from '../../../plugins/task_manager/server'; +import { InvalidateAPIKeyParams, SecurityPluginSetup } from '../../security/server'; +import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server'; +import { TaskManagerStartContract } from '../../task_manager/server'; export interface AlertsClientFactoryOpts { logger: Logger; @@ -20,7 +20,7 @@ export interface AlertsClientFactoryOpts { getSpaceId: (request: KibanaRequest) => string | undefined; spaceIdToNamespace: SpaceIdToNamespaceFunction; encryptedSavedObjectsClient: EncryptedSavedObjectsClient; - preconfiguredActions: PreConfiguredAction[]; + actions: ActionsPluginStartContract; } export class AlertsClientFactory { @@ -32,7 +32,7 @@ export class AlertsClientFactory { private getSpaceId!: (request: KibanaRequest) => string | undefined; private spaceIdToNamespace!: SpaceIdToNamespaceFunction; private encryptedSavedObjectsClient!: EncryptedSavedObjectsClient; - private preconfiguredActions!: PreConfiguredAction[]; + private actions!: ActionsPluginStartContract; public initialize(options: AlertsClientFactoryOpts) { if (this.isInitialized) { @@ -46,14 +46,14 @@ export class AlertsClientFactory { this.securityPluginSetup = options.securityPluginSetup; this.spaceIdToNamespace = options.spaceIdToNamespace; this.encryptedSavedObjectsClient = options.encryptedSavedObjectsClient; - this.preconfiguredActions = options.preconfiguredActions; + this.actions = options.actions; } public create( request: KibanaRequest, savedObjectsClient: SavedObjectsClientContract ): AlertsClient { - const { securityPluginSetup } = this; + const { securityPluginSetup, actions } = this; const spaceId = this.getSpaceId(request); return new AlertsClient({ spaceId, @@ -104,7 +104,9 @@ export class AlertsClientFactory { result: invalidateAPIKeyResult, }; }, - preconfiguredActions: this.preconfiguredActions, + async getActionsClient() { + return actions.getActionsClientWithRequest(request); + }, }); } } diff --git a/x-pack/plugins/alerting/server/constants/plugin.ts b/x-pack/plugins/alerts/server/constants/plugin.ts similarity index 86% rename from x-pack/plugins/alerting/server/constants/plugin.ts rename to x-pack/plugins/alerts/server/constants/plugin.ts index 9c276ed1d75de..c180b68680841 100644 --- a/x-pack/plugins/alerting/server/constants/plugin.ts +++ b/x-pack/plugins/alerts/server/constants/plugin.ts @@ -7,12 +7,12 @@ import { LICENSE_TYPE_BASIC, LicenseType } from '../../../../legacy/common/constants'; export const PLUGIN = { - ID: 'alerting', + ID: 'alerts', MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_BASIC as LicenseType, // TODO: supposed to be changed up on requirements // all plugins seem to use getI18nName with any // eslint-disable-next-line @typescript-eslint/no-explicit-any getI18nName: (i18n: any): string => - i18n.translate('xpack.alerting.appName', { - defaultMessage: 'Alerting', + i18n.translate('xpack.alerts.appName', { + defaultMessage: 'Alerts', }), }; diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerts/server/index.ts similarity index 100% rename from x-pack/plugins/alerting/server/index.ts rename to x-pack/plugins/alerts/server/index.ts diff --git a/x-pack/plugins/alerting/server/lib/delete_task_if_it_exists.test.ts b/x-pack/plugins/alerts/server/lib/delete_task_if_it_exists.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/lib/delete_task_if_it_exists.test.ts rename to x-pack/plugins/alerts/server/lib/delete_task_if_it_exists.test.ts diff --git a/x-pack/plugins/alerting/server/lib/delete_task_if_it_exists.ts b/x-pack/plugins/alerts/server/lib/delete_task_if_it_exists.ts similarity index 100% rename from x-pack/plugins/alerting/server/lib/delete_task_if_it_exists.ts rename to x-pack/plugins/alerts/server/lib/delete_task_if_it_exists.ts diff --git a/x-pack/plugins/alerting/server/lib/index.ts b/x-pack/plugins/alerts/server/lib/index.ts similarity index 100% rename from x-pack/plugins/alerting/server/lib/index.ts rename to x-pack/plugins/alerts/server/lib/index.ts diff --git a/x-pack/plugins/alerting/server/lib/is_alert_not_found_error.test.ts b/x-pack/plugins/alerts/server/lib/is_alert_not_found_error.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/lib/is_alert_not_found_error.test.ts rename to x-pack/plugins/alerts/server/lib/is_alert_not_found_error.test.ts diff --git a/x-pack/plugins/alerting/server/lib/is_alert_not_found_error.ts b/x-pack/plugins/alerts/server/lib/is_alert_not_found_error.ts similarity index 100% rename from x-pack/plugins/alerting/server/lib/is_alert_not_found_error.ts rename to x-pack/plugins/alerts/server/lib/is_alert_not_found_error.ts diff --git a/x-pack/plugins/alerting/server/lib/license_api_access.ts b/x-pack/plugins/alerts/server/lib/license_api_access.ts similarity index 100% rename from x-pack/plugins/alerting/server/lib/license_api_access.ts rename to x-pack/plugins/alerts/server/lib/license_api_access.ts diff --git a/x-pack/plugins/alerting/server/lib/license_state.mock.ts b/x-pack/plugins/alerts/server/lib/license_state.mock.ts similarity index 100% rename from x-pack/plugins/alerting/server/lib/license_state.mock.ts rename to x-pack/plugins/alerts/server/lib/license_state.mock.ts diff --git a/x-pack/plugins/alerting/server/lib/license_state.test.ts b/x-pack/plugins/alerts/server/lib/license_state.test.ts similarity index 95% rename from x-pack/plugins/alerting/server/lib/license_state.test.ts rename to x-pack/plugins/alerts/server/lib/license_state.test.ts index cbab98a6311dd..50b4e6b4439f7 100644 --- a/x-pack/plugins/alerting/server/lib/license_state.test.ts +++ b/x-pack/plugins/alerts/server/lib/license_state.test.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { LicenseState } from './license_state'; -import { licensingMock } from '../../../../plugins/licensing/server/mocks'; +import { licensingMock } from '../../../licensing/server/mocks'; describe('license_state', () => { const getRawLicense = jest.fn(); diff --git a/x-pack/plugins/alerting/server/lib/license_state.ts b/x-pack/plugins/alerts/server/lib/license_state.ts similarity index 90% rename from x-pack/plugins/alerting/server/lib/license_state.ts rename to x-pack/plugins/alerts/server/lib/license_state.ts index 211d7a75dc4fa..ea0106f717b02 100644 --- a/x-pack/plugins/alerting/server/lib/license_state.ts +++ b/x-pack/plugins/alerts/server/lib/license_state.ts @@ -7,7 +7,7 @@ import Boom from 'boom'; import { i18n } from '@kbn/i18n'; import { Observable, Subscription } from 'rxjs'; -import { ILicense } from '../../../../plugins/licensing/common/types'; +import { ILicense } from '../../../licensing/common/types'; import { assertNever } from '../../../../../src/core/server'; import { PLUGIN } from '../constants/plugin'; @@ -43,10 +43,10 @@ export class LicenseState { showAppLink: true, enableAppLink: false, message: i18n.translate( - 'xpack.alerting.serverSideErrors.unavailableLicenseInformationErrorMessage', + 'xpack.alerts.serverSideErrors.unavailableLicenseInformationErrorMessage', { defaultMessage: - 'Alerting is unavailable - license information is not available at this time.', + 'Alerts is unavailable - license information is not available at this time.', } ), }; diff --git a/x-pack/plugins/alerting/server/lib/result_type.ts b/x-pack/plugins/alerts/server/lib/result_type.ts similarity index 100% rename from x-pack/plugins/alerting/server/lib/result_type.ts rename to x-pack/plugins/alerts/server/lib/result_type.ts diff --git a/x-pack/plugins/alerting/server/lib/types.test.ts b/x-pack/plugins/alerts/server/lib/types.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/lib/types.test.ts rename to x-pack/plugins/alerts/server/lib/types.test.ts diff --git a/x-pack/plugins/alerting/server/lib/types.ts b/x-pack/plugins/alerts/server/lib/types.ts similarity index 100% rename from x-pack/plugins/alerting/server/lib/types.ts rename to x-pack/plugins/alerts/server/lib/types.ts diff --git a/x-pack/plugins/alerting/server/lib/validate_alert_type_params.test.ts b/x-pack/plugins/alerts/server/lib/validate_alert_type_params.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/lib/validate_alert_type_params.test.ts rename to x-pack/plugins/alerts/server/lib/validate_alert_type_params.test.ts diff --git a/x-pack/plugins/alerting/server/lib/validate_alert_type_params.ts b/x-pack/plugins/alerts/server/lib/validate_alert_type_params.ts similarity index 100% rename from x-pack/plugins/alerting/server/lib/validate_alert_type_params.ts rename to x-pack/plugins/alerts/server/lib/validate_alert_type_params.ts diff --git a/x-pack/plugins/alerting/server/mocks.ts b/x-pack/plugins/alerts/server/mocks.ts similarity index 100% rename from x-pack/plugins/alerting/server/mocks.ts rename to x-pack/plugins/alerts/server/mocks.ts diff --git a/x-pack/plugins/alerting/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts similarity index 97% rename from x-pack/plugins/alerting/server/plugin.test.ts rename to x-pack/plugins/alerts/server/plugin.test.ts index 0411899290ab2..008a9bb804c5b 100644 --- a/x-pack/plugins/alerting/server/plugin.test.ts +++ b/x-pack/plugins/alerts/server/plugin.test.ts @@ -6,8 +6,8 @@ import { AlertingPlugin, AlertingPluginsSetup, AlertingPluginsStart } from './plugin'; import { coreMock } from '../../../../src/core/server/mocks'; -import { licensingMock } from '../../../plugins/licensing/server/mocks'; -import { encryptedSavedObjectsMock } from '../../../plugins/encrypted_saved_objects/server/mocks'; +import { licensingMock } from '../../licensing/server/mocks'; +import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; import { taskManagerMock } from '../../task_manager/server/mocks'; import { eventLogServiceMock } from '../../event_log/server/event_log_service.mock'; import { KibanaRequest, CoreSetup } from 'kibana/server'; diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts similarity index 98% rename from x-pack/plugins/alerting/server/plugin.ts rename to x-pack/plugins/alerts/server/plugin.ts index 33fb8d9e0d212..324bc9fbfb72b 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -53,7 +53,7 @@ import { LicensingPluginSetup } from '../../licensing/server'; import { PluginSetupContract as ActionsPluginSetupContract, PluginStartContract as ActionsPluginStartContract, -} from '../../../plugins/actions/server'; +} from '../../actions/server'; import { Services } from './types'; import { registerAlertsUsageCollector } from './usage'; import { initializeAlertingTelemetry, scheduleAlertingTelemetry } from './usage/task'; @@ -216,7 +216,7 @@ export class AlertingPlugin { getSpaceId(request: KibanaRequest) { return spaces?.getSpaceId(request); }, - preconfiguredActions: plugins.actions.preconfiguredActions, + actions: plugins.actions, }); taskRunnerFactory.initialize({ diff --git a/x-pack/plugins/alerting/server/routes/_mock_handler_arguments.ts b/x-pack/plugins/alerts/server/routes/_mock_handler_arguments.ts similarity index 100% rename from x-pack/plugins/alerting/server/routes/_mock_handler_arguments.ts rename to x-pack/plugins/alerts/server/routes/_mock_handler_arguments.ts diff --git a/x-pack/plugins/alerting/server/routes/create.test.ts b/x-pack/plugins/alerts/server/routes/create.test.ts similarity index 98% rename from x-pack/plugins/alerting/server/routes/create.test.ts rename to x-pack/plugins/alerts/server/routes/create.test.ts index a4910495c8a40..9e941903eeaed 100644 --- a/x-pack/plugins/alerting/server/routes/create.test.ts +++ b/x-pack/plugins/alerts/server/routes/create.test.ts @@ -74,7 +74,7 @@ describe('createAlertRoute', () => { const [config, handler] = router.post.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ diff --git a/x-pack/plugins/alerting/server/routes/create.ts b/x-pack/plugins/alerts/server/routes/create.ts similarity index 98% rename from x-pack/plugins/alerting/server/routes/create.ts rename to x-pack/plugins/alerts/server/routes/create.ts index cc3b7d48162e3..6238fca024e55 100644 --- a/x-pack/plugins/alerting/server/routes/create.ts +++ b/x-pack/plugins/alerts/server/routes/create.ts @@ -43,7 +43,7 @@ export const bodySchema = schema.object({ export const createAlertRoute = (router: IRouter, licenseState: LicenseState) => { router.post( { - path: BASE_ALERT_API_PATH, + path: `${BASE_ALERT_API_PATH}/alert`, validate: { body: bodySchema, }, diff --git a/x-pack/plugins/alerting/server/routes/delete.test.ts b/x-pack/plugins/alerts/server/routes/delete.test.ts similarity index 97% rename from x-pack/plugins/alerting/server/routes/delete.test.ts rename to x-pack/plugins/alerts/server/routes/delete.test.ts index 416628d015b5a..9ba4e20312e17 100644 --- a/x-pack/plugins/alerting/server/routes/delete.test.ts +++ b/x-pack/plugins/alerts/server/routes/delete.test.ts @@ -29,7 +29,7 @@ describe('deleteAlertRoute', () => { const [config, handler] = router.delete.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ diff --git a/x-pack/plugins/alerting/server/routes/delete.ts b/x-pack/plugins/alerts/server/routes/delete.ts similarity index 96% rename from x-pack/plugins/alerting/server/routes/delete.ts rename to x-pack/plugins/alerts/server/routes/delete.ts index f5a7add632edc..2034bd21fbed6 100644 --- a/x-pack/plugins/alerting/server/routes/delete.ts +++ b/x-pack/plugins/alerts/server/routes/delete.ts @@ -23,7 +23,7 @@ const paramSchema = schema.object({ export const deleteAlertRoute = (router: IRouter, licenseState: LicenseState) => { router.delete( { - path: `${BASE_ALERT_API_PATH}/{id}`, + path: `${BASE_ALERT_API_PATH}/alert/{id}`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/disable.test.ts b/x-pack/plugins/alerts/server/routes/disable.test.ts similarity index 95% rename from x-pack/plugins/alerting/server/routes/disable.test.ts rename to x-pack/plugins/alerts/server/routes/disable.test.ts index fde095e9145b6..a82d09854a604 100644 --- a/x-pack/plugins/alerting/server/routes/disable.test.ts +++ b/x-pack/plugins/alerts/server/routes/disable.test.ts @@ -29,7 +29,7 @@ describe('disableAlertRoute', () => { const [config, handler] = router.post.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/_disable"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_disable"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ diff --git a/x-pack/plugins/alerting/server/routes/disable.ts b/x-pack/plugins/alerts/server/routes/disable.ts similarity index 96% rename from x-pack/plugins/alerting/server/routes/disable.ts rename to x-pack/plugins/alerts/server/routes/disable.ts index e1eb089cf4e85..dfc5dfbdd5aa2 100644 --- a/x-pack/plugins/alerting/server/routes/disable.ts +++ b/x-pack/plugins/alerts/server/routes/disable.ts @@ -23,7 +23,7 @@ const paramSchema = schema.object({ export const disableAlertRoute = (router: IRouter, licenseState: LicenseState) => { router.post( { - path: `${BASE_ALERT_API_PATH}/{id}/_disable`, + path: `${BASE_ALERT_API_PATH}/alert/{id}/_disable`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/enable.test.ts b/x-pack/plugins/alerts/server/routes/enable.test.ts similarity index 95% rename from x-pack/plugins/alerting/server/routes/enable.test.ts rename to x-pack/plugins/alerts/server/routes/enable.test.ts index e4e89e3f06380..4ee3a12a59dc7 100644 --- a/x-pack/plugins/alerting/server/routes/enable.test.ts +++ b/x-pack/plugins/alerts/server/routes/enable.test.ts @@ -28,7 +28,7 @@ describe('enableAlertRoute', () => { const [config, handler] = router.post.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/_enable"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_enable"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ diff --git a/x-pack/plugins/alerting/server/routes/enable.ts b/x-pack/plugins/alerts/server/routes/enable.ts similarity index 96% rename from x-pack/plugins/alerting/server/routes/enable.ts rename to x-pack/plugins/alerts/server/routes/enable.ts index 90e8f552898d9..b6f86b97d6a3a 100644 --- a/x-pack/plugins/alerting/server/routes/enable.ts +++ b/x-pack/plugins/alerts/server/routes/enable.ts @@ -24,7 +24,7 @@ const paramSchema = schema.object({ export const enableAlertRoute = (router: IRouter, licenseState: LicenseState) => { router.post( { - path: `${BASE_ALERT_API_PATH}/{id}/_enable`, + path: `${BASE_ALERT_API_PATH}/alert/{id}/_enable`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/find.test.ts b/x-pack/plugins/alerts/server/routes/find.test.ts similarity index 93% rename from x-pack/plugins/alerting/server/routes/find.test.ts rename to x-pack/plugins/alerts/server/routes/find.test.ts index cc601bd42b8ca..f20ee0a54dcd9 100644 --- a/x-pack/plugins/alerting/server/routes/find.test.ts +++ b/x-pack/plugins/alerts/server/routes/find.test.ts @@ -30,7 +30,7 @@ describe('findAlertRoute', () => { const [config, handler] = router.get.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/_find"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/_find"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ @@ -76,13 +76,8 @@ describe('findAlertRoute', () => { Object { "options": Object { "defaultSearchOperator": "OR", - "fields": undefined, - "filter": undefined, "page": 1, "perPage": 1, - "search": undefined, - "sortField": undefined, - "sortOrder": undefined, }, }, ] diff --git a/x-pack/plugins/alerting/server/routes/find.ts b/x-pack/plugins/alerts/server/routes/find.ts similarity index 81% rename from x-pack/plugins/alerting/server/routes/find.ts rename to x-pack/plugins/alerts/server/routes/find.ts index 3de95c9580cd4..80c9c20eec7da 100644 --- a/x-pack/plugins/alerting/server/routes/find.ts +++ b/x-pack/plugins/alerts/server/routes/find.ts @@ -12,10 +12,11 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; -import { FindOptions } from '../../../alerting/server'; import { LicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; import { BASE_ALERT_API_PATH } from '../../common'; +import { renameKeys } from './lib/rename_keys'; +import { FindOptions } from '..'; // config definition const querySchema = schema.object({ @@ -63,31 +64,29 @@ export const findAlertRoute = (router: IRouter, licenseState: LicenseState) => { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); } const alertsClient = context.alerting.getAlertsClient(); + const query = req.query; - const options: FindOptions['options'] = { - perPage: query.per_page, - page: query.page, - search: query.search, - defaultSearchOperator: query.default_search_operator, - sortField: query.sort_field, - fields: query.fields, - filter: query.filter, - sortOrder: query.sort_order, + const renameMap = { + default_search_operator: 'defaultSearchOperator', + fields: 'fields', + has_reference: 'hasReference', + page: 'page', + per_page: 'perPage', + search: 'search', + sort_field: 'sortField', + sort_order: 'sortOrder', + filter: 'filter', }; + const options = renameKeys>(renameMap, query); + if (query.search_fields) { options.searchFields = Array.isArray(query.search_fields) ? query.search_fields : [query.search_fields]; } - if (query.has_reference) { - options.hasReference = query.has_reference; - } - - const findResult = await alertsClient.find({ - options, - }); + const findResult = await alertsClient.find({ options }); return res.ok({ body: findResult, }); diff --git a/x-pack/plugins/alerting/server/routes/get.test.ts b/x-pack/plugins/alerts/server/routes/get.test.ts similarity index 97% rename from x-pack/plugins/alerting/server/routes/get.test.ts rename to x-pack/plugins/alerts/server/routes/get.test.ts index 7335f13c85a4d..b11224ff4794e 100644 --- a/x-pack/plugins/alerting/server/routes/get.test.ts +++ b/x-pack/plugins/alerts/server/routes/get.test.ts @@ -60,7 +60,7 @@ describe('getAlertRoute', () => { getAlertRoute(router, licenseState); const [config, handler] = router.get.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ diff --git a/x-pack/plugins/alerting/server/routes/get.ts b/x-pack/plugins/alerts/server/routes/get.ts similarity index 96% rename from x-pack/plugins/alerting/server/routes/get.ts rename to x-pack/plugins/alerts/server/routes/get.ts index cd78e7fbacddb..ae9ebe1299371 100644 --- a/x-pack/plugins/alerting/server/routes/get.ts +++ b/x-pack/plugins/alerts/server/routes/get.ts @@ -23,7 +23,7 @@ const paramSchema = schema.object({ export const getAlertRoute = (router: IRouter, licenseState: LicenseState) => { router.get( { - path: `${BASE_ALERT_API_PATH}/{id}`, + path: `${BASE_ALERT_API_PATH}/alert/{id}`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts b/x-pack/plugins/alerts/server/routes/get_alert_state.test.ts similarity index 94% rename from x-pack/plugins/alerting/server/routes/get_alert_state.test.ts rename to x-pack/plugins/alerts/server/routes/get_alert_state.test.ts index 20a420ca00986..8c9051093f85b 100644 --- a/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts +++ b/x-pack/plugins/alerts/server/routes/get_alert_state.test.ts @@ -47,7 +47,7 @@ describe('getAlertStateRoute', () => { const [config, handler] = router.get.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/state"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/state"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ @@ -90,7 +90,7 @@ describe('getAlertStateRoute', () => { const [config, handler] = router.get.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/state"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/state"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ @@ -133,7 +133,7 @@ describe('getAlertStateRoute', () => { const [config, handler] = router.get.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/state"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/state"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ diff --git a/x-pack/plugins/alerting/server/routes/get_alert_state.ts b/x-pack/plugins/alerts/server/routes/get_alert_state.ts similarity index 96% rename from x-pack/plugins/alerting/server/routes/get_alert_state.ts rename to x-pack/plugins/alerts/server/routes/get_alert_state.ts index a5cb14154db67..b27ae3758e1b9 100644 --- a/x-pack/plugins/alerting/server/routes/get_alert_state.ts +++ b/x-pack/plugins/alerts/server/routes/get_alert_state.ts @@ -23,7 +23,7 @@ const paramSchema = schema.object({ export const getAlertStateRoute = (router: IRouter, licenseState: LicenseState) => { router.get( { - path: `${BASE_ALERT_API_PATH}/{id}/state`, + path: `${BASE_ALERT_API_PATH}/alert/{id}/state`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/health.test.ts b/x-pack/plugins/alerts/server/routes/health.test.ts similarity index 99% rename from x-pack/plugins/alerting/server/routes/health.test.ts rename to x-pack/plugins/alerts/server/routes/health.test.ts index 9b1c95c393f56..b3f41e03ebdc9 100644 --- a/x-pack/plugins/alerting/server/routes/health.test.ts +++ b/x-pack/plugins/alerts/server/routes/health.test.ts @@ -31,7 +31,7 @@ describe('healthRoute', () => { const [config] = router.get.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/_health"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/_health"`); }); it('queries the usage api', async () => { diff --git a/x-pack/plugins/alerting/server/routes/health.ts b/x-pack/plugins/alerts/server/routes/health.ts similarity index 98% rename from x-pack/plugins/alerting/server/routes/health.ts rename to x-pack/plugins/alerts/server/routes/health.ts index fb4c5e76a02c9..b66e28b24e8a7 100644 --- a/x-pack/plugins/alerting/server/routes/health.ts +++ b/x-pack/plugins/alerts/server/routes/health.ts @@ -34,7 +34,7 @@ export function healthRoute( ) { router.get( { - path: '/api/alert/_health', + path: '/api/alerts/_health', validate: false, }, router.handleLegacyErrors(async function ( diff --git a/x-pack/plugins/alerting/server/routes/index.ts b/x-pack/plugins/alerts/server/routes/index.ts similarity index 100% rename from x-pack/plugins/alerting/server/routes/index.ts rename to x-pack/plugins/alerts/server/routes/index.ts diff --git a/x-pack/plugins/alerting/server/routes/lib/error_handler.ts b/x-pack/plugins/alerts/server/routes/lib/error_handler.ts similarity index 94% rename from x-pack/plugins/alerting/server/routes/lib/error_handler.ts rename to x-pack/plugins/alerts/server/routes/lib/error_handler.ts index b3cf48c52fe17..e0c620b0670c9 100644 --- a/x-pack/plugins/alerting/server/routes/lib/error_handler.ts +++ b/x-pack/plugins/alerts/server/routes/lib/error_handler.ts @@ -27,7 +27,7 @@ export function handleDisabledApiKeysError( if (isApiKeyDisabledError(e)) { return response.badRequest({ body: new Error( - i18n.translate('xpack.alerting.api.error.disabledApiKeys', { + i18n.translate('xpack.alerts.api.error.disabledApiKeys', { defaultMessage: 'Alerting relies upon API keys which appear to be disabled', }) ), diff --git a/x-pack/plugins/alerts/server/routes/lib/rename_keys.ts b/x-pack/plugins/alerts/server/routes/lib/rename_keys.ts new file mode 100644 index 0000000000000..bfe60a0ecc648 --- /dev/null +++ b/x-pack/plugins/alerts/server/routes/lib/rename_keys.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const renameKeys = , U extends Record>( + keysMap: Record, + obj: Record +): T => + Object.keys(obj).reduce((acc, key) => { + return { + ...acc, + ...{ [keysMap[key] || key]: obj[key] }, + }; + }, {} as T); diff --git a/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts b/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts similarity index 94% rename from x-pack/plugins/alerting/server/routes/list_alert_types.test.ts rename to x-pack/plugins/alerts/server/routes/list_alert_types.test.ts index e940b2d102045..3192154f6664c 100644 --- a/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts +++ b/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts @@ -27,7 +27,7 @@ describe('listAlertTypesRoute', () => { const [config, handler] = router.get.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/types"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/list_alert_types"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ @@ -89,7 +89,7 @@ describe('listAlertTypesRoute', () => { const [config, handler] = router.get.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/types"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/list_alert_types"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ @@ -140,7 +140,7 @@ describe('listAlertTypesRoute', () => { const [config, handler] = router.get.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/types"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/list_alert_types"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ diff --git a/x-pack/plugins/alerting/server/routes/list_alert_types.ts b/x-pack/plugins/alerts/server/routes/list_alert_types.ts similarity index 95% rename from x-pack/plugins/alerting/server/routes/list_alert_types.ts rename to x-pack/plugins/alerts/server/routes/list_alert_types.ts index f5b4e3263f341..51a4558108e29 100644 --- a/x-pack/plugins/alerting/server/routes/list_alert_types.ts +++ b/x-pack/plugins/alerts/server/routes/list_alert_types.ts @@ -18,7 +18,7 @@ import { BASE_ALERT_API_PATH } from '../../common'; export const listAlertTypesRoute = (router: IRouter, licenseState: LicenseState) => { router.get( { - path: `${BASE_ALERT_API_PATH}/types`, + path: `${BASE_ALERT_API_PATH}/list_alert_types`, validate: {}, options: { tags: ['access:alerting-read'], diff --git a/x-pack/plugins/alerting/server/routes/mute_all.test.ts b/x-pack/plugins/alerts/server/routes/mute_all.test.ts similarity index 95% rename from x-pack/plugins/alerting/server/routes/mute_all.test.ts rename to x-pack/plugins/alerts/server/routes/mute_all.test.ts index 5ef9e3694f8f4..bcdb8cbd022ac 100644 --- a/x-pack/plugins/alerting/server/routes/mute_all.test.ts +++ b/x-pack/plugins/alerts/server/routes/mute_all.test.ts @@ -28,7 +28,7 @@ describe('muteAllAlertRoute', () => { const [config, handler] = router.post.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/_mute_all"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_mute_all"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ diff --git a/x-pack/plugins/alerting/server/routes/mute_all.ts b/x-pack/plugins/alerts/server/routes/mute_all.ts similarity index 96% rename from x-pack/plugins/alerting/server/routes/mute_all.ts rename to x-pack/plugins/alerts/server/routes/mute_all.ts index b43a1ec30ed1f..5b05d7231c385 100644 --- a/x-pack/plugins/alerting/server/routes/mute_all.ts +++ b/x-pack/plugins/alerts/server/routes/mute_all.ts @@ -23,7 +23,7 @@ const paramSchema = schema.object({ export const muteAllAlertRoute = (router: IRouter, licenseState: LicenseState) => { router.post( { - path: `${BASE_ALERT_API_PATH}/{id}/_mute_all`, + path: `${BASE_ALERT_API_PATH}/alert/{id}/_mute_all`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/mute_instance.test.ts b/x-pack/plugins/alerts/server/routes/mute_instance.test.ts similarity index 92% rename from x-pack/plugins/alerting/server/routes/mute_instance.test.ts rename to x-pack/plugins/alerts/server/routes/mute_instance.test.ts index 2e6adedb76df9..c382c12de21cd 100644 --- a/x-pack/plugins/alerting/server/routes/mute_instance.test.ts +++ b/x-pack/plugins/alerts/server/routes/mute_instance.test.ts @@ -29,7 +29,7 @@ describe('muteAlertInstanceRoute', () => { const [config, handler] = router.post.mock.calls[0]; expect(config.path).toMatchInlineSnapshot( - `"/api/alert/{alertId}/alert_instance/{alertInstanceId}/_mute"` + `"/api/alerts/alert/{alert_id}/alert_instance/{alert_instance_id}/_mute"` ); expect(config.options).toMatchInlineSnapshot(` Object { @@ -45,8 +45,8 @@ describe('muteAlertInstanceRoute', () => { { alertsClient }, { params: { - alertId: '1', - alertInstanceId: '2', + alert_id: '1', + alert_instance_id: '2', }, }, ['noContent'] diff --git a/x-pack/plugins/alerting/server/routes/mute_instance.ts b/x-pack/plugins/alerts/server/routes/mute_instance.ts similarity index 72% rename from x-pack/plugins/alerting/server/routes/mute_instance.ts rename to x-pack/plugins/alerts/server/routes/mute_instance.ts index c0c69fe9653da..00550f4af3418 100644 --- a/x-pack/plugins/alerting/server/routes/mute_instance.ts +++ b/x-pack/plugins/alerts/server/routes/mute_instance.ts @@ -15,16 +15,18 @@ import { import { LicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; import { BASE_ALERT_API_PATH } from '../../common'; +import { renameKeys } from './lib/rename_keys'; +import { MuteOptions } from '../alerts_client'; const paramSchema = schema.object({ - alertId: schema.string(), - alertInstanceId: schema.string(), + alert_id: schema.string(), + alert_instance_id: schema.string(), }); export const muteAlertInstanceRoute = (router: IRouter, licenseState: LicenseState) => { router.post( { - path: `${BASE_ALERT_API_PATH}/{alertId}/alert_instance/{alertInstanceId}/_mute`, + path: `${BASE_ALERT_API_PATH}/alert/{alert_id}/alert_instance/{alert_instance_id}/_mute`, validate: { params: paramSchema, }, @@ -42,8 +44,14 @@ export const muteAlertInstanceRoute = (router: IRouter, licenseState: LicenseSta return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); } const alertsClient = context.alerting.getAlertsClient(); - const { alertId, alertInstanceId } = req.params; - await alertsClient.muteInstance({ alertId, alertInstanceId }); + + const renameMap = { + alert_id: 'alertId', + alert_instance_id: 'alertInstanceId', + }; + + const renamedQuery = renameKeys>(renameMap, req.params); + await alertsClient.muteInstance(renamedQuery); return res.noContent(); }) ); diff --git a/x-pack/plugins/alerting/server/routes/unmute_all.test.ts b/x-pack/plugins/alerts/server/routes/unmute_all.test.ts similarity index 95% rename from x-pack/plugins/alerting/server/routes/unmute_all.test.ts rename to x-pack/plugins/alerts/server/routes/unmute_all.test.ts index 1756dbd3fb41d..e13af38fe4cb1 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_all.test.ts +++ b/x-pack/plugins/alerts/server/routes/unmute_all.test.ts @@ -27,7 +27,7 @@ describe('unmuteAllAlertRoute', () => { const [config, handler] = router.post.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/_unmute_all"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_unmute_all"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ diff --git a/x-pack/plugins/alerting/server/routes/unmute_all.ts b/x-pack/plugins/alerts/server/routes/unmute_all.ts similarity index 96% rename from x-pack/plugins/alerting/server/routes/unmute_all.ts rename to x-pack/plugins/alerts/server/routes/unmute_all.ts index d4b6e8b7d61b1..1efc9ed40054e 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_all.ts +++ b/x-pack/plugins/alerts/server/routes/unmute_all.ts @@ -23,7 +23,7 @@ const paramSchema = schema.object({ export const unmuteAllAlertRoute = (router: IRouter, licenseState: LicenseState) => { router.post( { - path: `${BASE_ALERT_API_PATH}/{id}/_unmute_all`, + path: `${BASE_ALERT_API_PATH}/alert/{id}/_unmute_all`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts b/x-pack/plugins/alerts/server/routes/unmute_instance.test.ts similarity index 95% rename from x-pack/plugins/alerting/server/routes/unmute_instance.test.ts rename to x-pack/plugins/alerts/server/routes/unmute_instance.test.ts index 9b9542c606741..b2e2f24e91de9 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts +++ b/x-pack/plugins/alerts/server/routes/unmute_instance.test.ts @@ -29,7 +29,7 @@ describe('unmuteAlertInstanceRoute', () => { const [config, handler] = router.post.mock.calls[0]; expect(config.path).toMatchInlineSnapshot( - `"/api/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute"` + `"/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute"` ); expect(config.options).toMatchInlineSnapshot(` Object { diff --git a/x-pack/plugins/alerting/server/routes/unmute_instance.ts b/x-pack/plugins/alerts/server/routes/unmute_instance.ts similarity index 94% rename from x-pack/plugins/alerting/server/routes/unmute_instance.ts rename to x-pack/plugins/alerts/server/routes/unmute_instance.ts index 97ccd8f0adce7..967f9f890c9fb 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_instance.ts +++ b/x-pack/plugins/alerts/server/routes/unmute_instance.ts @@ -24,7 +24,7 @@ const paramSchema = schema.object({ export const unmuteAlertInstanceRoute = (router: IRouter, licenseState: LicenseState) => { router.post( { - path: `${BASE_ALERT_API_PATH}/{alertId}/alert_instance/{alertInstanceId}/_unmute`, + path: `${BASE_ALERT_API_PATH}/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/update.test.ts b/x-pack/plugins/alerts/server/routes/update.test.ts similarity index 98% rename from x-pack/plugins/alerting/server/routes/update.test.ts rename to x-pack/plugins/alerts/server/routes/update.test.ts index cd96f289b8714..c7d23f2670b45 100644 --- a/x-pack/plugins/alerting/server/routes/update.test.ts +++ b/x-pack/plugins/alerts/server/routes/update.test.ts @@ -51,7 +51,7 @@ describe('updateAlertRoute', () => { const [config, handler] = router.put.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ diff --git a/x-pack/plugins/alerting/server/routes/update.ts b/x-pack/plugins/alerts/server/routes/update.ts similarity index 98% rename from x-pack/plugins/alerting/server/routes/update.ts rename to x-pack/plugins/alerts/server/routes/update.ts index 23fea7dc4002f..99b81dfc5b56e 100644 --- a/x-pack/plugins/alerting/server/routes/update.ts +++ b/x-pack/plugins/alerts/server/routes/update.ts @@ -44,7 +44,7 @@ const bodySchema = schema.object({ export const updateAlertRoute = (router: IRouter, licenseState: LicenseState) => { router.put( { - path: `${BASE_ALERT_API_PATH}/{id}`, + path: `${BASE_ALERT_API_PATH}/alert/{id}`, validate: { body: bodySchema, params: paramSchema, diff --git a/x-pack/plugins/alerting/server/routes/update_api_key.test.ts b/x-pack/plugins/alerts/server/routes/update_api_key.test.ts similarity index 95% rename from x-pack/plugins/alerting/server/routes/update_api_key.test.ts rename to x-pack/plugins/alerts/server/routes/update_api_key.test.ts index 0347feb24a235..babae59553b5b 100644 --- a/x-pack/plugins/alerting/server/routes/update_api_key.test.ts +++ b/x-pack/plugins/alerts/server/routes/update_api_key.test.ts @@ -28,7 +28,7 @@ describe('updateApiKeyRoute', () => { const [config, handler] = router.post.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/_update_api_key"`); + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_update_api_key"`); expect(config.options).toMatchInlineSnapshot(` Object { "tags": Array [ diff --git a/x-pack/plugins/alerting/server/routes/update_api_key.ts b/x-pack/plugins/alerts/server/routes/update_api_key.ts similarity index 96% rename from x-pack/plugins/alerting/server/routes/update_api_key.ts rename to x-pack/plugins/alerts/server/routes/update_api_key.ts index 9d88201d7cd43..4736351a25cbd 100644 --- a/x-pack/plugins/alerting/server/routes/update_api_key.ts +++ b/x-pack/plugins/alerts/server/routes/update_api_key.ts @@ -24,7 +24,7 @@ const paramSchema = schema.object({ export const updateApiKeyRoute = (router: IRouter, licenseState: LicenseState) => { router.post( { - path: `${BASE_ALERT_API_PATH}/{id}/_update_api_key`, + path: `${BASE_ALERT_API_PATH}/alert/{id}/_update_api_key`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/saved_objects/index.ts b/x-pack/plugins/alerts/server/saved_objects/index.ts similarity index 100% rename from x-pack/plugins/alerting/server/saved_objects/index.ts rename to x-pack/plugins/alerts/server/saved_objects/index.ts diff --git a/x-pack/plugins/alerting/server/saved_objects/mappings.json b/x-pack/plugins/alerts/server/saved_objects/mappings.json similarity index 100% rename from x-pack/plugins/alerting/server/saved_objects/mappings.json rename to x-pack/plugins/alerts/server/saved_objects/mappings.json diff --git a/x-pack/plugins/alerting/server/task_runner/alert_task_instance.test.ts b/x-pack/plugins/alerts/server/task_runner/alert_task_instance.test.ts similarity index 98% rename from x-pack/plugins/alerting/server/task_runner/alert_task_instance.test.ts rename to x-pack/plugins/alerts/server/task_runner/alert_task_instance.test.ts index 28b3576dffc6e..efac4c5dcdc01 100644 --- a/x-pack/plugins/alerting/server/task_runner/alert_task_instance.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/alert_task_instance.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ConcreteTaskInstance, TaskStatus } from '../../../../plugins/task_manager/server'; +import { ConcreteTaskInstance, TaskStatus } from '../../../task_manager/server'; import { AlertTaskInstance, taskInstanceToAlertTaskInstance } from './alert_task_instance'; import uuid from 'uuid'; import { SanitizedAlert } from '../types'; diff --git a/x-pack/plugins/alerting/server/task_runner/alert_task_instance.ts b/x-pack/plugins/alerts/server/task_runner/alert_task_instance.ts similarity index 95% rename from x-pack/plugins/alerting/server/task_runner/alert_task_instance.ts rename to x-pack/plugins/alerts/server/task_runner/alert_task_instance.ts index 4be506b78493b..a290f3fa33c70 100644 --- a/x-pack/plugins/alerting/server/task_runner/alert_task_instance.ts +++ b/x-pack/plugins/alerts/server/task_runner/alert_task_instance.ts @@ -6,7 +6,7 @@ import * as t from 'io-ts'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; -import { ConcreteTaskInstance } from '../../../../plugins/task_manager/server'; +import { ConcreteTaskInstance } from '../../../task_manager/server'; import { SanitizedAlert, AlertTaskState, diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts rename to x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts similarity index 98% rename from x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts rename to x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts index 61bbab50b1222..3c58c6d9ba288 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts @@ -8,7 +8,7 @@ import { pluck } from 'lodash'; import { AlertAction, State, Context, AlertType } from '../types'; import { Logger } from '../../../../../src/core/server'; import { transformActionParams } from './transform_action_params'; -import { PluginStartContract as ActionsPluginStartContract } from '../../../../plugins/actions/server'; +import { PluginStartContract as ActionsPluginStartContract } from '../../../actions/server'; import { IEventLogger, IEvent, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server'; import { EVENT_LOG_ACTIONS } from '../plugin'; diff --git a/x-pack/plugins/alerting/server/task_runner/get_next_run_at.test.ts b/x-pack/plugins/alerts/server/task_runner/get_next_run_at.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/task_runner/get_next_run_at.test.ts rename to x-pack/plugins/alerts/server/task_runner/get_next_run_at.test.ts diff --git a/x-pack/plugins/alerting/server/task_runner/get_next_run_at.ts b/x-pack/plugins/alerts/server/task_runner/get_next_run_at.ts similarity index 100% rename from x-pack/plugins/alerting/server/task_runner/get_next_run_at.ts rename to x-pack/plugins/alerts/server/task_runner/get_next_run_at.ts diff --git a/x-pack/plugins/alerting/server/task_runner/index.ts b/x-pack/plugins/alerts/server/task_runner/index.ts similarity index 100% rename from x-pack/plugins/alerting/server/task_runner/index.ts rename to x-pack/plugins/alerts/server/task_runner/index.ts diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts similarity index 99% rename from x-pack/plugins/alerting/server/task_runner/task_runner.test.ts rename to x-pack/plugins/alerts/server/task_runner/task_runner.test.ts index 98824b8cf4e1a..983dff86d5602 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts @@ -7,10 +7,10 @@ import sinon from 'sinon'; import { schema } from '@kbn/config-schema'; import { AlertExecutorOptions } from '../types'; -import { ConcreteTaskInstance, TaskStatus } from '../../../../plugins/task_manager/server'; +import { ConcreteTaskInstance, TaskStatus } from '../../../task_manager/server'; import { TaskRunnerContext } from './task_runner_factory'; import { TaskRunner } from './task_runner'; -import { encryptedSavedObjectsMock } from '../../../../plugins/encrypted_saved_objects/server/mocks'; +import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; import { loggingServiceMock } from '../../../../../src/core/server/mocks'; import { PluginStartContract as ActionsPluginStart } from '../../../actions/server'; import { actionsMock } from '../../../actions/server/mocks'; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts similarity index 99% rename from x-pack/plugins/alerting/server/task_runner/task_runner.ts rename to x-pack/plugins/alerts/server/task_runner/task_runner.ts index 0831163d1d326..be399893088e3 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -7,7 +7,7 @@ import { pick, mapValues, omit, without } from 'lodash'; import { Logger, SavedObject, KibanaRequest } from '../../../../../src/core/server'; import { TaskRunnerContext } from './task_runner_factory'; -import { ConcreteTaskInstance } from '../../../../plugins/task_manager/server'; +import { ConcreteTaskInstance } from '../../../task_manager/server'; import { createExecutionHandler } from './create_execution_handler'; import { AlertInstance, createAlertInstanceFactory } from '../alert_instance'; import { getNextRunAt } from './get_next_run_at'; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts similarity index 93% rename from x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts rename to x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts index c1318bac48dfb..7d9710d8a3e08 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts @@ -5,9 +5,9 @@ */ import sinon from 'sinon'; -import { ConcreteTaskInstance, TaskStatus } from '../../../../plugins/task_manager/server'; +import { ConcreteTaskInstance, TaskStatus } from '../../../task_manager/server'; import { TaskRunnerContext, TaskRunnerFactory } from './task_runner_factory'; -import { encryptedSavedObjectsMock } from '../../../../plugins/encrypted_saved_objects/server/mocks'; +import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; import { loggingServiceMock } from '../../../../../src/core/server/mocks'; import { actionsMock } from '../../../actions/server/mocks'; import { alertsMock } from '../mocks'; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts similarity index 87% rename from x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts rename to x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts index c50e288d2e520..ca762cf2b2105 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import { Logger } from '../../../../../src/core/server'; -import { RunContext } from '../../../../plugins/task_manager/server'; -import { EncryptedSavedObjectsClient } from '../../../../plugins/encrypted_saved_objects/server'; -import { PluginStartContract as ActionsPluginStartContract } from '../../../../plugins/actions/server'; +import { RunContext } from '../../../task_manager/server'; +import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; +import { PluginStartContract as ActionsPluginStartContract } from '../../../actions/server'; import { AlertType, GetBasePathFunction, diff --git a/x-pack/plugins/alerting/server/task_runner/transform_action_params.test.ts b/x-pack/plugins/alerts/server/task_runner/transform_action_params.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/task_runner/transform_action_params.test.ts rename to x-pack/plugins/alerts/server/task_runner/transform_action_params.test.ts diff --git a/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts b/x-pack/plugins/alerts/server/task_runner/transform_action_params.ts similarity index 100% rename from x-pack/plugins/alerting/server/task_runner/transform_action_params.ts rename to x-pack/plugins/alerts/server/task_runner/transform_action_params.ts diff --git a/x-pack/plugins/alerting/server/test_utils/index.ts b/x-pack/plugins/alerts/server/test_utils/index.ts similarity index 100% rename from x-pack/plugins/alerting/server/test_utils/index.ts rename to x-pack/plugins/alerts/server/test_utils/index.ts diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerts/server/types.ts similarity index 100% rename from x-pack/plugins/alerting/server/types.ts rename to x-pack/plugins/alerts/server/types.ts diff --git a/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts b/x-pack/plugins/alerts/server/usage/alerts_telemetry.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts rename to x-pack/plugins/alerts/server/usage/alerts_telemetry.test.ts diff --git a/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts b/x-pack/plugins/alerts/server/usage/alerts_telemetry.ts similarity index 100% rename from x-pack/plugins/alerting/server/usage/alerts_telemetry.ts rename to x-pack/plugins/alerts/server/usage/alerts_telemetry.ts diff --git a/x-pack/plugins/alerting/server/usage/alerts_usage_collector.test.ts b/x-pack/plugins/alerts/server/usage/alerts_usage_collector.test.ts similarity index 100% rename from x-pack/plugins/alerting/server/usage/alerts_usage_collector.test.ts rename to x-pack/plugins/alerts/server/usage/alerts_usage_collector.test.ts diff --git a/x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts b/x-pack/plugins/alerts/server/usage/alerts_usage_collector.ts similarity index 100% rename from x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts rename to x-pack/plugins/alerts/server/usage/alerts_usage_collector.ts diff --git a/x-pack/plugins/alerting/server/usage/index.ts b/x-pack/plugins/alerts/server/usage/index.ts similarity index 100% rename from x-pack/plugins/alerting/server/usage/index.ts rename to x-pack/plugins/alerts/server/usage/index.ts diff --git a/x-pack/plugins/alerting/server/usage/task.ts b/x-pack/plugins/alerts/server/usage/task.ts similarity index 100% rename from x-pack/plugins/alerting/server/usage/task.ts rename to x-pack/plugins/alerts/server/usage/task.ts diff --git a/x-pack/plugins/alerting/server/usage/types.ts b/x-pack/plugins/alerts/server/usage/types.ts similarity index 100% rename from x-pack/plugins/alerting/server/usage/types.ts rename to x-pack/plugins/alerts/server/usage/types.ts diff --git a/x-pack/plugins/apm/dev_docs/vscode_setup.md b/x-pack/plugins/apm/dev_docs/vscode_setup.md index 1c80d1476520d..c7adad4fd0942 100644 --- a/x-pack/plugins/apm/dev_docs/vscode_setup.md +++ b/x-pack/plugins/apm/dev_docs/vscode_setup.md @@ -1,8 +1,8 @@ -### Visual Studio Code +# Visual Studio Code When using [Visual Studio Code](https://code.visualstudio.com/) with APM it's best to set up a [multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces) and add the `x-pack/plugins/apm` directory, the `x-pack` directory, and the root of the Kibana repository to the workspace. This makes it so you can navigate and search within APM and use the wider workspace roots when you need to widen your search. -#### Using the Jest extension +## Using the Jest extension The [vscode-jest extension](https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest) is a good way to run your Jest tests inside the editor. @@ -22,31 +22,21 @@ If you have a workspace configured as described above you should have: "jest.disabledWorkspaceFolders": ["kibana", "x-pack"] ``` -in your Workspace settings, and: - -```json -"jest.pathToJest": "node scripts/jest.js --testPathPattern=plugins/apm", -"jest.rootPath": "../../.." -``` - -in the settings for the APM folder. - -#### Jest debugging +## Jest debugging To make the [VSCode debugger](https://vscode.readthedocs.io/en/latest/editor/debugging/) work with Jest (you can set breakpoints in the code and tests and use the VSCode debugger) you'll need the [Node Debug extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.node-debug2) installed and can set up a launch configuration like: ```json { "type": "node", - "name": "APM Jest", + "name": "vscode-jest-tests", "request": "launch", - "args": ["--runInBand", "--testPathPattern=plugins/apm"], - "cwd": "${workspaceFolder}/../../..", - "console": "internalConsole", - "internalConsoleOptions": "openOnSessionStart", + "args": ["--runInBand"], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", "disableOptimisticBPs": true, - "program": "${workspaceFolder}/../../../scripts/jest.js", - "runtimeVersion": "10.15.2" + "program": "${workspaceFolder}/../../../node_modules/jest/bin/jest" } ``` diff --git a/x-pack/plugins/apm/e2e/.gitignore b/x-pack/plugins/apm/e2e/.gitignore index 9eb738ede51e3..5042f0bca0300 100644 --- a/x-pack/plugins/apm/e2e/.gitignore +++ b/x-pack/plugins/apm/e2e/.gitignore @@ -1,4 +1,5 @@ cypress/screenshots/* -cypress/videos/* cypress/test-results +cypress/videos/* +/snapshots.js tmp diff --git a/x-pack/plugins/apm/e2e/run-e2e.sh b/x-pack/plugins/apm/e2e/run-e2e.sh index 157c42cc7e4ee..ae764d171c45c 100755 --- a/x-pack/plugins/apm/e2e/run-e2e.sh +++ b/x-pack/plugins/apm/e2e/run-e2e.sh @@ -26,20 +26,23 @@ cd ${E2E_DIR} # # Ask user to start Kibana ################################################## -echo "\n${bold}To start Kibana please run the following command:${normal} +echo "" # newline +echo "${bold}To start Kibana please run the following command:${normal} node ./scripts/kibana --no-base-path --dev --no-dev-config --config x-pack/plugins/apm/e2e/ci/kibana.e2e.yml" # # Create tmp folder ################################################## -echo "\n${bold}Temporary folder${normal}" -echo "Temporary files will be stored in: ${TMP_DIR}" +echo "" # newline +echo "${bold}Temporary folder${normal}" +echo "Temporary files will be stored in: ${E2E_DIR}${TMP_DIR}" mkdir -p ${TMP_DIR} # # apm-integration-testing ################################################## -printf "\n${bold}apm-integration-testing (logs: ${TMP_DIR}/apm-it.log)\n${normal}" +echo "" # newline +echo "${bold}apm-integration-testing (logs: ${E2E_DIR}${TMP_DIR}/apm-it.log)${normal}" # pull if folder already exists if [ -d ${APM_IT_DIR} ]; then @@ -54,7 +57,7 @@ fi # Stop if clone/pull failed if [ $? -ne 0 ]; then - printf "\n⚠️ Initializing apm-integration-testing failed. \n" + echo "⚠️ Initializing apm-integration-testing failed." exit 1 fi @@ -71,23 +74,34 @@ ${APM_IT_DIR}/scripts/compose.py start master \ # Stop if apm-integration-testing failed to start correctly if [ $? -ne 0 ]; then - printf "⚠️ apm-integration-testing could not be started.\n" - printf "Please see the logs in ${TMP_DIR}/apm-it.log\n\n" - printf "As a last resort, reset docker with:\n\n cd ${APM_IT_DIR} && scripts/compose.py stop && docker system prune --all --force --volumes\n" + echo "⚠️ apm-integration-testing could not be started" + echo "" # newline + echo "As a last resort, reset docker with:" + echo "" # newline + echo "cd ${E2E_DIR}${APM_IT_DIR} && scripts/compose.py stop && docker system prune --all --force --volumes" + echo "" # newline + + # output logs for excited docker containers + cd ${APM_IT_DIR} && docker-compose ps --filter "status=exited" -q | xargs -L1 docker logs --tail=10 && cd - + + echo "" # newline + echo "Find the full logs in ${E2E_DIR}${TMP_DIR}/apm-it.log" exit 1 fi # # Cypress ################################################## -echo "\n${bold}Cypress (logs: ${TMP_DIR}/e2e-yarn.log)${normal}" +echo "" # newline +echo "${bold}Cypress (logs: ${E2E_DIR}${TMP_DIR}/e2e-yarn.log)${normal}" echo "Installing cypress dependencies " yarn &> ${TMP_DIR}/e2e-yarn.log # # Static mock data ################################################## -printf "\n${bold}Static mock data (logs: ${TMP_DIR}/ingest-data.log)\n${normal}" +echo "" # newline +echo "${bold}Static mock data (logs: ${E2E_DIR}${TMP_DIR}/ingest-data.log)${normal}" # Download static data if not already done if [ ! -e "${TMP_DIR}/events.json" ]; then @@ -102,16 +116,32 @@ curl --silent --user admin:changeme -XDELETE "localhost:${ELASTICSEARCH_PORT}/ap # Ingest data into APM Server node ingest-data/replay.js --server-url http://localhost:$APM_SERVER_PORT --events ${TMP_DIR}/events.json 2>> ${TMP_DIR}/ingest-data.log -# Stop if not all events were ingested correctly +# Abort if not all events were ingested correctly if [ $? -ne 0 ]; then - printf "\n⚠️ Not all events were ingested correctly. This might affect test tests. \n" + echo "⚠️ Not all events were ingested correctly. This might affect test tests." + echo "Aborting. Please try again." + echo "" # newline + echo "Full logs in ${E2E_DIR}${TMP_DIR}/ingest-data.log:" + + # output logs for excited docker containers + cd ${APM_IT_DIR} && docker-compose ps --filter "status=exited" -q | xargs -L1 docker logs --tail=3 && cd - + + # stop docker containers + cd ${APM_IT_DIR} && ./scripts/compose.py stop > /dev/null && cd - exit 1 fi +# create empty snapshot file if it doesn't exist +SNAPSHOTS_FILE=cypress/integration/snapshots.js +if [ ! -f ${SNAPSHOTS_FILE} ]; then + echo "{}" > ${SNAPSHOTS_FILE} +fi + # # Wait for Kibana to start ################################################## -echo "\n${bold}Waiting for Kibana to start...${normal}" +echo "" # newline +echo "${bold}Waiting for Kibana to start...${normal}" echo "Note: you need to start Kibana manually. Find the instructions at the top." yarn wait-on -i 500 -w 500 http-get://admin:changeme@localhost:$KIBANA_PORT/api/status > /dev/null @@ -119,12 +149,13 @@ yarn wait-on -i 500 -w 500 http-get://admin:changeme@localhost:$KIBANA_PORT/api/ ## See: https://github.com/elastic/kibana/issues/66326 if [ -e kibana.log ] ; then grep -m 1 "http server running" <(tail -f -n +1 kibana.log) - echo "\n✅ Kibana server running...\n" + echo "✅ Kibana server running..." grep -m 1 "bundles compiled successfully" <(tail -f -n +1 kibana.log) - echo "\n✅ Kibana bundles have been compiled...\n" + echo "✅ Kibana bundles have been compiled..." fi -echo "\n✅ Setup completed successfully. Running tests...\n" + +echo "✅ Setup completed successfully. Running tests..." # # run cypress tests @@ -134,9 +165,6 @@ yarn cypress run --config pageLoadTimeout=100000,watchForFileChanges=true # # Run interactively ################################################## -echo " - -${bold}If you want to run the test interactively, run:${normal} - -yarn cypress open --config pageLoadTimeout=100000,watchForFileChanges=true -" +echo "${bold}If you want to run the test interactively, run:${normal}" +echo "" # newline +echo "cd ${E2E_DIR} && yarn cypress open --config pageLoadTimeout=100000,watchForFileChanges=true" diff --git a/x-pack/plugins/apm/jest.config.js b/x-pack/plugins/apm/jest.config.js new file mode 100644 index 0000000000000..c3ae694fe8e14 --- /dev/null +++ b/x-pack/plugins/apm/jest.config.js @@ -0,0 +1,44 @@ +/* + * 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. + */ + +// This is an APM-specific Jest configuration which overrides the x-pack +// configuration. It's intended for use in development and does not run in CI, +// which runs the entire x-pack suite. Run `npx jest`. + +require('../../../src/setup_node_env'); + +const { createJestConfig } = require('../../dev-tools/jest/create_jest_config'); +const { resolve } = require('path'); + +const rootDir = resolve(__dirname, '.'); +const xPackKibanaDirectory = resolve(__dirname, '../..'); +const kibanaDirectory = resolve(__dirname, '../../..'); + +const jestConfig = createJestConfig({ + kibanaDirectory, + rootDir, + xPackKibanaDirectory, +}); + +module.exports = { + ...jestConfig, + reporters: ['default'], + roots: [`${rootDir}/common`, `${rootDir}/public`, `${rootDir}/server`], + collectCoverage: true, + collectCoverageFrom: [ + '**/*.{js,jsx,ts,tsx}', + '!**/{__test__,__snapshots__,__examples__,integration_tests,tests}/**', + '!**/*.test.{js,ts,tsx}', + '!**/dev_docs/**', + '!**/e2e/**', + '!**/scripts/**', + '!**/target/**', + '!**/typings/**', + '!**/mocks/**', + ], + coverageDirectory: `${rootDir}/target/coverage/jest`, + coverageReporters: ['html'], +}; diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json index dd89fac66f6e8..2de3c9c97065d 100644 --- a/x-pack/plugins/apm/kibana.json +++ b/x-pack/plugins/apm/kibana.json @@ -15,7 +15,7 @@ "usageCollection", "taskManager", "actions", - "alerting", + "alerts", "observability", "security" ], diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx index c3d426a6275a7..0dbde5ea86a18 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx @@ -28,7 +28,7 @@ export function ServiceDetails({ tab }: Props) { const canSaveAlerts = !!plugin.core.application.capabilities.apm[ 'alerting:save' ]; - const isAlertingPluginEnabled = 'alerting' in plugin.plugins; + const isAlertingPluginEnabled = 'alerts' in plugin.plugins; const isAlertingAvailable = isAlertingPluginEnabled && (canReadAlerts || canSaveAlerts); diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx index dc4413ee98360..988edb197a230 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx @@ -105,10 +105,11 @@ export const TransactionActionMenu: FunctionComponent = ({ if (app === 'uptime' || app === 'metrics' || app === 'logs') { event.preventDefault(); + const search = parsed.search || ''; + + const path = `${rest.join('/')}${search}`; core.application.navigateToApp(app, { - path: `${rest.join('/')}${ - parsed.search ? `&${parsed.search}` : '' - }`, + path, }); } }, diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx index 718f81b3c1027..bad9292f3e768 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx @@ -6,6 +6,7 @@ import React from 'react'; import { render, fireEvent, act } from '@testing-library/react'; +import { merge, tail } from 'lodash'; import { TransactionActionMenu } from '../TransactionActionMenu'; import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import * as Transactions from './mockData'; @@ -16,13 +17,43 @@ import { import * as hooks from '../../../../hooks/useFetcher'; import { LicenseContext } from '../../../../context/LicenseContext'; import { License } from '../../../../../../licensing/common/license'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; +import { + MockApmPluginContextWrapper, + mockApmPluginContextValue, +} from '../../../../context/ApmPluginContext/MockApmPluginContext'; import * as apmApi from '../../../../services/rest/createCallApmApi'; +import { ApmPluginContextValue } from '../../../../context/ApmPluginContext'; + +const getMock = () => { + return (merge({}, mockApmPluginContextValue, { + core: { + application: { + navigateToApp: jest.fn(), + }, + http: { + basePath: { + remove: jest.fn((path: string) => { + return tail(path.split('/')).join('/'); + }), + }, + }, + }, + }) as unknown) as ApmPluginContextValue; +}; -const renderTransaction = async (transaction: Record) => { +const renderTransaction = async ( + transaction: Record, + mock: ApmPluginContextValue = getMock() +) => { const rendered = render( , - { wrapper: MockApmPluginContextWrapper } + { + wrapper: ({ children }: { children?: React.ReactNode }) => ( + + {children} + + ), + } ); fireEvent.click(rendered.getByText('Actions')); @@ -49,11 +80,21 @@ describe('TransactionActionMenu component', () => { }); it('should always render the trace logs link', async () => { - const { queryByText } = await renderTransaction( - Transactions.transactionWithMinimalData + const mock = getMock(); + + const { queryByText, getByText } = await renderTransaction( + Transactions.transactionWithMinimalData, + mock ); expect(queryByText('Trace logs')).not.toBeNull(); + + fireEvent.click(getByText('Trace logs')); + + expect(mock.core.application.navigateToApp).toHaveBeenCalledWith('logs', { + path: + 'link-to/logs?time=1545092070952&filter=trace.id:%228b60bd32ecc6e1506735a8b6cfcf175c%22%20OR%208b60bd32ecc6e1506735a8b6cfcf175c', + }); }); it('should not render the pod links when there is no pod id', async () => { @@ -66,12 +107,33 @@ describe('TransactionActionMenu component', () => { }); it('should render the pod links when there is a pod id', async () => { - const { queryByText } = await renderTransaction( - Transactions.transactionWithKubernetesData + const mock = getMock(); + + const { queryByText, getByText } = await renderTransaction( + Transactions.transactionWithKubernetesData, + mock ); expect(queryByText('Pod logs')).not.toBeNull(); expect(queryByText('Pod metrics')).not.toBeNull(); + + fireEvent.click(getByText('Pod logs')); + + expect(mock.core.application.navigateToApp).toHaveBeenCalledWith('logs', { + path: 'link-to/pod-logs/pod123456abcdef?time=1545092070952', + }); + + (mock.core.application.navigateToApp as jest.Mock).mockClear(); + + fireEvent.click(getByText('Pod metrics')); + + expect(mock.core.application.navigateToApp).toHaveBeenCalledWith( + 'metrics', + { + path: + 'link-to/pod-detail/pod123456abcdef?from=1545091770952&to=1545092370952', + } + ); }); it('should not render the container links when there is no container id', async () => { @@ -84,12 +146,33 @@ describe('TransactionActionMenu component', () => { }); it('should render the container links when there is a container id', async () => { - const { queryByText } = await renderTransaction( - Transactions.transactionWithContainerData + const mock = getMock(); + + const { queryByText, getByText } = await renderTransaction( + Transactions.transactionWithContainerData, + mock ); expect(queryByText('Container logs')).not.toBeNull(); expect(queryByText('Container metrics')).not.toBeNull(); + + fireEvent.click(getByText('Container logs')); + + expect(mock.core.application.navigateToApp).toHaveBeenCalledWith('logs', { + path: 'link-to/container-logs/container123456abcdef?time=1545092070952', + }); + + (mock.core.application.navigateToApp as jest.Mock).mockClear(); + + fireEvent.click(getByText('Container metrics')); + + expect(mock.core.application.navigateToApp).toHaveBeenCalledWith( + 'metrics', + { + path: + 'link-to/container-detail/container123456abcdef?from=1545091770952&to=1545092370952', + } + ); }); it('should not render the host links when there is no hostname', async () => { @@ -102,12 +185,32 @@ describe('TransactionActionMenu component', () => { }); it('should render the host links when there is a hostname', async () => { - const { queryByText } = await renderTransaction( - Transactions.transactionWithHostData + const mock = getMock(); + const { queryByText, getByText } = await renderTransaction( + Transactions.transactionWithHostData, + mock ); expect(queryByText('Host logs')).not.toBeNull(); expect(queryByText('Host metrics')).not.toBeNull(); + + fireEvent.click(getByText('Host logs')); + + expect(mock.core.application.navigateToApp).toHaveBeenCalledWith('logs', { + path: 'link-to/host-logs/227453131a17?time=1545092070952', + }); + + (mock.core.application.navigateToApp as jest.Mock).mockClear(); + + fireEvent.click(getByText('Host metrics')); + + expect(mock.core.application.navigateToApp).toHaveBeenCalledWith( + 'metrics', + { + path: + 'link-to/host-detail/227453131a17?from=1545091770952&to=1545092370952', + } + ); }); it('should not render the uptime link if there is no url available', async () => { @@ -127,11 +230,20 @@ describe('TransactionActionMenu component', () => { }); it('should render the uptime link if there is a url with a domain', async () => { - const { queryByText } = await renderTransaction( - Transactions.transactionWithUrlAndDomain + const mock = getMock(); + + const { queryByText, getByText } = await renderTransaction( + Transactions.transactionWithUrlAndDomain, + mock ); expect(queryByText('Status')).not.toBeNull(); + + fireEvent.click(getByText('Status')); + + expect(mock.core.application.navigateToApp).toHaveBeenCalledWith('uptime', { + path: '?search=url.domain:%22example.com%22', + }); }); it('should match the snapshot', async () => { diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts index 60cedfde24258..7f99939a0a0d0 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts @@ -62,7 +62,7 @@ export const getSections = ({ const uptimeLink = url.format({ pathname: basePath.prepend('/app/uptime'), - hash: `/?${fromQuery( + search: `?${fromQuery( pick( { dateRangeStart: urlParams.rangeFrom, diff --git a/x-pack/plugins/apm/public/context/LoadingIndicatorContext.tsx b/x-pack/plugins/apm/public/context/LoadingIndicatorContext.tsx index c1776d7437e05..32e52f8e396b5 100644 --- a/x-pack/plugins/apm/public/context/LoadingIndicatorContext.tsx +++ b/x-pack/plugins/apm/public/context/LoadingIndicatorContext.tsx @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ import { EuiPortal, EuiProgress } from '@elastic/eui'; +import { pick } from 'lodash'; import React, { Fragment, useMemo, useReducer } from 'react'; import { useDelayedVisibility } from '../components/shared/useDelayedVisibility'; export const LoadingIndicatorContext = React.createContext({ statuses: {}, - dispatchStatus: (action: Action) => undefined as void, + dispatchStatus: (action: Action) => {}, }); interface State { @@ -22,14 +23,13 @@ interface Action { } function reducer(statuses: State, action: Action) { - // add loading status - if (action.isLoading) { - return { ...statuses, [action.id]: true }; - } - - // remove loading status - const { [action.id]: statusToRemove, ...restStatuses } = statuses; - return restStatuses; + // Return an object with only the ids with `true` as their value, so that ids + // that previously had `false` are removed and do not remain hanging around in + // the object. + return pick( + { ...statuses, [action.id.toString()]: action.isLoading }, + Boolean + ); } function getIsAnyLoading(statuses: State) { diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index e732e695b36b1..76320efe617ea 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -17,7 +17,7 @@ import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { PluginSetupContract as AlertingPluginPublicSetup, PluginStartContract as AlertingPluginPublicStart, -} from '../../alerting/public'; +} from '../../alerts/public'; import { FeaturesPluginSetup } from '../../features/public'; import { DataPublicPluginSetup, @@ -44,7 +44,7 @@ export type ApmPluginSetup = void; export type ApmPluginStart = void; export interface ApmPluginSetupDeps { - alerting?: AlertingPluginPublicSetup; + alerts?: AlertingPluginPublicSetup; data: DataPublicPluginSetup; features: FeaturesPluginSetup; home: HomePublicPluginSetup; @@ -53,7 +53,7 @@ export interface ApmPluginSetupDeps { } export interface ApmPluginStartDeps { - alerting?: AlertingPluginPublicStart; + alerts?: AlertingPluginPublicStart; data: DataPublicPluginStart; home: void; licensing: void; diff --git a/x-pack/plugins/apm/readme.md b/x-pack/plugins/apm/readme.md index 62465e920d793..ceed5e6c39716 100644 --- a/x-pack/plugins/apm/readme.md +++ b/x-pack/plugins/apm/readme.md @@ -39,18 +39,26 @@ _Starts Kibana (:5701), APM Server (:8201) and Elasticsearch (:9201). Ingests sa ### Unit testing -Note: Run the following commands from `kibana/x-pack`. +Note: Run the following commands from `kibana/x-pack/plugins/apm`. #### Run unit tests ``` -node scripts/jest.js plugins/apm --watch +npx jest --watch ``` #### Update snapshots ``` -node scripts/jest.js plugins/apm --updateSnapshot +npx jest --updateSnapshot +``` + +#### Coverage + +HTML coverage report can be found in target/coverage/jest after tests have run. + +``` +open target/coverage/jest/index.html ``` ### Functional tests diff --git a/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts b/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts index 8af9f386ecebf..4b8e9cf937a2b 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts @@ -5,25 +5,25 @@ */ import { Observable } from 'rxjs'; -import { AlertingPlugin } from '../../../../alerting/server'; +import { AlertingPlugin } from '../../../../alerts/server'; import { ActionsPlugin } from '../../../../actions/server'; import { registerTransactionDurationAlertType } from './register_transaction_duration_alert_type'; import { registerErrorRateAlertType } from './register_error_rate_alert_type'; import { APMConfig } from '../..'; interface Params { - alerting: AlertingPlugin['setup']; + alerts: AlertingPlugin['setup']; actions: ActionsPlugin['setup']; config$: Observable; } export function registerApmAlerts(params: Params) { registerTransactionDurationAlertType({ - alerting: params.alerting, + alerts: params.alerts, config$: params.config$, }); registerErrorRateAlertType({ - alerting: params.alerting, + alerts: params.alerts, config$: params.config$, }); } diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_rate_alert_type.ts index ee7bd9eeb4b6f..53843b7f7412b 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_rate_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_rate_alert_type.ts @@ -19,12 +19,12 @@ import { SERVICE_NAME, SERVICE_ENVIRONMENT, } from '../../../common/elasticsearch_fieldnames'; -import { AlertingPlugin } from '../../../../alerting/server'; +import { AlertingPlugin } from '../../../../alerts/server'; import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { APMConfig } from '../..'; interface RegisterAlertParams { - alerting: AlertingPlugin['setup']; + alerts: AlertingPlugin['setup']; config$: Observable; } @@ -39,10 +39,10 @@ const paramsSchema = schema.object({ const alertTypeConfig = ALERT_TYPES_CONFIG[AlertType.ErrorRate]; export function registerErrorRateAlertType({ - alerting, + alerts, config$, }: RegisterAlertParams) { - alerting.registerType({ + alerts.registerType({ id: AlertType.ErrorRate, name: alertTypeConfig.name, actionGroups: alertTypeConfig.actionGroups, diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts index afb402200a07b..1fd1aef4c8b70 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts @@ -18,12 +18,12 @@ import { TRANSACTION_DURATION, SERVICE_ENVIRONMENT, } from '../../../common/elasticsearch_fieldnames'; -import { AlertingPlugin } from '../../../../alerting/server'; +import { AlertingPlugin } from '../../../../alerts/server'; import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { APMConfig } from '../..'; interface RegisterAlertParams { - alerting: AlertingPlugin['setup']; + alerts: AlertingPlugin['setup']; config$: Observable; } @@ -44,10 +44,10 @@ const paramsSchema = schema.object({ const alertTypeConfig = ALERT_TYPES_CONFIG[AlertType.TransactionDuration]; export function registerTransactionDurationAlertType({ - alerting, + alerts, config$, }: RegisterAlertParams) { - alerting.registerType({ + alerts.registerType({ id: AlertType.TransactionDuration, name: alertTypeConfig.name, actionGroups: alertTypeConfig.actionGroups, diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index b9ad14f7ec47d..d32d16d4c3cc8 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -17,7 +17,7 @@ import { ObservabilityPluginSetup } from '../../observability/server'; import { SecurityPluginSetup } from '../../security/public'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; import { TaskManagerSetupContract } from '../../task_manager/server'; -import { AlertingPlugin } from '../../alerting/server'; +import { AlertingPlugin } from '../../alerts/server'; import { ActionsPlugin } from '../../actions/server'; import { APMOSSPluginSetup } from '../../../../src/plugins/apm_oss/server'; import { createApmAgentConfigurationIndex } from './lib/settings/agent_configuration/create_agent_config_index'; @@ -57,7 +57,7 @@ export class APMPlugin implements Plugin { cloud?: CloudSetup; usageCollection?: UsageCollectionSetup; taskManager?: TaskManagerSetupContract; - alerting?: AlertingPlugin['setup']; + alerts?: AlertingPlugin['setup']; actions?: ActionsPlugin['setup']; observability?: ObservabilityPluginSetup; features: FeaturesPluginSetup; @@ -73,9 +73,9 @@ export class APMPlugin implements Plugin { core.savedObjects.registerType(apmIndices); core.savedObjects.registerType(apmTelemetry); - if (plugins.actions && plugins.alerting) { + if (plugins.actions && plugins.alerts) { registerApmAlerts({ - alerting: plugins.alerting, + alerts: plugins.alerts, actions: plugins.actions, config$: mergedConfig$, }); 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/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/index_lifecycle_management/public/application/services/api.js b/x-pack/plugins/index_lifecycle_management/public/application/services/api.js index 386df63111a89..6b46d6e6ea735 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/api.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/api.js @@ -28,8 +28,7 @@ export async function loadIndexTemplates() { } export async function loadPolicies(withIndices) { - const query = withIndices ? '?withIndices=true' : ''; - return await sendGet('policies', query); + return await sendGet('policies', { withIndices }); } export async function savePolicy(policy) { diff --git a/x-pack/plugins/index_management/__jest__/client_integration/template_create.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/template_create.test.tsx index 05abe284fab32..8f464987418c0 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/template_create.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/template_create.test.tsx @@ -59,7 +59,8 @@ const KEYWORD_MAPPING_FIELD = { type: 'keyword', }; -describe('', () => { +// FLAKY: https://github.com/elastic/kibana/issues/67833 +describe.skip('', () => { let testBed: TemplateFormTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); diff --git a/x-pack/plugins/index_management/server/client/elasticsearch.ts b/x-pack/plugins/index_management/server/client/elasticsearch.ts new file mode 100644 index 0000000000000..65bd5411a249b --- /dev/null +++ b/x-pack/plugins/index_management/server/client/elasticsearch.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. + */ + +export const elasticsearchJsPlugin = (Client: any, config: any, components: any) => { + const ca = components.clientAction.factory; + + Client.prototype.dataManagement = components.clientAction.namespaceFactory(); + const dataManagement = Client.prototype.dataManagement.prototype; + + dataManagement.getComponentTemplates = ca({ + urls: [ + { + fmt: '/_component_template', + }, + ], + method: 'GET', + }); + + dataManagement.getComponentTemplate = ca({ + urls: [ + { + fmt: '/_component_template/<%=name%>', + req: { + name: { + type: 'string', + }, + }, + }, + ], + method: 'GET', + }); + + dataManagement.saveComponentTemplate = ca({ + urls: [ + { + fmt: '/_component_template/<%=name%>', + req: { + name: { + type: 'string', + }, + }, + }, + ], + method: 'PUT', + }); + + dataManagement.deleteComponentTemplate = ca({ + urls: [ + { + fmt: '/_component_template/<%=name%>', + req: { + name: { + type: 'string', + }, + }, + }, + ], + method: 'DELETE', + }); +}; diff --git a/x-pack/plugins/index_management/server/plugin.ts b/x-pack/plugins/index_management/server/plugin.ts index e5bd7451b028f..f254333007c39 100644 --- a/x-pack/plugins/index_management/server/plugin.ts +++ b/x-pack/plugins/index_management/server/plugin.ts @@ -3,14 +3,33 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +declare module 'kibana/server' { + interface RequestHandlerContext { + dataManagement?: DataManagementContext; + } +} + import { i18n } from '@kbn/i18n'; -import { CoreSetup, Plugin, Logger, PluginInitializerContext } from 'src/core/server'; +import { + CoreSetup, + Plugin, + Logger, + PluginInitializerContext, + IScopedClusterClient, + ICustomClusterClient, +} from 'src/core/server'; import { PLUGIN } from '../common'; import { Dependencies } from './types'; import { ApiRoutes } from './routes'; import { License, IndexDataEnricher } from './services'; import { isEsError } from './lib/is_es_error'; +import { elasticsearchJsPlugin } from './client/elasticsearch'; + +export interface DataManagementContext { + client: IScopedClusterClient; +} export interface IndexManagementPluginSetup { indexDataEnricher: { @@ -18,11 +37,18 @@ export interface IndexManagementPluginSetup { }; } +async function getCustomEsClient(getStartServices: CoreSetup['getStartServices']) { + const [core] = await getStartServices(); + const esClientConfig = { plugins: [elasticsearchJsPlugin] }; + return core.elasticsearch.legacy.createClient('dataManagement', esClientConfig); +} + export class IndexMgmtServerPlugin implements Plugin { private readonly apiRoutes: ApiRoutes; private readonly license: License; private readonly logger: Logger; private readonly indexDataEnricher: IndexDataEnricher; + private dataManagementESClient?: ICustomClusterClient; constructor(initContext: PluginInitializerContext) { this.logger = initContext.logger.get(); @@ -31,7 +57,10 @@ export class IndexMgmtServerPlugin implements Plugin { + this.dataManagementESClient = + this.dataManagementESClient ?? (await getCustomEsClient(getStartServices)); + + return { + client: this.dataManagementESClient.asScoped(request), + }; + }); + this.apiRoutes.setup({ router, license: this.license, @@ -65,5 +103,10 @@ export class IndexMgmtServerPlugin implements Plugin { + router.post( + { + path: addBasePath('/component_templates'), + validate: { + body: bodySchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.dataManagement!.client; + + const { name, ...componentTemplateDefinition } = req.body; + + try { + // Check that a component template with the same name doesn't already exist + const componentTemplateResponse = await callAsCurrentUser( + 'dataManagement.getComponentTemplate', + { name } + ); + + const { component_templates: componentTemplates } = componentTemplateResponse; + + if (componentTemplates.length) { + return res.conflict({ + body: new Error( + i18n.translate('xpack.idxMgmt.componentTemplates.createRoute.duplicateErrorMessage', { + defaultMessage: "There is already a component template with name '{name}'.", + values: { + name, + }, + }) + ), + }); + } + } catch (e) { + // Silently swallow error + } + + try { + const response = await callAsCurrentUser('dataManagement.saveComponentTemplate', { + name, + body: componentTemplateDefinition, + }); + + return res.ok({ body: response }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); +}; diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/delete.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/delete.ts new file mode 100644 index 0000000000000..9e11967202b9c --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/delete.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../index'; + +const paramsSchema = schema.object({ + names: schema.string(), +}); + +export const registerDeleteRoute = ({ router, license }: RouteDependencies): void => { + router.delete( + { + path: addBasePath('/component_templates/{names}'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.dataManagement!.client; + const { names } = req.params; + const componentNames = names.split(','); + + const response: { itemsDeleted: string[]; errors: any[] } = { + itemsDeleted: [], + errors: [], + }; + + await Promise.all( + componentNames.map((componentName) => { + return callAsCurrentUser('dataManagement.deleteComponentTemplate', { + name: componentName, + }) + .then(() => response.itemsDeleted.push(componentName)) + .catch((e) => + response.errors.push({ + name: componentName, + error: e, + }) + ); + }) + ); + + return res.ok({ body: response }); + }) + ); +}; diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts new file mode 100644 index 0000000000000..87aa64421624e --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../index'; + +const paramsSchema = schema.object({ + name: schema.string(), +}); + +export function registerGetAllRoute({ router, license, lib: { isEsError } }: RouteDependencies) { + // Get all component templates + router.get( + { path: addBasePath('/component_templates'), validate: false }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.dataManagement!.client; + + try { + const response = await callAsCurrentUser('dataManagement.getComponentTemplates'); + + return res.ok({ body: response.component_templates }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); + + // Get single component template + router.get( + { + path: addBasePath('/component_templates/{name}'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.dataManagement!.client; + const { name } = req.params; + + try { + const { component_templates: componentTemplates } = await callAsCurrentUser( + 'dataManagement.getComponentTemplates', + { + name, + } + ); + + return res.ok({ + body: { + ...componentTemplates[0], + name, + }, + }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); +} diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/index.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/index.ts new file mode 100644 index 0000000000000..7ecb71182e87e --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/index.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 { RouteDependencies } from '../../../types'; + +import { registerGetAllRoute } from './get'; +import { registerCreateRoute } from './create'; +import { registerUpdateRoute } from './update'; +import { registerDeleteRoute } from './delete'; + +export function registerComponentTemplateRoutes(dependencies: RouteDependencies) { + registerGetAllRoute(dependencies); + registerCreateRoute(dependencies); + registerUpdateRoute(dependencies); + registerDeleteRoute(dependencies); +} diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts new file mode 100644 index 0000000000000..7d32637c6b977 --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; + +export const componentTemplateSchema = { + template: schema.object({ + settings: schema.maybe(schema.object({}, { unknowns: 'allow' })), + aliases: schema.maybe(schema.object({}, { unknowns: 'allow' })), + mappings: schema.maybe(schema.object({}, { unknowns: 'allow' })), + }), + version: schema.maybe(schema.number()), + _meta: schema.maybe(schema.object({}, { unknowns: 'allow' })), +}; diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts new file mode 100644 index 0000000000000..7e447bb110c67 --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts @@ -0,0 +1,62 @@ +/* + * 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 { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../index'; +import { componentTemplateSchema } from './schema_validation'; + +const bodySchema = schema.object(componentTemplateSchema); + +const paramsSchema = schema.object({ + name: schema.string(), +}); + +export const registerUpdateRoute = ({ + router, + license, + lib: { isEsError }, +}: RouteDependencies): void => { + router.put( + { + path: addBasePath('/component_templates/{name}'), + validate: { + body: bodySchema, + params: paramsSchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.dataManagement!.client; + const { name } = req.params; + const { template, version, _meta } = req.body; + + try { + // Verify component exists; ES will throw 404 if not + await callAsCurrentUser('dataManagement.getComponentTemplate', { name }); + + const response = await callAsCurrentUser('dataManagement.saveComponentTemplate', { + name, + body: { + template, + version, + _meta, + }, + }); + + return res.ok({ body: response }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); +}; diff --git a/x-pack/plugins/index_management/server/routes/index.ts b/x-pack/plugins/index_management/server/routes/index.ts index 870cfa36ecc6a..1e5aaf8087624 100644 --- a/x-pack/plugins/index_management/server/routes/index.ts +++ b/x-pack/plugins/index_management/server/routes/index.ts @@ -11,6 +11,7 @@ import { registerTemplateRoutes } from './api/templates'; import { registerMappingRoute } from './api/mapping'; import { registerSettingsRoutes } from './api/settings'; import { registerStatsRoute } from './api/stats'; +import { registerComponentTemplateRoutes } from './api/component_templates'; export class ApiRoutes { setup(dependencies: RouteDependencies) { @@ -19,6 +20,7 @@ export class ApiRoutes { registerSettingsRoutes(dependencies); registerStatsRoute(dependencies); registerMappingRoute(dependencies); + registerComponentTemplateRoutes(dependencies); } start() {} diff --git a/x-pack/plugins/index_management/server/services/license.ts b/x-pack/plugins/index_management/server/services/license.ts index 2d863e283d440..9b68acd073c4a 100644 --- a/x-pack/plugins/index_management/server/services/license.ts +++ b/x-pack/plugins/index_management/server/services/license.ts @@ -53,12 +53,12 @@ export class License { }); } - guardApiRoute(handler: RequestHandler) { + guardApiRoute(handler: RequestHandler) { const license = this; return function licenseCheck( ctx: RequestHandlerContext, - request: KibanaRequest, + request: KibanaRequest, response: KibanaResponseFactory ) { const licenseStatus = license.getStatus(); diff --git a/x-pack/plugins/infra/kibana.json b/x-pack/plugins/infra/kibana.json index ea66ae7a46d4e..4701182c96813 100644 --- a/x-pack/plugins/infra/kibana.json +++ b/x-pack/plugins/infra/kibana.json @@ -10,7 +10,7 @@ "data", "dataEnhanced", "visTypeTimeseries", - "alerting", + "alerts", "triggers_actions_ui" ], "server": true, 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) => { /> isILMPolicy(entry) - ); +export async function installILMPolicy(paths: string[], callCluster: CallESAsCurrentUser) { + const ilmPaths = paths.filter((path) => isILMPolicy(path)); if (!ilmPaths.length) return; await Promise.all( ilmPaths.map(async (path) => { @@ -36,7 +28,7 @@ export async function installILMPolicy( }) ); } -const isILMPolicy = ({ path }: Registry.ArchiveEntry) => { +const isILMPolicy = (path: string) => { const pathParts = Registry.pathParts(path); return pathParts.type === ElasticsearchAssetType.ilmPolicy; }; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts index bdf6ecfcdb9aa..11543fe73886f 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts @@ -22,9 +22,11 @@ interface RewriteSubstitution { export const installPipelines = async ( registryPackage: RegistryPackage, + paths: string[], callCluster: CallESAsCurrentUser ) => { const datasets = registryPackage.datasets; + const pipelinePaths = paths.filter((path) => isPipeline(path)); if (datasets) { const pipelines = datasets.reduce>>((acc, dataset) => { if (dataset.ingest_pipeline) { @@ -32,7 +34,7 @@ export const installPipelines = async ( installPipelinesForDataset({ dataset, callCluster, - pkgName: registryPackage.name, + paths: pipelinePaths, pkgVersion: registryPackage.version, }) ); @@ -67,20 +69,16 @@ export function rewriteIngestPipeline( export async function installPipelinesForDataset({ callCluster, - pkgName, pkgVersion, + paths, dataset, }: { callCluster: CallESAsCurrentUser; - pkgName: string; pkgVersion: string; + paths: string[]; dataset: Dataset; }): Promise { - const pipelinePaths = await Registry.getArchiveInfo( - pkgName, - pkgVersion, - (entry: Registry.ArchiveEntry) => isDatasetPipeline(entry, dataset.path) - ); + const pipelinePaths = paths.filter((path) => isDatasetPipeline(path, dataset.path)); let pipelines: any[] = []; const substitutions: RewriteSubstitution[] = []; @@ -152,8 +150,8 @@ async function installPipeline({ } const isDirectory = ({ path }: Registry.ArchiveEntry) => path.endsWith('/'); -const isDatasetPipeline = ({ path }: Registry.ArchiveEntry, datasetName: string) => { - // TODO: better way to get particular assets + +const isDatasetPipeline = (path: string, datasetName: string) => { const pathParts = Registry.pathParts(path); return ( !isDirectory({ path }) && @@ -162,6 +160,10 @@ const isDatasetPipeline = ({ path }: Registry.ArchiveEntry, datasetName: string) datasetName === pathParts.dataset ); }; +const isPipeline = (path: string) => { + const pathParts = Registry.pathParts(path); + return pathParts.type === ElasticsearchAssetType.ingestPipeline; +}; // XXX: assumes path/to/file.ext -- 0..n '/' and exactly one '.' const getNameAndExtension = ( diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts index c600c8ba3efb8..9d0b6b5d078ad 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts @@ -16,13 +16,14 @@ export const installTemplates = async ( registryPackage: RegistryPackage, callCluster: CallESAsCurrentUser, pkgName: string, - pkgVersion: string + pkgVersion: string, + paths: string[] ): Promise => { // install any pre-built index template assets, // atm, this is only the base package's global index templates // Install component templates first, as they are used by the index templates - await installPreBuiltComponentTemplates(pkgName, pkgVersion, callCluster); - await installPreBuiltTemplates(pkgName, pkgVersion, callCluster); + await installPreBuiltComponentTemplates(paths, callCluster); + await installPreBuiltTemplates(paths, callCluster); // build templates per dataset from yml files const datasets = registryPackage.datasets; @@ -44,16 +45,8 @@ export const installTemplates = async ( return []; }; -const installPreBuiltTemplates = async ( - pkgName: string, - pkgVersion: string, - callCluster: CallESAsCurrentUser -) => { - const templatePaths = await Registry.getArchiveInfo( - pkgName, - pkgVersion, - (entry: Registry.ArchiveEntry) => isTemplate(entry) - ); +const installPreBuiltTemplates = async (paths: string[], callCluster: CallESAsCurrentUser) => { + const templatePaths = paths.filter((path) => isTemplate(path)); const templateInstallPromises = templatePaths.map(async (path) => { const { file } = Registry.pathParts(path); const templateName = file.substr(0, file.lastIndexOf('.')); @@ -95,15 +88,10 @@ const installPreBuiltTemplates = async ( }; const installPreBuiltComponentTemplates = async ( - pkgName: string, - pkgVersion: string, + paths: string[], callCluster: CallESAsCurrentUser ) => { - const templatePaths = await Registry.getArchiveInfo( - pkgName, - pkgVersion, - (entry: Registry.ArchiveEntry) => isComponentTemplate(entry) - ); + const templatePaths = paths.filter((path) => isComponentTemplate(path)); const templateInstallPromises = templatePaths.map(async (path) => { const { file } = Registry.pathParts(path); const templateName = file.substr(0, file.lastIndexOf('.')); @@ -134,12 +122,12 @@ const installPreBuiltComponentTemplates = async ( } }; -const isTemplate = ({ path }: Registry.ArchiveEntry) => { +const isTemplate = (path: string) => { const pathParts = Registry.pathParts(path); return pathParts.type === ElasticsearchAssetType.indexTemplate; }; -const isComponentTemplate = ({ path }: Registry.ArchiveEntry) => { +const isComponentTemplate = (path: string) => { const pathParts = Registry.pathParts(path); return pathParts.type === ElasticsearchAssetType.componentTemplate; }; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts index f321e2d614a04..0f7b1d6cab178 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts @@ -86,6 +86,14 @@ export async function installIndexPatterns( savedObjectsClient, InstallationStatus.installed ); + + // TODO: move to install package + // cache all installed packages if they don't exist + const packagePromises = installedPackages.map((pkg) => + Registry.ensureCachedArchiveInfo(pkg.pkgName, pkg.pkgVersion) + ); + await Promise.all(packagePromises); + if (pkgName && pkgVersion) { // add this package to the array if it doesn't already exist const foundPkg = installedPackages.find((pkg) => pkg.pkgName === pkgName); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts index c6f7a1f6b97aa..37fcf0db67131 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts @@ -6,7 +6,7 @@ import { RegistryPackage } from '../../../types'; import * as Registry from '../registry'; -import { cacheHas } from '../registry/cache'; +import { ensureCachedArchiveInfo } from '../registry'; // paths from RegistryPackage are routes to the assets on EPR // e.g. `/package/nginx/1.2.0/dataset/access/fields/fields.yml` @@ -57,8 +57,8 @@ export async function getAssetsData( datasetName?: string ): Promise { // TODO: Needs to be called to fill the cache but should not be required - const pkgkey = packageInfo.name + '-' + packageInfo.version; - if (!cacheHas(pkgkey)) await Registry.getArchiveInfo(packageInfo.name, packageInfo.version); + + await ensureCachedArchiveInfo(packageInfo.name, packageInfo.version); // Gather all asset data const assets = getAssets(packageInfo, filter, datasetName); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index dddb21bc4e075..7c0d5d571f6a5 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -90,7 +90,7 @@ export async function installPackage(options: { const { savedObjectsClient, pkgkey, callCluster } = options; // TODO: change epm API to /packageName/version so we don't need to do this const [pkgName, pkgVersion] = pkgkey.split('-'); - + const paths = await Registry.getArchiveInfo(pkgName, pkgVersion); // see if some version of this package is already installed // TODO: calls to getInstallationObject, Registry.fetchInfo, and Registry.fetchFindLatestPackge // and be replaced by getPackageInfo after adjusting for it to not group/use archive assets @@ -119,15 +119,16 @@ export async function installPackage(options: { savedObjectsClient, pkgName, pkgVersion, + paths, }), - installPipelines(registryPackageInfo, callCluster), + installPipelines(registryPackageInfo, paths, callCluster), // index patterns and ilm policies are not currently associated with a particular package // so we do not save them in the package saved object state. installIndexPatterns(savedObjectsClient, pkgName, pkgVersion), // currenly only the base package has an ILM policy // at some point ILM policies can be installed/modified // per dataset and we should then save them - installILMPolicy(pkgName, pkgVersion, callCluster), + installILMPolicy(paths, callCluster), ]); // install or update the templates @@ -135,7 +136,8 @@ export async function installPackage(options: { registryPackageInfo, callCluster, pkgName, - pkgVersion + pkgVersion, + paths ); const toSaveESIndexPatterns = generateESIndexPatterns(registryPackageInfo.datasets); @@ -186,13 +188,14 @@ export async function installKibanaAssets(options: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; pkgVersion: string; + paths: string[]; }) { - const { savedObjectsClient, pkgName, pkgVersion } = options; + const { savedObjectsClient, paths } = options; // Only install Kibana assets during package installation. const kibanaAssetTypes = Object.values(KibanaAssetType); const installationPromises = kibanaAssetTypes.map(async (assetType) => - installKibanaSavedObjects({ savedObjectsClient, pkgName, pkgVersion, assetType }) + installKibanaSavedObjects({ savedObjectsClient, assetType, paths }) ); // installKibanaSavedObjects returns AssetReference[], so .map creates AssetReference[][] @@ -237,19 +240,16 @@ export async function saveInstallationReferences(options: { async function installKibanaSavedObjects({ savedObjectsClient, - pkgName, - pkgVersion, assetType, + paths, }: { savedObjectsClient: SavedObjectsClientContract; - pkgName: string; - pkgVersion: string; assetType: KibanaAssetType; + paths: string[]; }) { - const isSameType = ({ path }: Registry.ArchiveEntry) => - assetType === Registry.pathParts(path).type; - const paths = await Registry.getArchiveInfo(pkgName, pkgVersion, isSameType); - const toBeSavedObjects = await Promise.all(paths.map(getObject)); + const isSameType = (path: string) => assetType === Registry.pathParts(path).type; + const pathsOfType = paths.filter((path) => isSameType(path)); + const toBeSavedObjects = await Promise.all(pathsOfType.map(getObject)); if (toBeSavedObjects.length === 0) { return []; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/cache.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/cache.ts index 17d52bc745a55..d2a14fcf04dff 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/cache.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/cache.ts @@ -8,3 +8,4 @@ const cache: Map = new Map(); export const cacheGet = (key: string) => cache.get(key); export const cacheSet = (key: string, value: Buffer) => cache.set(key, value); export const cacheHas = (key: string) => cache.has(key); +export const getCacheKey = (key: string) => key + '.tar.gz'; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts index 8e9b920875617..0393cabca8ba2 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts @@ -16,7 +16,7 @@ import { RegistrySearchResults, RegistrySearchResult, } from '../../../types'; -import { cacheGet, cacheSet } from './cache'; +import { cacheGet, cacheSet, getCacheKey, cacheHas } from './cache'; import { ArchiveEntry, untarBuffer } from './extract'; import { fetchUrl, getResponse, getResponseStream } from './requests'; import { streamToBuffer } from './streams'; @@ -135,7 +135,7 @@ async function extract( async function getOrFetchArchiveBuffer(pkgName: string, pkgVersion: string): Promise { // assume .tar.gz for now. add support for .zip if/when we need it - const key = `${pkgName}-${pkgVersion}.tar.gz`; + const key = getCacheKey(`${pkgName}-${pkgVersion}`); let buffer = cacheGet(key); if (!buffer) { buffer = await fetchArchiveBuffer(pkgName, pkgVersion); @@ -149,6 +149,13 @@ async function getOrFetchArchiveBuffer(pkgName: string, pkgVersion: string): Pro } } +export async function ensureCachedArchiveInfo(name: string, version: string) { + const pkgkey = getCacheKey(`${name}-${version}`); + if (!cacheHas(pkgkey)) { + await getArchiveInfo(name, version); + } +} + async function fetchArchiveBuffer(pkgName: string, pkgVersion: string): Promise { const { download: archivePath } = await fetchInfo(pkgName, pkgVersion); const registryUrl = getRegistryUrl(); diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 1762965478292..f1a2edd2d554f 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -9,6 +9,7 @@ import { ReactWrapper } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { App } from './app'; import { EditorFrameInstance } from '../types'; +import { AppMountParameters } from 'kibana/public'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { Document, SavedObjectStore } from '../persistence'; import { mount } from 'enzyme'; @@ -111,6 +112,7 @@ describe('Lens App', () => { newlyCreated?: boolean ) => void; originatingApp: string | undefined; + onAppLeave: AppMountParameters['onAppLeave']; }> { return ({ navigation: navigationStartMock, @@ -153,6 +155,7 @@ describe('Lens App', () => { newlyCreated?: boolean ) => {} ), + onAppLeave: jest.fn(), } as unknown) as jest.Mocked<{ navigation: typeof navigationStartMock; editorFrame: EditorFrameInstance; @@ -168,6 +171,7 @@ describe('Lens App', () => { newlyCreated?: boolean ) => void; originatingApp: string | undefined; + onAppLeave: AppMountParameters['onAppLeave']; }>; } @@ -357,22 +361,7 @@ describe('Lens App', () => { newTitle: string; } - let defaultArgs: jest.Mocked<{ - editorFrame: EditorFrameInstance; - navigation: typeof navigationStartMock; - data: typeof dataStartMock; - core: typeof core; - storage: Storage; - docId?: string; - docStorage: SavedObjectStore; - redirectTo: ( - id?: string, - returnToOrigin?: boolean, - originatingApp?: string | undefined, - newlyCreated?: boolean - ) => void; - originatingApp: string | undefined; - }>; + let defaultArgs: ReturnType; beforeEach(() => { defaultArgs = makeDefaultArgs(); @@ -486,30 +475,6 @@ describe('Lens App', () => { expect(getButton(instance).disableButton).toEqual(true); }); - it('shows a disabled save button when there are no changes to the document', async () => { - const args = defaultArgs; - (args.docStorage.load as jest.Mock).mockResolvedValue({ - id: '1234', - title: 'My cool doc', - expression: '', - } as jest.ResolvedValue); - args.editorFrame = frame; - - instance = mount(); - expect(getButton(instance).disableButton).toEqual(true); - - const onChange = frame.mount.mock.calls[0][1].onChange; - - act(() => { - onChange({ - filterableIndexPatterns: [], - doc: ({ id: '1234', expression: 'valid expression' } as unknown) as Document, - }); - }); - instance.update(); - expect(getButton(instance).disableButton).toEqual(false); - }); - it('shows a save button that is enabled when the frame has provided its state', async () => { const args = defaultArgs; args.editorFrame = frame; @@ -691,21 +656,7 @@ describe('Lens App', () => { }); describe('query bar state management', () => { - let defaultArgs: jest.Mocked<{ - editorFrame: EditorFrameInstance; - data: typeof dataStartMock; - navigation: typeof navigationStartMock; - core: typeof core; - storage: Storage; - docId?: string; - docStorage: SavedObjectStore; - redirectTo: ( - id?: string, - returnToOrigin?: boolean, - originatingApp?: string | undefined, - newlyCreated?: boolean - ) => void; - }>; + let defaultArgs: ReturnType; beforeEach(() => { defaultArgs = makeDefaultArgs(); @@ -1001,4 +952,159 @@ describe('Lens App', () => { expect(args.core.notifications.toasts.addDanger).toHaveBeenCalled(); }); + + describe('showing a confirm message when leaving', () => { + let defaultArgs: ReturnType; + let defaultLeave: jest.Mock; + let confirmLeave: jest.Mock; + + beforeEach(() => { + defaultArgs = makeDefaultArgs(); + defaultLeave = jest.fn(); + confirmLeave = jest.fn(); + (defaultArgs.docStorage.load as jest.Mock).mockResolvedValue({ + id: '1234', + title: 'My cool doc', + expression: 'valid expression', + state: { + query: 'kuery', + datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, + }, + } as jest.ResolvedValue); + }); + + it('should not show a confirm message if there is no expression to save', () => { + instance = mount(); + + const lastCall = + defaultArgs.onAppLeave.mock.calls[defaultArgs.onAppLeave.mock.calls.length - 1][0]; + lastCall({ default: defaultLeave, confirm: confirmLeave }); + + expect(defaultLeave).toHaveBeenCalled(); + expect(confirmLeave).not.toHaveBeenCalled(); + }); + + it('does not confirm if the user is missing save permissions', () => { + const args = defaultArgs; + args.core.application = { + ...args.core.application, + capabilities: { + ...args.core.application.capabilities, + visualize: { save: false, saveQuery: false, show: true }, + }, + }; + args.editorFrame = frame; + + instance = mount(); + + const onChange = frame.mount.mock.calls[0][1].onChange; + act(() => + onChange({ + filterableIndexPatterns: [], + doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, + }) + ); + instance.update(); + + const lastCall = + defaultArgs.onAppLeave.mock.calls[defaultArgs.onAppLeave.mock.calls.length - 1][0]; + lastCall({ default: defaultLeave, confirm: confirmLeave }); + + expect(defaultLeave).toHaveBeenCalled(); + expect(confirmLeave).not.toHaveBeenCalled(); + }); + + it('should confirm when leaving with an unsaved doc', () => { + defaultArgs.editorFrame = frame; + instance = mount(); + + const onChange = frame.mount.mock.calls[0][1].onChange; + act(() => + onChange({ + filterableIndexPatterns: [], + doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, + }) + ); + instance.update(); + + const lastCall = + defaultArgs.onAppLeave.mock.calls[defaultArgs.onAppLeave.mock.calls.length - 1][0]; + lastCall({ default: defaultLeave, confirm: confirmLeave }); + + expect(confirmLeave).toHaveBeenCalled(); + expect(defaultLeave).not.toHaveBeenCalled(); + }); + + it('should confirm when leaving with unsaved changes to an existing doc', async () => { + defaultArgs.editorFrame = frame; + instance = mount(); + await act(async () => { + instance.setProps({ docId: '1234' }); + }); + + const onChange = frame.mount.mock.calls[0][1].onChange; + act(() => + onChange({ + filterableIndexPatterns: [], + doc: ({ id: '1234', expression: 'different expression' } as unknown) as Document, + }) + ); + instance.update(); + + const lastCall = + defaultArgs.onAppLeave.mock.calls[defaultArgs.onAppLeave.mock.calls.length - 1][0]; + lastCall({ default: defaultLeave, confirm: confirmLeave }); + + expect(confirmLeave).toHaveBeenCalled(); + expect(defaultLeave).not.toHaveBeenCalled(); + }); + + it('should not confirm when changes are saved', async () => { + defaultArgs.editorFrame = frame; + instance = mount(); + await act(async () => { + instance.setProps({ docId: '1234' }); + }); + + const onChange = frame.mount.mock.calls[0][1].onChange; + act(() => + onChange({ + filterableIndexPatterns: [], + doc: ({ id: '1234', expression: 'valid expression' } as unknown) as Document, + }) + ); + instance.update(); + + const lastCall = + defaultArgs.onAppLeave.mock.calls[defaultArgs.onAppLeave.mock.calls.length - 1][0]; + lastCall({ default: defaultLeave, confirm: confirmLeave }); + + expect(defaultLeave).toHaveBeenCalled(); + expect(confirmLeave).not.toHaveBeenCalled(); + }); + + it('should confirm when the latest doc is invalid', async () => { + defaultArgs.editorFrame = frame; + instance = mount(); + await act(async () => { + instance.setProps({ docId: '1234' }); + }); + + const onChange = frame.mount.mock.calls[0][1].onChange; + act(() => + onChange({ + filterableIndexPatterns: [], + doc: ({ id: '1234', expression: null } as unknown) as Document, + }) + ); + instance.update(); + + const lastCall = + defaultArgs.onAppLeave.mock.calls[defaultArgs.onAppLeave.mock.calls.length - 1][0]; + lastCall({ default: defaultLeave, confirm: confirmLeave }); + + expect(confirmLeave).toHaveBeenCalled(); + expect(defaultLeave).not.toHaveBeenCalled(); + }); + }); }); diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index a77fbbb597564..ffa59a6fb6bc9 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -10,7 +10,7 @@ import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { Query, DataPublicPluginStart } from 'src/plugins/data/public'; import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; -import { AppMountContext, NotificationsStart } from 'kibana/public'; +import { AppMountContext, AppMountParameters, NotificationsStart } from 'kibana/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { @@ -57,6 +57,7 @@ export function App({ redirectTo, originatingAppFromUrl, navigation, + onAppLeave, }: { editorFrame: EditorFrameInstance; data: DataPublicPluginStart; @@ -72,6 +73,7 @@ export function App({ newlyCreated?: boolean ) => void; originatingAppFromUrl?: string | undefined; + onAppLeave: AppMountParameters['onAppLeave']; }) { const language = storage.get('kibana.userQueryLanguage') || core.uiSettings.get('search:queryLanguage'); @@ -94,6 +96,12 @@ export function App({ const { lastKnownDoc } = state; + const isSaveable = + lastKnownDoc && + lastKnownDoc.expression && + lastKnownDoc.expression.length > 0 && + core.application.capabilities.visualize.save; + useEffect(() => { // Clear app-specific filters when navigating to Lens. Necessary because Lens // can be loaded without a full page refresh @@ -123,7 +131,31 @@ export function App({ filterSubscription.unsubscribe(); timeSubscription.unsubscribe(); }; - }, []); + }, [data.query.filterManager, data.query.timefilter.timefilter]); + + useEffect(() => { + onAppLeave((actions) => { + // Confirm when the user has made any changes to an existing doc + // or when the user has configured something without saving + if ( + core.application.capabilities.visualize.save && + (state.persistedDoc?.expression + ? !_.isEqual(lastKnownDoc?.expression, state.persistedDoc.expression) + : lastKnownDoc?.expression) + ) { + return actions.confirm( + i18n.translate('xpack.lens.app.unsavedWorkMessage', { + defaultMessage: 'Leave Lens with unsaved work?', + }), + i18n.translate('xpack.lens.app.unsavedWorkTitle', { + defaultMessage: 'Unsaved changes', + }) + ); + } else { + return actions.default(); + } + }); + }, [lastKnownDoc, onAppLeave, state.persistedDoc, core.application.capabilities.visualize.save]); // Sync Kibana breadcrumbs any time the saved document's title changes useEffect(() => { @@ -144,7 +176,7 @@ export function App({ : i18n.translate('xpack.lens.breadcrumbsCreate', { defaultMessage: 'Create' }), }, ]); - }, [state.persistedDoc && state.persistedDoc.title]); + }, [core.application, core.chrome, core.http.basePath, state.persistedDoc]); useEffect(() => { if (docId && (!state.persistedDoc || state.persistedDoc.id !== docId)) { @@ -187,13 +219,16 @@ export function App({ redirectTo(); }); } - }, [docId]); - - const isSaveable = - lastKnownDoc && - lastKnownDoc.expression && - lastKnownDoc.expression.length > 0 && - core.application.capabilities.visualize.save; + }, [ + core.notifications, + data.indexPatterns, + data.query.filterManager, + docId, + // TODO: These dependencies are changing too often + // docStorage, + // redirectTo, + // state.persistedDoc, + ]); const runSave = ( saveProps: Omit & { @@ -257,7 +292,7 @@ export function App({ core.notifications.toasts.addDanger({ title: e.message, }), - [] + [core.notifications.toasts] ); const { TopNavMenu } = navigation.ui; diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 7c875935f6320..032ce8325dca1 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -92,6 +92,7 @@ export async function mountApp( redirectTo(routeProps, id, returnToOrigin, originatingApp, newlyCreated) } originatingAppFromUrl={originatingAppFromUrl} + onAppLeave={params.onAppLeave} /> ); }; diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx index 1fd01654d8149..143bec227ebee 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx @@ -22,7 +22,7 @@ import { } from '../../../../../src/plugins/expressions/public'; import { VisualizationContainer } from '../visualization_container'; import { EmptyPlaceholder } from '../shared_components'; - +import { desanitizeFilterContext } from '../utils'; export interface DatatableColumns { columnIds: string[]; } @@ -180,7 +180,7 @@ export function DatatableComponent(props: DatatableRenderProps) { ], timeFieldName, }; - props.onClickValue(data); + props.onClickValue(desanitizeFilterContext(data)); }, [firstTable] ); diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx index 6bf629912f53c..765522067eaf0 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { render, shallow, mount } from 'enzyme'; +import { render, mount } from 'enzyme'; import { DragDrop } from './drag_drop'; import { ChildDragDropProvider } from './providers'; @@ -24,7 +24,7 @@ describe('DragDrop', () => { test('dragover calls preventDefault if droppable is true', () => { const preventDefault = jest.fn(); - const component = shallow(Hello!); + const component = mount(Hello!); component.find('[data-test-subj="lnsDragDrop"]').simulate('dragover', { preventDefault }); @@ -33,7 +33,7 @@ describe('DragDrop', () => { test('dragover does not call preventDefault if droppable is false', () => { const preventDefault = jest.fn(); - const component = shallow(Hello!); + const component = mount(Hello!); component.find('[data-test-subj="lnsDragDrop"]').simulate('dragover', { preventDefault }); diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx index 72b0d58122405..5a0fc3b3839f7 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx @@ -88,11 +88,39 @@ type Props = DraggableProps | NonDraggableProps; * * @param props */ -export function DragDrop(props: Props) { + +export const DragDrop = (props: Props) => { const { dragging, setDragging } = useContext(DragContext); + const { value, draggable, droppable } = props; + return ( + + ); +}; + +const DragDropInner = React.memo(function DragDropInner( + props: Props & { + dragging: unknown; + setDragging: (dragging: unknown) => void; + isDragging: boolean; + } +) { const [state, setState] = useState({ isActive: false }); - const { className, onDrop, value, children, droppable, draggable } = props; - const isDragging = draggable && value === dragging; + const { + className, + onDrop, + value, + children, + droppable, + draggable, + dragging, + setDragging, + isDragging, + } = props; const classes = classNames('lnsDragDrop', className, { 'lnsDragDrop-isDropTarget': droppable, @@ -166,4 +194,4 @@ export function DragDrop(props: Props) { {children} ); -} +}); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss index 3fbc42f9a25a0..924f44a37c459 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss @@ -31,3 +31,6 @@ min-height: $euiSizeXXL; } +.lnsLayerPanel__styleEditor { + width: $euiSize * 28; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx index 0d86a051b0faa..e53e465c18950 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -45,7 +45,6 @@ function LayerPanels( } ) { const { - framePublicAPI, activeVisualization, visualizationState, dispatch, @@ -109,12 +108,10 @@ function LayerPanels( {...props} key={layerId} layerId={layerId} - activeVisualization={activeVisualization} visualizationState={visualizationState} updateVisualization={setVisualizationState} updateDatasource={updateDatasource} updateAll={updateAll} - frame={framePublicAPI} isOnlyLayer={layerIds.length === 1} onRemoveLayer={() => { dispatch({ diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_popover.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_popover.tsx index f89b6ef32d3f7..cc8d97a445016 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_popover.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_popover.tsx @@ -36,7 +36,7 @@ export function DimensionPopover({ (popoverState.openId === accessor || (noMatch && popoverState.addingToGroupId === groupId)) } closePopover={() => { - setPopoverState({ isOpen: false, openId: null, addingToGroupId: null }); + setPopoverState({ isOpen: false, openId: null, addingToGroupId: null, tabId: null }); }} button={trigger} anchorPosition="leftUp" diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx new file mode 100644 index 0000000000000..1f987f86d3950 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -0,0 +1,271 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { + createMockVisualization, + createMockFramePublicAPI, + createMockDatasource, + DatasourceMock, +} from '../../mocks'; +import { EuiFormRow, EuiPopover } from '@elastic/eui'; +import { mount } from 'enzyme'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { Visualization } from '../../../types'; +import { LayerPanel } from './layer_panel'; +import { coreMock } from 'src/core/public/mocks'; +import { generateId } from '../../../id_generator'; + +jest.mock('../../../id_generator'); + +describe('LayerPanel', () => { + let mockVisualization: jest.Mocked; + let mockDatasource: DatasourceMock; + + function getDefaultProps() { + const frame = createMockFramePublicAPI(); + frame.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + return { + layerId: 'first', + activeVisualizationId: 'vis1', + visualizationMap: { + vis1: mockVisualization, + }, + activeDatasourceId: 'ds1', + datasourceMap: { + ds1: mockDatasource, + }, + datasourceStates: { + ds1: { + isLoading: false, + state: 'state', + }, + }, + visualizationState: 'state', + updateVisualization: jest.fn(), + updateDatasource: jest.fn(), + updateAll: jest.fn(), + framePublicAPI: frame, + isOnlyLayer: true, + onRemoveLayer: jest.fn(), + dispatch: jest.fn(), + core: coreMock.createStart(), + }; + } + + beforeEach(() => { + mockVisualization = { + ...createMockVisualization(), + id: 'testVis', + visualizationTypes: [ + { + icon: 'empty', + id: 'testVis', + label: 'TEST1', + }, + ], + }; + + mockVisualization.getLayerIds.mockReturnValue(['first']); + mockDatasource = createMockDatasource('ds1'); + }); + + it('should fail to render if the public API is out of date', () => { + const props = getDefaultProps(); + props.framePublicAPI.datasourceLayers = {}; + const component = mountWithIntl(); + expect(component.isEmptyRender()).toBe(true); + }); + + it('should fail to render if the active visualization is missing', () => { + const component = mountWithIntl( + + ); + expect(component.isEmptyRender()).toBe(true); + }); + + describe('layer reset and remove', () => { + it('should show the reset button when single layer', () => { + const component = mountWithIntl(); + expect(component.find('[data-test-subj="lns_layer_remove"]').first().text()).toContain( + 'Reset layer' + ); + }); + + it('should show the delete button when multiple layers', () => { + const component = mountWithIntl(); + expect(component.find('[data-test-subj="lns_layer_remove"]').first().text()).toContain( + 'Delete layer' + ); + }); + + it('should call the clear callback', () => { + const cb = jest.fn(); + const component = mountWithIntl(); + act(() => { + component.find('[data-test-subj="lns_layer_remove"]').first().simulate('click'); + }); + expect(cb).toHaveBeenCalled(); + }); + }); + + describe('single group', () => { + it('should render the non-editable state', () => { + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: ['x'], + filterOperations: () => true, + supportsMoreColumns: false, + dataTestSubj: 'lnsGroup', + }, + ], + }); + + const component = mountWithIntl(); + + const group = component.find('DragDrop[data-test-subj="lnsGroup"]'); + expect(group).toHaveLength(1); + }); + + it('should render the group with a way to add a new column', () => { + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + }, + ], + }); + + const component = mountWithIntl(); + + const group = component.find('DragDrop[data-test-subj="lnsGroup"]'); + expect(group).toHaveLength(1); + }); + + it('should render the required warning when only one group is configured', () => { + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: ['x'], + filterOperations: () => true, + supportsMoreColumns: false, + dataTestSubj: 'lnsGroup', + }, + { + groupLabel: 'B', + groupId: 'b', + accessors: [], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + required: true, + }, + ], + }); + + const component = mountWithIntl(); + + const group = component + .find(EuiFormRow) + .findWhere((e) => e.prop('error') === 'Required dimension'); + expect(group).toHaveLength(1); + }); + + it('should render the datasource and visualization panels inside the dimension popover', () => { + mockVisualization.getConfiguration.mockReturnValueOnce({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: ['newid'], + filterOperations: () => true, + supportsMoreColumns: false, + dataTestSubj: 'lnsGroup', + enableDimensionEditor: true, + }, + ], + }); + mockVisualization.renderDimensionEditor = jest.fn(); + + const component = mountWithIntl(); + + const group = component.find('DimensionPopover'); + const panel = mount(group.prop('panel')); + + expect(panel.find('EuiTabbedContent').prop('tabs')).toHaveLength(2); + act(() => { + panel.find('EuiTab#visualization').simulate('click'); + }); + expect(mockVisualization.renderDimensionEditor).toHaveBeenCalledWith( + expect.any(Element), + expect.objectContaining({ + groupId: 'a', + accessor: 'newid', + }) + ); + }); + + it('should keep the popover open when configuring a new dimension', () => { + /** + * The ID generation system for new dimensions has been messy before, so + * this tests that the ID used in the first render is used to keep the popover + * open in future renders + */ + (generateId as jest.Mock).mockReturnValueOnce(`newid`); + (generateId as jest.Mock).mockReturnValueOnce(`bad`); + mockVisualization.getConfiguration.mockReturnValueOnce({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + }, + ], + }); + // Normally the configuration would change in response to a state update, + // but this test is updating it directly + mockVisualization.getConfiguration.mockReturnValueOnce({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: ['newid'], + filterOperations: () => true, + supportsMoreColumns: false, + dataTestSubj: 'lnsGroup', + }, + ], + }); + + const component = mountWithIntl(); + + const group = component.find('DimensionPopover'); + const triggerButton = mountWithIntl(group.prop('trigger')); + act(() => { + triggerButton.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); + }); + component.update(); + + expect(component.find(EuiPopover).prop('isOpen')).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 814b7fc644c9c..bd501db2b752a 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -13,11 +13,12 @@ import { EuiFlexItem, EuiButtonEmpty, EuiFormRow, + EuiTabbedContent, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { NativeRenderer } from '../../../native_renderer'; -import { Visualization, FramePublicAPI, StateSetter } from '../../../types'; +import { StateSetter } from '../../../types'; import { DragContext, DragDrop, ChildDragDropProvider } from '../../../drag_drop'; import { LayerSettings } from './layer_settings'; import { trackUiEvent } from '../../../lens_ui_telemetry'; @@ -27,11 +28,8 @@ import { DimensionPopover } from './dimension_popover'; export function LayerPanel( props: Exclude & { - frame: FramePublicAPI; layerId: string; isOnlyLayer: boolean; - activeVisualization: Visualization; - visualizationState: unknown; updateVisualization: StateSetter; updateDatasource: (datasourceId: string, newState: unknown) => void; updateAll: ( @@ -47,13 +45,19 @@ export function LayerPanel( isOpen: false, openId: null, addingToGroupId: null, + tabId: null, }); - const { framePublicAPI, layerId, activeVisualization, isOnlyLayer, onRemoveLayer } = props; + const { framePublicAPI, layerId, isOnlyLayer, onRemoveLayer } = props; const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId]; - if (!datasourcePublicAPI) { + if ( + !datasourcePublicAPI || + !props.activeVisualizationId || + !props.visualizationMap[props.activeVisualizationId] + ) { return null; } + const activeVisualization = props.visualizationMap[props.activeVisualizationId]; const layerVisualizationConfigProps = { layerId, dragDropContext, @@ -158,104 +162,156 @@ export function LayerPanel( } > <> - {group.accessors.map((accessor) => ( - { - layerDatasource.onDrop({ - ...layerDatasourceDropProps, - droppedItem, - columnId: accessor, - filterOperations: group.filterOperations, - }); - }} - > - { - if (popoverState.isOpen) { - setPopoverState({ - isOpen: false, - openId: null, - addingToGroupId: null, - }); - } else { - setPopoverState({ - isOpen: true, - openId: accessor, - addingToGroupId: null, // not set for existing dimension - }); - } - }, - }} - /> - } - panel={ - - } - /> + {group.accessors.map((accessor) => { + const tabs = [ + { + id: 'datasource', + name: i18n.translate('xpack.lens.editorFrame.quickFunctionsLabel', { + defaultMessage: 'Quick functions', + }), + content: ( + <> + + + + ), + }, + ]; - { - trackUiEvent('indexpattern_dimension_removed'); - props.updateAll( - datasourceId, - layerDatasource.removeColumn({ - layerId, - columnId: accessor, - prevState: layerDatasourceState, - }), - props.activeVisualization.removeDimension({ - layerId, - columnId: accessor, - prevState: props.visualizationState, - }) - ); + if (activeVisualization.renderDimensionEditor) { + tabs.push({ + id: 'visualization', + name: i18n.translate('xpack.lens.editorFrame.formatStyleLabel', { + defaultMessage: 'Format & style', + }), + content: ( +
    + + +
    + ), + }); + } + + return ( + { + layerDatasource.onDrop({ + ...layerDatasourceDropProps, + droppedItem, + columnId: accessor, + filterOperations: group.filterOperations, + }); }} - /> - - ))} + > + { + if (popoverState.isOpen) { + setPopoverState({ + isOpen: false, + openId: null, + addingToGroupId: null, + tabId: null, + }); + } else { + setPopoverState({ + isOpen: true, + openId: accessor, + addingToGroupId: null, // not set for existing dimension + tabId: 'datasource', + }); + } + }, + }} + /> + } + panel={ + t.id === popoverState.tabId)} + size="s" + onTabClick={(tab) => { + setPopoverState({ + ...popoverState, + tabId: tab.id as typeof popoverState['tabId'], + }); + }} + /> + } + /> + + { + trackUiEvent('indexpattern_dimension_removed'); + props.updateAll( + datasourceId, + layerDatasource.removeColumn({ + layerId, + columnId: accessor, + prevState: layerDatasourceState, + }), + activeVisualization.removeDimension({ + layerId, + columnId: accessor, + prevState: props.visualizationState, + }) + ); + }} + /> +
    + ); + })} {group.supportsMoreColumns ? ( ({ field, indexPatternId: indexPattern.id } as DraggedField), [ + field, + indexPattern.id, + ]); return ( ); -} +}); function FieldItemPopoverContents(props: State & FieldItemProps) { const { diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index be74ec352287f..36e8d9660ab70 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -31,6 +31,7 @@ import { ColumnGroups, PieExpressionProps } from './types'; import { getSliceValueWithFallback, getFilterContext } from './render_helpers'; import { EmptyPlaceholder } from '../shared_components'; import './visualization.scss'; +import { desanitizeFilterContext } from '../utils'; const EMPTY_SLICE = Symbol('empty_slice'); @@ -242,7 +243,7 @@ export function PieComponent( firstTable ); - onClickValue(context); + onClickValue(desanitizeFilterContext(context)); }} /> = VisualizationConfigProp setState: (newState: T) => void; }; +export type VisualizationDimensionEditorProps = VisualizationConfigProps & { + groupId: string; + accessor: string; + setState: (newState: T) => void; +}; + export type VisualizationDimensionGroupConfig = SharedDimensionProps & { groupLabel: string; @@ -300,6 +306,12 @@ export type VisualizationDimensionGroupConfig = SharedDimensionProps & { /** If required, a warning will appear if accessors are empty */ required?: boolean; dataTestSubj?: string; + + /** + * When the dimension editor is enabled for this group, all dimensions in the group + * will render the extra tab for the dimension editor + */ + enableDimensionEditor?: boolean; }; interface VisualizationDimensionChangeProps { @@ -459,6 +471,15 @@ export interface Visualization { */ removeDimension: (props: VisualizationDimensionChangeProps) => T; + /** + * Additional editor that gets rendered inside the dimension popover. + * This can be used to configure dimension-specific options + */ + renderDimensionEditor?: ( + domElement: Element, + props: VisualizationDimensionEditorProps + ) => void; + /** * The frame will call this function on all visualizations at different times. The * main use cases where visualization suggestions are requested are: diff --git a/x-pack/plugins/lens/public/utils.test.ts b/x-pack/plugins/lens/public/utils.test.ts new file mode 100644 index 0000000000000..170579b7c551b --- /dev/null +++ b/x-pack/plugins/lens/public/utils.test.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 { LensFilterEvent } from './types'; +import { desanitizeFilterContext } from './utils'; + +describe('desanitizeFilterContext', () => { + it(`When filtered value equals '(empty)' replaces it with '' in table and in value.`, () => { + const table = { + rows: [ + { + 'f903668f-1175-4705-a5bd-713259d10326': 1589414640000, + '5d5446b2-72e8-4f86-91e0-88380f0fa14c': '(empty)', + 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 1, + }, + { + 'f903668f-1175-4705-a5bd-713259d10326': 1589414670000, + 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 0, + }, + { + 'f903668f-1175-4705-a5bd-713259d10326': 1589414880000, + '5d5446b2-72e8-4f86-91e0-88380f0fa14c': '123123123', + 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 1, + }, + { + 'f903668f-1175-4705-a5bd-713259d10326': 1589414910000, + '5d5446b2-72e8-4f86-91e0-88380f0fa14c': '(empty)', + 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 1, + }, + ], + columns: [ + { + id: 'f903668f-1175-4705-a5bd-713259d10326', + name: 'order_date per 30 seconds', + }, + { + id: '5d5446b2-72e8-4f86-91e0-88380f0fa14c', + name: 'Top values of customer_phone', + }, + { + id: '9f0b6f88-c399-43a0-a993-0ad943c9af25', + name: 'Count of records', + }, + ], + }; + + const contextWithEmptyValue: LensFilterEvent['data'] = { + data: [ + { + row: 3, + column: 0, + value: 1589414910000, + table, + }, + { + row: 0, + column: 1, + value: '(empty)', + table, + }, + ], + timeFieldName: 'order_date', + }; + + const desanitizedFilterContext = desanitizeFilterContext(contextWithEmptyValue); + + expect(desanitizedFilterContext).toEqual({ + data: [ + { + row: 3, + column: 0, + value: 1589414910000, + table, + }, + { + value: '', + row: 0, + column: 1, + table: { + rows: [ + { + 'f903668f-1175-4705-a5bd-713259d10326': 1589414640000, + '5d5446b2-72e8-4f86-91e0-88380f0fa14c': '', + 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 1, + }, + { + 'f903668f-1175-4705-a5bd-713259d10326': 1589414670000, + 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 0, + }, + { + 'f903668f-1175-4705-a5bd-713259d10326': 1589414880000, + '5d5446b2-72e8-4f86-91e0-88380f0fa14c': '123123123', + 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 1, + }, + { + 'f903668f-1175-4705-a5bd-713259d10326': 1589414910000, + '5d5446b2-72e8-4f86-91e0-88380f0fa14c': '(empty)', + 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 1, + }, + ], + columns: table.columns, + }, + }, + ], + timeFieldName: 'order_date', + }); + }); +}); diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts new file mode 100644 index 0000000000000..171707dcb9d26 --- /dev/null +++ b/x-pack/plugins/lens/public/utils.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { LensFilterEvent } from './types'; + +/** replaces the value `(empty) to empty string for proper filtering` */ +export const desanitizeFilterContext = ( + context: LensFilterEvent['data'] +): LensFilterEvent['data'] => { + const emptyTextValue = i18n.translate('xpack.lens.indexpattern.emptyTextColumnValue', { + defaultMessage: '(empty)', + }); + return { + ...context, + data: context.data.map((point) => + point.value === emptyTextValue + ? { + ...point, + value: '', + table: { + ...point.table, + rows: point.table.rows.map((row, index) => + index === point.row + ? { + ...row, + [point.table.columns[point.column].id]: '', + } + : row + ), + }, + } + : point + ), + }; +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx index 4ad2b2f22c98a..003036b211f03 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx @@ -39,6 +39,7 @@ import { VisualizationContainer } from '../visualization_container'; import { isHorizontalChart } from './state_helpers'; import { parseInterval } from '../../../../../src/plugins/data/common'; import { EmptyPlaceholder } from '../shared_components'; +import { desanitizeFilterContext } from '../utils'; type InferPropType = T extends React.FunctionComponent ? P : T; type SeriesSpec = InferPropType & @@ -354,7 +355,7 @@ export function XYChart({ })), timeFieldName, }; - onClickValue(context); + onClickValue(desanitizeFilterContext(context)); }} /> diff --git a/x-pack/plugins/lens/server/usage/task.ts b/x-pack/plugins/lens/server/usage/task.ts index 5a5d26fa2afde..cde6e7eb6c090 100644 --- a/x-pack/plugins/lens/server/usage/task.ts +++ b/x-pack/plugins/lens/server/usage/task.ts @@ -6,6 +6,7 @@ import { APICaller, CoreSetup, Logger } from 'kibana/server'; import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; import moment from 'moment'; import { RunContext, @@ -191,7 +192,7 @@ export function telemetryTaskRunner( return { async run() { - const kibanaIndex = (await config.toPromise()).kibana.index; + const kibanaIndex = (await config.pipe(first()).toPromise()).kibana.index; return Promise.all([ getDailyEvents(kibanaIndex, callCluster), diff --git a/x-pack/plugins/licensing/server/plugin.ts b/x-pack/plugins/licensing/server/plugin.ts index 33f70c549914d..e1aa4a1b32517 100644 --- a/x-pack/plugins/licensing/server/plugin.ts +++ b/x-pack/plugins/licensing/server/plugin.ts @@ -107,7 +107,7 @@ export class LicensingPlugin implements Plugin ): ReturnType { const [coreStart] = await core.getStartServices(); - const client = coreStart.elasticsearch.legacy.client; - return await client.asScoped(request).callAsCurrentUser(...args); + const _client = coreStart.elasticsearch.legacy.client; + return await _client.asScoped(request).callAsCurrentUser(...args); }, callAsInternalUser, }; @@ -124,7 +124,7 @@ export class LicensingPlugin implements Plugin { const [, , { featureUsage }] = await getStartServices(); return response.ok({ - body: [...featureUsage.getLastUsages().entries()].reduce( - (res, [featureName, lastUsage]) => { - return { - ...res, - [featureName]: new Date(lastUsage).toISOString(), - }; - }, - {} - ), + body: { + features: featureUsage.getLastUsages().map((usage) => ({ + name: usage.name, + last_used: usage.lastUsed, + license_level: usage.licenseType, + })), + }, }); } ); diff --git a/x-pack/plugins/licensing/server/services/feature_usage_service.test.ts b/x-pack/plugins/licensing/server/services/feature_usage_service.test.ts index f0ef0dbec0b22..39f7aa6503b35 100644 --- a/x-pack/plugins/licensing/server/services/feature_usage_service.test.ts +++ b/x-pack/plugins/licensing/server/services/feature_usage_service.test.ts @@ -17,16 +17,13 @@ describe('FeatureUsageService', () => { jest.restoreAllMocks(); }); - const toObj = (map: ReadonlyMap): Record => - Object.fromEntries(map.entries()); - describe('#setup', () => { describe('#register', () => { it('throws when registering the same feature twice', () => { const setup = service.setup(); - setup.register('foo'); + setup.register('foo', 'basic'); expect(() => { - setup.register('foo'); + setup.register('foo', 'basic'); }).toThrowErrorMatchingInlineSnapshot(`"Feature 'foo' has already been registered."`); }); }); @@ -36,32 +33,50 @@ describe('FeatureUsageService', () => { describe('#notifyUsage', () => { it('allows to notify a feature usage', () => { const setup = service.setup(); - setup.register('feature'); + setup.register('feature', 'basic'); const start = service.start(); start.notifyUsage('feature', 127001); - expect(start.getLastUsages().get('feature')).toBe(127001); + expect(start.getLastUsages()).toEqual([ + { + lastUsed: new Date(127001), + licenseType: 'basic', + name: 'feature', + }, + ]); }); it('can receive a Date object', () => { const setup = service.setup(); - setup.register('feature'); + setup.register('feature', 'basic'); const start = service.start(); const usageTime = new Date(2015, 9, 21, 17, 54, 12); start.notifyUsage('feature', usageTime); - expect(start.getLastUsages().get('feature')).toBe(usageTime.getTime()); + expect(start.getLastUsages()).toEqual([ + { + lastUsed: usageTime, + licenseType: 'basic', + name: 'feature', + }, + ]); }); it('uses the current time when `usedAt` is unspecified', () => { jest.spyOn(Date, 'now').mockReturnValue(42); const setup = service.setup(); - setup.register('feature'); + setup.register('feature', 'basic'); const start = service.start(); start.notifyUsage('feature'); - expect(start.getLastUsages().get('feature')).toBe(42); + expect(start.getLastUsages()).toEqual([ + { + lastUsed: new Date(42), + licenseType: 'basic', + name: 'feature', + }, + ]); }); it('throws when notifying for an unregistered feature', () => { @@ -76,40 +91,41 @@ describe('FeatureUsageService', () => { describe('#getLastUsages', () => { it('returns the last usage for all used features', () => { const setup = service.setup(); - setup.register('featureA'); - setup.register('featureB'); + setup.register('featureA', 'basic'); + setup.register('featureB', 'gold'); const start = service.start(); start.notifyUsage('featureA', 127001); start.notifyUsage('featureB', 6666); - expect(toObj(start.getLastUsages())).toEqual({ - featureA: 127001, - featureB: 6666, - }); + expect(start.getLastUsages()).toEqual([ + { lastUsed: new Date(127001), licenseType: 'basic', name: 'featureA' }, + { lastUsed: new Date(6666), licenseType: 'gold', name: 'featureB' }, + ]); }); it('returns the last usage even after notifying for an older usage', () => { const setup = service.setup(); - setup.register('featureA'); + setup.register('featureA', 'basic'); const start = service.start(); start.notifyUsage('featureA', 1000); start.notifyUsage('featureA', 500); - expect(toObj(start.getLastUsages())).toEqual({ - featureA: 1000, - }); + expect(start.getLastUsages()).toEqual([ + { lastUsed: new Date(1000), licenseType: 'basic', name: 'featureA' }, + ]); }); - it('does not return entries for unused registered features', () => { + it('returns entries for unused registered features', () => { const setup = service.setup(); - setup.register('featureA'); - setup.register('featureB'); + setup.register('featureA', 'basic'); + setup.register('featureB', 'gold'); const start = service.start(); start.notifyUsage('featureA', 127001); - expect(toObj(start.getLastUsages())).toEqual({ - featureA: 127001, - }); + expect(start.getLastUsages()).toEqual([ + { lastUsed: new Date(127001), licenseType: 'basic', name: 'featureA' }, + { lastUsed: null, licenseType: 'gold', name: 'featureB' }, + ]); }); }); }); diff --git a/x-pack/plugins/licensing/server/services/feature_usage_service.ts b/x-pack/plugins/licensing/server/services/feature_usage_service.ts index 0c6613d37f63a..9bfcb28f36b2a 100644 --- a/x-pack/plugins/licensing/server/services/feature_usage_service.ts +++ b/x-pack/plugins/licensing/server/services/feature_usage_service.ts @@ -5,13 +5,20 @@ */ import { isDate } from 'lodash'; +import { LicenseType } from '../../common/types'; /** @public */ export interface FeatureUsageServiceSetup { /** * Register a feature to be able to notify of it's usages using the {@link FeatureUsageServiceStart | service start contract}. */ - register(featureName: string): void; + register(featureName: string, licenseType: LicenseType): void; +} + +export interface LastFeatureUsage { + name: string; + lastUsed: Date | null; + licenseType: LicenseType; } /** @public */ @@ -27,20 +34,23 @@ export interface FeatureUsageServiceStart { * Return a map containing last usage timestamp for all features. * Features that were not used yet do not appear in the map. */ - getLastUsages(): ReadonlyMap; + getLastUsages(): LastFeatureUsage[]; } export class FeatureUsageService { - private readonly features: string[] = []; - private readonly lastUsages = new Map(); + private readonly lastUsages = new Map(); public setup(): FeatureUsageServiceSetup { return { - register: (featureName) => { - if (this.features.includes(featureName)) { + register: (featureName, licenseType) => { + if (this.lastUsages.has(featureName)) { throw new Error(`Feature '${featureName}' has already been registered.`); } - this.features.push(featureName); + this.lastUsages.set(featureName, { + name: featureName, + lastUsed: null, + licenseType, + }); }, }; } @@ -48,16 +58,17 @@ export class FeatureUsageService { public start(): FeatureUsageServiceStart { return { notifyUsage: (featureName, usedAt = Date.now()) => { - if (!this.features.includes(featureName)) { + const usage = this.lastUsages.get(featureName); + if (!usage) { throw new Error(`Feature '${featureName}' is not registered.`); } - if (isDate(usedAt)) { - usedAt = usedAt.getTime(); + + const lastUsed = isDate(usedAt) ? usedAt : new Date(usedAt); + if (usage.lastUsed == null || lastUsed > usage.lastUsed) { + usage.lastUsed = lastUsed; } - const currentValue = this.lastUsages.get(featureName) ?? 0; - this.lastUsages.set(featureName, Math.max(usedAt, currentValue)); }, - getLastUsages: () => new Map(this.lastUsages.entries()), + getLastUsages: () => Array.from(this.lastUsages.values()), }; } } diff --git a/x-pack/plugins/lists/README.md b/x-pack/plugins/lists/README.md new file mode 100644 index 0000000000000..cb343c95b0103 --- /dev/null +++ b/x-pack/plugins/lists/README.md @@ -0,0 +1,254 @@ +README.md for developers working on the backend lists on how to get started +using the CURL scripts in the scripts folder. + +The scripts rely on CURL and jq: + +- [CURL](https://curl.haxx.se) +- [jq](https://stedolan.github.io/jq/) + +Install curl and jq (mac instructions) + +```sh +brew update +brew install curl +brew install jq +``` + +Open `$HOME/.zshrc` or `${HOME}.bashrc` depending on your SHELL output from `echo $SHELL` +and add these environment variables: + +```sh +export ELASTICSEARCH_USERNAME=${user} +export ELASTICSEARCH_PASSWORD=${password} +export ELASTICSEARCH_URL=https://${ip}:9200 +export KIBANA_URL=http://localhost:5601 +export TASK_MANAGER_INDEX=.kibana-task-manager-${your user id} +export KIBANA_INDEX=.kibana-${your user id} +``` + +source `$HOME/.zshrc` or `${HOME}.bashrc` to ensure variables are set: + +```sh +source ~/.zshrc +``` + +Open your `kibana.dev.yml` file and add these lines: + +```sh +# Enable lists feature +xpack.lists.enabled: true +xpack.lists.listIndex: '.lists-frank' +xpack.lists.listItemIndex: '.items-frank' +``` + +Restart Kibana and ensure that you are using `--no-base-path` as changing the base path is a feature but will +get in the way of the CURL scripts written as is. + +Go to the scripts folder `cd kibana/x-pack/plugins/lists/server/scripts` and run: + +```sh +./hard_reset.sh +./post_list.sh +``` + +which will: + +- Delete any existing lists you have +- Delete any existing list items you have +- Delete any existing exception lists you have +- Delete any existing exception list items you have +- Delete any existing mapping, policies, and templates, you might have previously had. +- Add the latest list and list item index and its mappings using your settings from `kibana.dev.yml` environment variable of `xpack.lists.listIndex` and `xpack.lists.listItemIndex`. +- Posts the sample list from `./lists/new/list_ip.json` + +Now you can run + +```sh +./post_list.sh +``` + +You should see the new list created like so: + +```sh +{ + "id": "list-ip", + "created_at": "2020-05-28T19:15:22.344Z", + "created_by": "yo", + "description": "This list describes bad internet ip", + "name": "Simple list with an ip", + "tie_breaker_id": "c57efbc4-4977-4a32-995f-cfd296bed521", + "type": "ip", + "updated_at": "2020-05-28T19:15:22.344Z", + "updated_by": "yo" +} +``` + +You can add a list item like so: + +```sh + ./post_list_item.sh +``` + +You should see the new list item created and attached to the above list like so: + +```sh +{ + "id": "hand_inserted_item_id", + "type": "ip", + "value": "127.0.0.1", + "created_at": "2020-05-28T19:15:49.790Z", + "created_by": "yo", + "list_id": "list-ip", + "tie_breaker_id": "a881bf2e-1e17-4592-bba8-d567cb07d234", + "updated_at": "2020-05-28T19:15:49.790Z", + "updated_by": "yo" +} +``` + +If you want to post an exception list it would be like so: + +```sh +./post_exception_list.sh +``` + +You should see the new exception list created like so: + +```sh +{ + "_tags": [ + "endpoint", + "process", + "malware", + "os:linux" + ], + "created_at": "2020-05-28T19:16:31.052Z", + "created_by": "yo", + "description": "This is a sample endpoint type exception", + "id": "bcb94680-a117-11ea-ad9d-c71f4820e65b", + "list_id": "endpoint_list", + "name": "Sample Endpoint Exception List", + "namespace_type": "single", + "tags": [ + "user added string for a tag", + "malware" + ], + "tie_breaker_id": "86e08c8c-c970-4b08-a6e2-cdba7bb4e023", + "type": "endpoint", + "updated_at": "2020-05-28T19:16:31.080Z", + "updated_by": "yo" +} +``` + +And you can attach exception list items like so: + +```ts +{ + "_tags": [ + "endpoint", + "process", + "malware", + "os:linux" + ], + "comment": [], + "created_at": "2020-05-28T19:17:21.099Z", + "created_by": "yo", + "description": "This is a sample endpoint type exception", + "entries": [ + { + "field": "actingProcess.file.signer", + "operator": "included", + "match": "Elastic, N.V." + }, + { + "field": "event.category", + "operator": "included", + "match_any": [ + "process", + "malware" + ] + } + ], + "id": "da8d3b30-a117-11ea-ad9d-c71f4820e65b", + "item_id": "endpoint_list_item", + "list_id": "endpoint_list", + "name": "Sample Endpoint Exception List", + "namespace_type": "single", + "tags": [ + "user added string for a tag", + "malware" + ], + "tie_breaker_id": "21f84703-9476-4af8-a212-aad31e18dcb9", + "type": "simple", + "updated_at": "2020-05-28T19:17:21.123Z", + "updated_by": "yo" +} +``` + +You can then do find for each one like so: + +```sh +./find_lists.sh +``` + +```sh +{ + "cursor": "WzIwLFsiYzU3ZWZiYzQtNDk3Ny00YTMyLTk5NWYtY2ZkMjk2YmVkNTIxIl1d", + "data": [ + { + "id": "list-ip", + "created_at": "2020-05-28T19:15:22.344Z", + "created_by": "yo", + "description": "This list describes bad internet ip", + "name": "Simple list with an ip", + "tie_breaker_id": "c57efbc4-4977-4a32-995f-cfd296bed521", + "type": "ip", + "updated_at": "2020-05-28T19:15:22.344Z", + "updated_by": "yo" + } + ], + "page": 1, + "per_page": 20, + "total": 1 +} +``` + +or for finding exception lists: + +```sh +./find_exception_lists.sh +``` + +```sh +{ + "data": [ + { + "_tags": [ + "endpoint", + "process", + "malware", + "os:linux" + ], + "created_at": "2020-05-28T19:16:31.052Z", + "created_by": "yo", + "description": "This is a sample endpoint type exception", + "id": "bcb94680-a117-11ea-ad9d-c71f4820e65b", + "list_id": "endpoint_list", + "name": "Sample Endpoint Exception List", + "namespace_type": "single", + "tags": [ + "user added string for a tag", + "malware" + ], + "tie_breaker_id": "86e08c8c-c970-4b08-a6e2-cdba7bb4e023", + "type": "endpoint", + "updated_at": "2020-05-28T19:16:31.080Z", + "updated_by": "yo" + } + ], + "page": 1, + "per_page": 20, + "total": 1 +} +``` + +See the full scripts folder for all the capabilities. diff --git a/x-pack/plugins/lists/common/constants.mock.ts b/x-pack/plugins/lists/common/constants.mock.ts index d174211f348ea..d8e4dfba1599e 100644 --- a/x-pack/plugins/lists/common/constants.mock.ts +++ b/x-pack/plugins/lists/common/constants.mock.ts @@ -28,3 +28,14 @@ export const META = {}; export const TYPE = 'ip'; export const VALUE = '127.0.0.1'; export const VALUE_2 = '255.255.255'; +export const NAMESPACE_TYPE = 'single'; + +// Exception List specific +export const ENDPOINT_TYPE = 'endpoint'; +export const ENTRIES = [ + { field: 'some.field', match: 'some value', match_any: undefined, operator: 'included' }, +]; +export const ITEM_TYPE = 'simple'; +export const _TAGS = []; +export const TAGS = []; +export const COMMENT = []; diff --git a/x-pack/plugins/lists/common/constants.ts b/x-pack/plugins/lists/common/constants.ts index 96d28bf618ce4..6cb88b19483ce 100644 --- a/x-pack/plugins/lists/common/constants.ts +++ b/x-pack/plugins/lists/common/constants.ts @@ -16,3 +16,9 @@ export const LIST_ITEM_URL = `${LIST_URL}/items`; */ export const EXCEPTION_LIST_URL = '/api/exception_lists'; export const EXCEPTION_LIST_ITEM_URL = '/api/exception_lists/items'; + +/** + * Exception list spaces + */ +export const EXCEPTION_LIST_NAMESPACE_AGNOSTIC = 'exception-list-agnostic'; +export const EXCEPTION_LIST_NAMESPACE = 'exception-list'; diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.ts b/x-pack/plugins/lists/common/schemas/common/schemas.ts index cd69685ffcb1b..14ae030c63df3 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.ts @@ -9,6 +9,7 @@ import * as t from 'io-ts'; import { DefaultStringArray, NonEmptyString } from '../types'; +import { DefaultNamespace } from '../types/default_namespace'; export const name = t.string; export type Name = t.TypeOf; @@ -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.mock.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.mock.ts new file mode 100644 index 0000000000000..f9af10245b7ee --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.mock.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 { + COMMENT, + DESCRIPTION, + ENTRIES, + ITEM_TYPE, + LIST_ID, + META, + NAME, + NAMESPACE_TYPE, + TAGS, + _TAGS, +} from '../../constants.mock'; + +import { CreateExceptionListItemSchema } from './create_exception_list_item_schema'; + +export const getCreateExceptionListItemSchemaMock = (): CreateExceptionListItemSchema => ({ + _tags: _TAGS, + comment: COMMENT, + description: DESCRIPTION, + entries: ENTRIES, + item_id: undefined, + list_id: LIST_ID, + meta: META, + name: NAME, + namespace_type: NAMESPACE_TYPE, + tags: TAGS, + type: ITEM_TYPE, +}); 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.mock.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts new file mode 100644 index 0000000000000..901715b601b80 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts @@ -0,0 +1,40 @@ +/* + * 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 { ExceptionListItemSchema } from './exception_list_item_schema'; + +export const getExceptionListItemSchemaMock = (): ExceptionListItemSchema => ({ + _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.', + match_any: undefined, + operator: 'included', + }, + { + field: 'event.category', + match: undefined, + 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', +}); 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.mock.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts new file mode 100644 index 0000000000000..017b959a2baf3 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.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 { ExceptionListSchema } from './exception_list_schema'; + +export const getExceptionListSchemaMock = (): ExceptionListSchema => ({ + _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', +}); 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_exception_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.mock.ts new file mode 100644 index 0000000000000..f760e602605ba --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.mock.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 { getExceptionListItemSchemaMock } from './exception_list_item_schema.mock'; +import { FoundExceptionListItemSchema } from './found_exception_list_item_schema'; + +export const getFoundExceptionListItemSchemaMock = (): FoundExceptionListItemSchema => ({ + data: [getExceptionListItemSchemaMock()], + page: 1, + per_page: 1, + total: 1, +}); diff --git a/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.mock.ts new file mode 100644 index 0000000000000..ce71a27dbc4d4 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/found_exception_list_schema.mock.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 { getExceptionListSchemaMock } from './exception_list_schema.mock'; +import { FoundExceptionListSchema } from './found_exception_list_schema'; + +export const getFoundExceptionListSchemaMock = (): FoundExceptionListSchema => ({ + data: [getExceptionListSchemaMock()], + page: 1, + per_page: 1, + total: 1, +}); diff --git a/x-pack/plugins/lists/common/schemas/response/found_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/found_list_item_schema.mock.ts new file mode 100644 index 0000000000000..e96188c619d78 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/found_list_item_schema.mock.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FoundListItemSchema } from './found_list_item_schema'; +import { getListItemResponseMock } from './list_item_schema.mock'; + +export const getFoundListItemSchemaMock = (): FoundListItemSchema => ({ + cursor: '123', + data: [getListItemResponseMock()], + page: 1, + per_page: 1, + total: 1, +}); 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.mock.ts b/x-pack/plugins/lists/common/schemas/response/found_list_schema.mock.ts new file mode 100644 index 0000000000000..63d6a3b220ac1 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/found_list_schema.mock.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FoundListSchema } from './found_list_schema'; +import { getListResponseMock } from './list_schema.mock'; + +export const getFoundListSchemaMock = (): FoundListSchema => ({ + cursor: '123', + data: [getListResponseMock()], + page: 1, + per_page: 1, + total: 1, +}); 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/__mocks__/api.ts b/x-pack/plugins/lists/public/exceptions/__mocks__/api.ts index f624189915dcf..ecc771279b3ab 100644 --- a/x-pack/plugins/lists/public/exceptions/__mocks__/api.ts +++ b/x-pack/plugins/lists/public/exceptions/__mocks__/api.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock'; +import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock'; import { ExceptionListItemSchema, ExceptionListSchema, @@ -15,37 +17,40 @@ import { ApiCallByIdProps, ApiCallByListIdProps, } from '../types'; -import { mockExceptionItem, mockExceptionList } from '../mock'; /* eslint-disable @typescript-eslint/no-unused-vars */ export const addExceptionList = async ({ http, list, signal, -}: AddExceptionListProps): Promise => Promise.resolve(mockExceptionList); +}: AddExceptionListProps): Promise => + Promise.resolve(getExceptionListSchemaMock()); export const addExceptionListItem = async ({ http, listItem, signal, }: AddExceptionListItemProps): Promise => - Promise.resolve(mockExceptionItem); + Promise.resolve(getExceptionListItemSchemaMock()); export const fetchExceptionListById = async ({ http, id, signal, -}: ApiCallByIdProps): Promise => Promise.resolve(mockExceptionList); +}: ApiCallByIdProps): Promise => Promise.resolve(getExceptionListSchemaMock()); export const fetchExceptionListItemsByListId = async ({ + filterOptions, http, listId, + pagination, signal, }: ApiCallByListIdProps): Promise => - Promise.resolve({ data: [mockExceptionItem], page: 1, per_page: 20, total: 1 }); + Promise.resolve({ data: [getExceptionListItemSchemaMock()], page: 1, per_page: 20, total: 1 }); export const fetchExceptionListItemById = async ({ http, id, signal, -}: ApiCallByIdProps): Promise => Promise.resolve(mockExceptionItem); +}: ApiCallByIdProps): Promise => + Promise.resolve(getExceptionListItemSchemaMock()); diff --git a/x-pack/plugins/lists/public/exceptions/api.test.ts b/x-pack/plugins/lists/public/exceptions/api.test.ts index cc172ee1e6109..b9512bb398745 100644 --- a/x-pack/plugins/lists/public/exceptions/api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/api.test.ts @@ -4,13 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { createKibanaCoreStartMock } from '../common/mocks/kibana_core'; +import { getExceptionListSchemaMock } from '../../common/schemas/response/exception_list_schema.mock'; +import { getExceptionListItemSchemaMock } from '../../common/schemas/response/exception_list_item_schema.mock'; +import { getCreateExceptionListSchemaMock } from '../../common/schemas/request/create_exception_list_schema.mock'; +import { getCreateExceptionListItemSchemaMock } from '../../common/schemas/request/create_exception_list_item_schema.mock'; -import { - mockExceptionItem, - mockExceptionList, - mockNewExceptionItem, - mockNewExceptionList, -} from './mock'; import { addExceptionList, addExceptionListItem, @@ -40,246 +38,355 @@ const mockKibanaHttpService = ((createKibanaCoreStartMock() as unknown) as jest. ); describe('Exceptions Lists API', () => { - describe('addExceptionList', () => { + describe('#addExceptionList', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue(mockExceptionList); + fetchMock.mockResolvedValue(getExceptionListSchemaMock()); }); - test('check parameter url, body', async () => { - await addExceptionList({ + test('it uses POST when "list.id" does not exist', async () => { + const payload = getCreateExceptionListSchemaMock(); + const exceptionResponse = await addExceptionList({ http: mockKibanaHttpService(), - list: mockNewExceptionList, + list: payload, signal: abortCtrl.signal, }); + expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', { - body: - '{"_tags":["endpoint","process","malware","os:linux"],"description":"This is a sample endpoint type exception","list_id":"endpoint_list","name":"Sample Endpoint Exception List","tags":["user added string for a tag","malware"],"type":"endpoint"}', + body: JSON.stringify(payload), method: 'POST', signal: abortCtrl.signal, }); + expect(exceptionResponse).toEqual({ id: '1', ...getExceptionListSchemaMock() }); }); - test('check parameter url, body when "list.id" exists', async () => { - await addExceptionList({ + test('it uses PUT when "list.id" exists', async () => { + const payload = getExceptionListSchemaMock(); + const exceptionResponse = await addExceptionList({ http: mockKibanaHttpService(), - list: mockExceptionList, + list: getExceptionListSchemaMock(), signal: abortCtrl.signal, }); + 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"}', + body: JSON.stringify(payload), method: 'PUT', signal: abortCtrl.signal, }); - }); - - test('happy path', async () => { - const exceptionResponse = await addExceptionList({ - http: mockKibanaHttpService(), - list: mockNewExceptionList, - signal: abortCtrl.signal, - }); - expect(exceptionResponse).toEqual(mockExceptionList); + expect(exceptionResponse).toEqual(getExceptionListSchemaMock()); }); }); - describe('addExceptionListItem', () => { + describe('#addExceptionListItem', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue(mockExceptionItem); + fetchMock.mockResolvedValue(getExceptionListItemSchemaMock()); }); - test('check parameter url, body', async () => { - await addExceptionListItem({ + test('it uses POST when "listItem.id" does not exist', async () => { + const payload = getCreateExceptionListItemSchemaMock(); + const exceptionResponse = await addExceptionListItem({ http: mockKibanaHttpService(), - listItem: mockNewExceptionItem, + listItem: payload, signal: abortCtrl.signal, }); + expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', { - body: - '{"_tags":["endpoint","process","malware","os:linux"],"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"}],"item_id":"endpoint_list_item","list_id":"endpoint_list","name":"Sample Endpoint Exception List","tags":["user added string for a tag","malware"],"type":"simple"}', + body: JSON.stringify(payload), method: 'POST', signal: abortCtrl.signal, }); + expect(exceptionResponse).toEqual(getExceptionListItemSchemaMock()); }); test('check parameter url, body when "listItem.id" exists', async () => { - await addExceptionListItem({ + const payload = getExceptionListItemSchemaMock(); + const exceptionResponse = await addExceptionListItem({ http: mockKibanaHttpService(), - listItem: mockExceptionItem, + listItem: getExceptionListItemSchemaMock(), signal: abortCtrl.signal, }); + 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"}', + body: JSON.stringify(payload), method: 'PUT', signal: abortCtrl.signal, }); - }); - - test('happy path', async () => { - const exceptionResponse = await addExceptionListItem({ - http: mockKibanaHttpService(), - listItem: mockNewExceptionItem, - signal: abortCtrl.signal, - }); - expect(exceptionResponse).toEqual(mockExceptionItem); + expect(exceptionResponse).toEqual(getExceptionListItemSchemaMock()); }); }); - describe('fetchExceptionListById', () => { + describe('#fetchExceptionListById', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue(mockExceptionList); + fetchMock.mockResolvedValue(getExceptionListSchemaMock()); }); - test('check parameter url, body', async () => { + test('it invokes "fetchExceptionListById" with expected url and body values', async () => { await fetchExceptionListById({ http: mockKibanaHttpService(), id: '1', + namespaceType: 'single', signal: abortCtrl.signal, }); expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', { method: 'GET', query: { id: '1', + namespace_type: 'single', }, signal: abortCtrl.signal, }); }); - test('happy path', async () => { + test('it returns expected exception list on success', async () => { const exceptionResponse = await fetchExceptionListById({ http: mockKibanaHttpService(), id: '1', + namespaceType: 'single', signal: abortCtrl.signal, }); - expect(exceptionResponse).toEqual(mockExceptionList); + expect(exceptionResponse).toEqual(getExceptionListSchemaMock()); }); }); - describe('fetchExceptionListItemsByListId', () => { + describe('#fetchExceptionListItemsByListId', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue([mockNewExceptionItem]); + fetchMock.mockResolvedValue([getExceptionListItemSchemaMock()]); }); - test('check parameter url, body', async () => { + test('it invokes "fetchExceptionListItemsByListId" with expected url and body values', async () => { await fetchExceptionListItemsByListId({ http: mockKibanaHttpService(), - listId: 'endpoint_list', + listId: 'myList', + namespaceType: 'single', + signal: abortCtrl.signal, + }); + + expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', { + method: 'GET', + query: { + list_id: 'myList', + namespace_type: 'single', + page: 1, + per_page: 20, + }, + signal: abortCtrl.signal, + }); + }); + + test('it invokes with expected url and body values when a filter exists and "namespaceType" of "single"', async () => { + await fetchExceptionListItemsByListId({ + filterOptions: { + filter: 'hello world', + tags: [], + }, + http: mockKibanaHttpService(), + listId: 'myList', + namespaceType: 'single', signal: abortCtrl.signal, }); + + expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', { + method: 'GET', + query: { + filter: 'exception-list.attributes.entries.field:hello world*', + list_id: 'myList', + namespace_type: 'single', + page: 1, + per_page: 20, + }, + signal: abortCtrl.signal, + }); + }); + + test('it invokes with expected url and body values when a filter exists and "namespaceType" of "agnostic"', async () => { + await fetchExceptionListItemsByListId({ + filterOptions: { + filter: 'hello world', + tags: [], + }, + http: mockKibanaHttpService(), + listId: 'myList', + namespaceType: 'agnostic', + signal: abortCtrl.signal, + }); + + expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', { + method: 'GET', + query: { + filter: 'exception-list-agnostic.attributes.entries.field:hello world*', + list_id: 'myList', + namespace_type: 'agnostic', + page: 1, + per_page: 20, + }, + signal: abortCtrl.signal, + }); + }); + + test('it invokes with expected url and body values when tags exists', async () => { + await fetchExceptionListItemsByListId({ + filterOptions: { + filter: '', + tags: ['malware'], + }, + http: mockKibanaHttpService(), + listId: 'myList', + namespaceType: 'agnostic', + signal: abortCtrl.signal, + }); + + expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', { + method: 'GET', + query: { + filter: 'exception-list-agnostic.attributes.tags:malware', + list_id: 'myList', + namespace_type: 'agnostic', + page: 1, + per_page: 20, + }, + signal: abortCtrl.signal, + }); + }); + + test('it invokes with expected url and body values when filter and tags exists', async () => { + await fetchExceptionListItemsByListId({ + filterOptions: { + filter: 'host.name', + tags: ['malware'], + }, + http: mockKibanaHttpService(), + listId: 'myList', + namespaceType: 'agnostic', + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', { method: 'GET', query: { - list_id: 'endpoint_list', + filter: + 'exception-list-agnostic.attributes.entries.field:host.name* AND exception-list-agnostic.attributes.tags:malware', + list_id: 'myList', + namespace_type: 'agnostic', + page: 1, + per_page: 20, }, signal: abortCtrl.signal, }); }); - test('happy path', async () => { + test('it returns expected format when call succeeds', async () => { const exceptionResponse = await fetchExceptionListItemsByListId({ http: mockKibanaHttpService(), listId: 'endpoint_list', + namespaceType: 'single', signal: abortCtrl.signal, }); - expect(exceptionResponse).toEqual([mockNewExceptionItem]); + expect(exceptionResponse).toEqual([getExceptionListItemSchemaMock()]); }); }); - describe('fetchExceptionListItemById', () => { + describe('#fetchExceptionListItemById', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue([mockNewExceptionItem]); + fetchMock.mockResolvedValue([getExceptionListItemSchemaMock()]); }); - test('check parameter url, body', async () => { + test('it invokes "fetchExceptionListItemById" with expected url and body values', async () => { await fetchExceptionListItemById({ http: mockKibanaHttpService(), id: '1', + namespaceType: 'single', signal: abortCtrl.signal, }); expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', { method: 'GET', query: { id: '1', + namespace_type: 'single', }, signal: abortCtrl.signal, }); }); - test('happy path', async () => { + test('it returns expected format when call succeeds', async () => { const exceptionResponse = await fetchExceptionListItemById({ http: mockKibanaHttpService(), id: '1', + namespaceType: 'single', signal: abortCtrl.signal, }); - expect(exceptionResponse).toEqual([mockNewExceptionItem]); + expect(exceptionResponse).toEqual([getExceptionListItemSchemaMock()]); }); }); - describe('deleteExceptionListById', () => { + describe('#deleteExceptionListById', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue(mockExceptionList); + fetchMock.mockResolvedValue(getExceptionListSchemaMock()); }); test('check parameter url, body when deleting exception item', async () => { await deleteExceptionListById({ http: mockKibanaHttpService(), id: '1', + namespaceType: 'single', signal: abortCtrl.signal, }); expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', { method: 'DELETE', query: { id: '1', + namespace_type: 'single', }, signal: abortCtrl.signal, }); }); - test('happy path', async () => { + test('it returns expected format when call succeeds', async () => { const exceptionResponse = await deleteExceptionListById({ http: mockKibanaHttpService(), id: '1', + namespaceType: 'single', signal: abortCtrl.signal, }); - expect(exceptionResponse).toEqual(mockExceptionList); + expect(exceptionResponse).toEqual(getExceptionListSchemaMock()); }); }); - describe('deleteExceptionListItemById', () => { + describe('#deleteExceptionListItemById', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue(mockExceptionItem); + fetchMock.mockResolvedValue(getExceptionListItemSchemaMock()); }); test('check parameter url, body when deleting exception item', async () => { await deleteExceptionListItemById({ http: mockKibanaHttpService(), id: '1', + namespaceType: 'single', signal: abortCtrl.signal, }); expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', { method: 'DELETE', query: { id: '1', + namespace_type: 'single', }, signal: abortCtrl.signal, }); }); - test('happy path', async () => { + test('it returns expected format when call succeeds', async () => { const exceptionResponse = await deleteExceptionListItemById({ http: mockKibanaHttpService(), id: '1', + namespaceType: 'single', signal: abortCtrl.signal, }); - expect(exceptionResponse).toEqual(mockExceptionItem); + expect(exceptionResponse).toEqual(getExceptionListItemSchemaMock()); }); }); }); diff --git a/x-pack/plugins/lists/public/exceptions/api.ts b/x-pack/plugins/lists/public/exceptions/api.ts index fdd9d62539e06..6968ba5f50e72 100644 --- a/x-pack/plugins/lists/public/exceptions/api.ts +++ b/x-pack/plugins/lists/public/exceptions/api.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '../../common/constants'; +import { + EXCEPTION_LIST_ITEM_URL, + EXCEPTION_LIST_NAMESPACE, + EXCEPTION_LIST_NAMESPACE_AGNOSTIC, + EXCEPTION_LIST_URL, +} from '../../common/constants'; import { ExceptionListItemSchema, ExceptionListSchema, @@ -21,6 +26,7 @@ import { /** * Add provided ExceptionList * + * @param http Kibana http service * @param list exception list to add * @param signal to cancel request * @@ -43,6 +49,7 @@ export const addExceptionList = async ({ /** * Add provided ExceptionListItem * + * @param http Kibana http service * @param listItem exception list item to add * @param signal to cancel request * @@ -65,7 +72,9 @@ export const addExceptionListItem = async ({ /** * Fetch an ExceptionList by providing a ExceptionList ID * + * @param http Kibana http service * @param id ExceptionList ID (not list_id) + * @param namespaceType ExceptionList namespace_type * @param signal to cancel request * * @throws An error if response is not OK @@ -73,18 +82,23 @@ export const addExceptionListItem = async ({ export const fetchExceptionListById = async ({ http, id, + namespaceType, signal, }: ApiCallByIdProps): Promise => http.fetch(`${EXCEPTION_LIST_URL}`, { method: 'GET', - query: { id }, + query: { id, namespace_type: namespaceType }, signal, }); /** * Fetch an ExceptionList's ExceptionItems by providing a ExceptionList list_id * - * @param id ExceptionList list_id (not ID) + * @param http Kibana http service + * @param listId ExceptionList list_id (not ID) + * @param namespaceType ExceptionList namespace_type + * @param filterOptions optional - filter by field or tags + * @param pagination optional * @param signal to cancel request * * @throws An error if response is not OK @@ -92,18 +106,48 @@ export const fetchExceptionListById = async ({ export const fetchExceptionListItemsByListId = async ({ http, listId, + namespaceType, + filterOptions = { + filter: '', + tags: [], + }, + pagination = { + page: 1, + perPage: 20, + total: 0, + }, signal, -}: ApiCallByListIdProps): Promise => - http.fetch(`${EXCEPTION_LIST_ITEM_URL}/_find`, { +}: ApiCallByListIdProps): Promise => { + const namespace = + namespaceType === 'agnostic' ? EXCEPTION_LIST_NAMESPACE_AGNOSTIC : EXCEPTION_LIST_NAMESPACE; + const filters = [ + ...(filterOptions.filter.length + ? [`${namespace}.attributes.entries.field:${filterOptions.filter}*`] + : []), + ...(filterOptions.tags?.map((t) => `${namespace}.attributes.tags:${t}`) ?? []), + ]; + + const query = { + list_id: listId, + namespace_type: namespaceType, + page: pagination.page, + per_page: pagination.perPage, + ...(filters.length ? { filter: filters.join(' AND ') } : {}), + }; + + return http.fetch(`${EXCEPTION_LIST_ITEM_URL}/_find`, { method: 'GET', - query: { list_id: listId }, + query, signal, }); +}; /** * Fetch an ExceptionListItem by providing a ExceptionListItem ID * + * @param http Kibana http service * @param id ExceptionListItem ID (not item_id) + * @param namespaceType ExceptionList namespace_type * @param signal to cancel request * * @throws An error if response is not OK @@ -111,18 +155,21 @@ export const fetchExceptionListItemsByListId = async ({ export const fetchExceptionListItemById = async ({ http, id, + namespaceType, signal, }: ApiCallByIdProps): Promise => http.fetch(`${EXCEPTION_LIST_ITEM_URL}`, { method: 'GET', - query: { id }, + query: { id, namespace_type: namespaceType }, signal, }); /** * Delete an ExceptionList by providing a ExceptionList ID * + * @param http Kibana http service * @param id ExceptionList ID (not list_id) + * @param namespaceType ExceptionList namespace_type * @param signal to cancel request * * @throws An error if response is not OK @@ -130,18 +177,21 @@ export const fetchExceptionListItemById = async ({ export const deleteExceptionListById = async ({ http, id, + namespaceType, signal, }: ApiCallByIdProps): Promise => http.fetch(`${EXCEPTION_LIST_URL}`, { method: 'DELETE', - query: { id }, + query: { id, namespace_type: namespaceType }, signal, }); /** * Delete an ExceptionListItem by providing a ExceptionListItem ID * + * @param http Kibana http service * @param id ExceptionListItem ID (not item_id) + * @param namespaceType ExceptionList namespace_type * @param signal to cancel request * * @throws An error if response is not OK @@ -149,10 +199,11 @@ export const deleteExceptionListById = async ({ export const deleteExceptionListItemById = async ({ http, id, + namespaceType, signal, }: ApiCallByIdProps): Promise => http.fetch(`${EXCEPTION_LIST_ITEM_URL}`, { method: 'DELETE', - query: { id }, + query: { id, namespace_type: namespaceType }, signal, }); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.tsx b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.tsx index 098ee1f81f492..1db18168b11fe 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.tsx +++ b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.tsx @@ -6,8 +6,10 @@ import { act, renderHook } from '@testing-library/react-hooks'; -import { mockExceptionItem } from '../mock'; +import * as api from '../api'; +import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock'; import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core'; +import { PersistHookProps } from '../types'; import { ReturnPersistExceptionItem, usePersistExceptionItem } from './persist_exception_item'; @@ -16,38 +18,66 @@ jest.mock('../api'); const mockKibanaHttpService = createKibanaCoreStartMock().http; describe('usePersistExceptionItem', () => { - test('init', async () => { - const onError = jest.fn(); - const { result } = renderHook(() => + const onError = jest.fn(); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('initializes hook', async () => { + const { result } = renderHook(() => usePersistExceptionItem({ http: mockKibanaHttpService, onError }) ); expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]]); }); - test('saving exception item with isLoading === true', async () => { + test('"isLoading" is "true" when exception item is being saved', async () => { await act(async () => { - const onError = jest.fn(); - const { result, rerender, waitForNextUpdate } = renderHook( - () => usePersistExceptionItem({ http: mockKibanaHttpService, onError }) - ); + const { result, rerender, waitForNextUpdate } = renderHook< + PersistHookProps, + ReturnPersistExceptionItem + >(() => usePersistExceptionItem({ http: mockKibanaHttpService, onError })); + await waitForNextUpdate(); - result.current[1](mockExceptionItem); + result.current[1](getExceptionListItemSchemaMock()); rerender(); + expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]); }); }); - test('saved exception item with isSaved === true', async () => { - const onError = jest.fn(); + test('"isSaved" is "true" when exception item saved successfully', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - usePersistExceptionItem({ http: mockKibanaHttpService, onError }) - ); + const { result, waitForNextUpdate } = renderHook< + PersistHookProps, + ReturnPersistExceptionItem + >(() => usePersistExceptionItem({ http: mockKibanaHttpService, onError })); + await waitForNextUpdate(); - result.current[1](mockExceptionItem); + result.current[1](getExceptionListItemSchemaMock()); await waitForNextUpdate(); + expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]); }); }); + + test('"onError" callback is invoked and "isSaved" is "false" when api call fails', async () => { + const error = new Error('persist rule failed'); + jest.spyOn(api, 'addExceptionListItem').mockRejectedValue(error); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + PersistHookProps, + ReturnPersistExceptionItem + >(() => usePersistExceptionItem({ http: mockKibanaHttpService, onError })); + + await waitForNextUpdate(); + result.current[1](getExceptionListItemSchemaMock()); + await waitForNextUpdate(); + + expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]]); + expect(onError).toHaveBeenCalledWith(error); + }); + }); }); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.tsx b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.tsx index 0ed007e805013..d9fe3a82ac177 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.tsx +++ b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.tsx @@ -19,6 +19,13 @@ export type ReturnPersistExceptionItem = [ Dispatch ]; +/** + * Hook for creating or updating ExceptionListItem + * + * @param http Kibana http service + * @param onError error callback + * + */ export const usePersistExceptionItem = ({ http, onError, diff --git a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.tsx b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.tsx index 5cad95a38dbec..80d6e27043c99 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.tsx +++ b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.tsx @@ -6,8 +6,10 @@ import { act, renderHook } from '@testing-library/react-hooks'; -import { mockExceptionList } from '../mock'; +import * as api from '../api'; +import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock'; import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core'; +import { PersistHookProps } from '../types'; import { ReturnPersistExceptionList, usePersistExceptionList } from './persist_exception_list'; @@ -16,38 +18,63 @@ jest.mock('../api'); const mockKibanaHttpService = createKibanaCoreStartMock().http; describe('usePersistExceptionList', () => { - test('init', async () => { - const onError = jest.fn(); - const { result } = renderHook(() => + const onError = jest.fn(); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('initializes hook', async () => { + const { result } = renderHook(() => usePersistExceptionList({ http: mockKibanaHttpService, onError }) ); expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]]); }); - test('saving exception list with isLoading === true', async () => { - const onError = jest.fn(); + test('"isLoading" is "true" when exception item is being saved', async () => { await act(async () => { - const { result, rerender, waitForNextUpdate } = renderHook( - () => usePersistExceptionList({ http: mockKibanaHttpService, onError }) - ); + const { result, rerender, waitForNextUpdate } = renderHook< + PersistHookProps, + ReturnPersistExceptionList + >(() => usePersistExceptionList({ http: mockKibanaHttpService, onError })); await waitForNextUpdate(); - result.current[1](mockExceptionList); + result.current[1](getExceptionListSchemaMock()); rerender(); + expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]); }); }); - test('saved exception list with isSaved === true', async () => { - const onError = jest.fn(); + test('"isSaved" is "true" when exception item saved successfully', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - usePersistExceptionList({ http: mockKibanaHttpService, onError }) - ); + const { result, waitForNextUpdate } = renderHook< + PersistHookProps, + ReturnPersistExceptionList + >(() => usePersistExceptionList({ http: mockKibanaHttpService, onError })); await waitForNextUpdate(); - result.current[1](mockExceptionList); + result.current[1](getExceptionListSchemaMock()); await waitForNextUpdate(); + expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]); }); }); + + test('"onError" callback is invoked and "isSaved" is "false" when api call fails', async () => { + const error = new Error('persist rule failed'); + jest.spyOn(api, 'addExceptionList').mockRejectedValue(error); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + PersistHookProps, + ReturnPersistExceptionList + >(() => usePersistExceptionList({ http: mockKibanaHttpService, onError })); + await waitForNextUpdate(); + result.current[1](getExceptionListSchemaMock()); + await waitForNextUpdate(); + + expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]]); + expect(onError).toHaveBeenCalledWith(error); + }); + }); }); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.tsx b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.tsx index 45330c9725ae7..5848a17145194 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.tsx +++ b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.tsx @@ -19,6 +19,13 @@ export type ReturnPersistExceptionList = [ Dispatch ]; +/** + * Hook for creating or updating ExceptionList + * + * @param http Kibana http service + * @param onError error callback + * + */ export const usePersistExceptionList = ({ http, onError, 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..a6a25ab4d4e9d 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 @@ -8,6 +8,9 @@ import { act, renderHook } from '@testing-library/react-hooks'; import * as api from '../api'; import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core'; +import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock'; +import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock'; +import { ExceptionListAndItems, UseExceptionListProps } from '../types'; import { ReturnExceptionListAndItems, useExceptionList } from './use_exception_list'; @@ -16,101 +19,166 @@ jest.mock('../api'); const mockKibanaHttpService = createKibanaCoreStartMock().http; describe('useExceptionList', () => { - test('init', async () => { - const onError = jest.fn(); + const onErrorMock = jest.fn(); + + afterEach(() => { + onErrorMock.mockClear(); + jest.clearAllMocks(); + }); + + test('initializes hook', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useExceptionList({ http: mockKibanaHttpService, id: 'myListId', onError }) + const { result, waitForNextUpdate } = renderHook< + UseExceptionListProps, + ReturnExceptionListAndItems + >(() => + useExceptionList({ + http: mockKibanaHttpService, + id: 'myListId', + namespaceType: 'single', + onError: onErrorMock, + }) ); await waitForNextUpdate(); - expect(result.current).toEqual([true, null]); + + expect(result.current).toEqual([true, null, result.current[2]]); + expect(typeof result.current[2]).toEqual('function'); }); }); test('fetch exception list and items', async () => { - const onError = jest.fn(); await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useExceptionList({ http: mockKibanaHttpService, id: 'myListId', onError }) + const { result, waitForNextUpdate } = renderHook< + UseExceptionListProps, + ReturnExceptionListAndItems + >(() => + useExceptionList({ + http: mockKibanaHttpService, + id: 'myListId', + namespaceType: 'single', + onError: onErrorMock, + }) ); await waitForNextUpdate(); await waitForNextUpdate(); - expect(result.current).toEqual([ - false, - { - _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', - exceptionItems: { - data: [ - { - _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.', - match_any: undefined, - operator: 'included', - }, - { - field: 'event.category', - match: undefined, - 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', - }, - ], + + const expectedResult: ExceptionListAndItems = { + ...getExceptionListSchemaMock(), + exceptionItems: { + items: [{ ...getExceptionListItemSchemaMock() }], + pagination: { page: 1, - per_page: 20, + perPage: 20, total: 1, }, - 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', }, - ]); + }; + + expect(result.current).toEqual([false, expectedResult, result.current[2]]); }); }); test('fetch a new exception list and its items', async () => { - const onError = jest.fn(); const spyOnfetchExceptionListById = jest.spyOn(api, 'fetchExceptionListById'); const spyOnfetchExceptionListItemsByListId = jest.spyOn(api, 'fetchExceptionListItemsByListId'); await act(async () => { - const { rerender, waitForNextUpdate } = renderHook( - (id) => useExceptionList({ http: mockKibanaHttpService, id, onError }), + const { rerender, waitForNextUpdate } = renderHook< + UseExceptionListProps, + ReturnExceptionListAndItems + >( + ({ filterOptions, http, id, namespaceType, pagination, onError }) => + useExceptionList({ filterOptions, http, id, namespaceType, onError, pagination }), { - initialProps: 'myListId', + initialProps: { + http: mockKibanaHttpService, + id: 'myListId', + namespaceType: 'single', + onError: onErrorMock, + }, } ); await waitForNextUpdate(); + rerender({ + http: mockKibanaHttpService, + id: 'newListId', + namespaceType: 'single', + onError: onErrorMock, + }); + await waitForNextUpdate(); + + expect(spyOnfetchExceptionListById).toHaveBeenCalledTimes(2); + expect(spyOnfetchExceptionListItemsByListId).toHaveBeenCalledTimes(2); + }); + }); + + test('fetches list and items when refreshExceptionList callback invoked', async () => { + const spyOnfetchExceptionListById = jest.spyOn(api, 'fetchExceptionListById'); + const spyOnfetchExceptionListItemsByListId = jest.spyOn(api, 'fetchExceptionListItemsByListId'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseExceptionListProps, + ReturnExceptionListAndItems + >(() => + useExceptionList({ + http: mockKibanaHttpService, + id: 'myListId', + namespaceType: 'single', + onError: onErrorMock, + }) + ); + await waitForNextUpdate(); await waitForNextUpdate(); - rerender('newListId'); + result.current[2](); await waitForNextUpdate(); + expect(spyOnfetchExceptionListById).toHaveBeenCalledTimes(2); expect(spyOnfetchExceptionListItemsByListId).toHaveBeenCalledTimes(2); }); }); + + test('invokes "onError" callback if "fetchExceptionListItemsByListId" fails', async () => { + const mockError = new Error('failed to fetch list items'); + const spyOnfetchExceptionListById = jest.spyOn(api, 'fetchExceptionListById'); + const spyOnfetchExceptionListItemsByListId = jest + .spyOn(api, 'fetchExceptionListItemsByListId') + .mockRejectedValue(mockError); + await act(async () => { + const { waitForNextUpdate } = renderHook( + () => + useExceptionList({ + http: mockKibanaHttpService, + id: 'myListId', + namespaceType: 'single', + onError: onErrorMock, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(spyOnfetchExceptionListById).toHaveBeenCalledTimes(1); + expect(onErrorMock).toHaveBeenCalledWith(mockError); + expect(spyOnfetchExceptionListItemsByListId).toHaveBeenCalledTimes(1); + }); + }); + + test('invokes "onError" callback if "fetchExceptionListById" fails', async () => { + const mockError = new Error('failed to fetch list'); + jest.spyOn(api, 'fetchExceptionListById').mockRejectedValue(mockError); + + await act(async () => { + const { waitForNextUpdate } = renderHook( + () => + useExceptionList({ + http: mockKibanaHttpService, + id: 'myListId', + namespaceType: 'single', + onError: onErrorMock, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(onErrorMock).toHaveBeenCalledWith(mockError); + }); + }); }); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.tsx b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.tsx index d0ac357e05aa0..116233cd89348 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.tsx +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.tsx @@ -4,66 +4,124 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { fetchExceptionListById, fetchExceptionListItemsByListId } from '../api'; import { ExceptionListAndItems, UseExceptionListProps } from '../types'; -export type ReturnExceptionListAndItems = [boolean, ExceptionListAndItems | null]; +export type ReturnExceptionListAndItems = [boolean, ExceptionListAndItems | null, () => void]; /** * Hook for using to get an ExceptionList and it's ExceptionListItems * + * @param http Kibana http service * @param id desired ExceptionList ID (not list_id) + * @param namespaceType list namespaceType determines list space + * @param onError error callback + * @param filterOptions optional - filter by fields or tags + * @param pagination optional * */ export const useExceptionList = ({ http, id, + namespaceType, + pagination = { + page: 1, + perPage: 20, + total: 0, + }, + filterOptions = { + filter: '', + tags: [], + }, onError, }: UseExceptionListProps): ReturnExceptionListAndItems => { const [exceptionListAndItems, setExceptionList] = useState(null); + const [shouldRefresh, setRefresh] = useState(true); + const refreshExceptionList = useCallback(() => setRefresh(true), [setRefresh]); const [loading, setLoading] = useState(true); + const tags = filterOptions.tags.sort().join(); - useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); + useEffect( + () => { + let isSubscribed = true; + const abortCtrl = new AbortController(); - const fetchData = async (idToFetch: string): Promise => { - try { - setLoading(true); - const exceptionList = await fetchExceptionListById({ - http, - id: idToFetch, - signal: abortCtrl.signal, - }); - const exceptionListItems = await fetchExceptionListItemsByListId({ - http, - listId: exceptionList.list_id, - signal: abortCtrl.signal, - }); - if (isSubscribed) { - setExceptionList({ ...exceptionList, exceptionItems: { ...exceptionListItems } }); + const fetchData = async (idToFetch: string): Promise => { + if (shouldRefresh) { + try { + setLoading(true); + + const { + list_id, + namespace_type, + ...restOfExceptionList + } = await fetchExceptionListById({ + http, + id: idToFetch, + namespaceType, + signal: abortCtrl.signal, + }); + const fetchListItemsResult = await fetchExceptionListItemsByListId({ + filterOptions, + http, + listId: list_id, + namespaceType: namespace_type, + pagination, + signal: abortCtrl.signal, + }); + + setRefresh(false); + + if (isSubscribed) { + setExceptionList({ + list_id, + namespace_type, + ...restOfExceptionList, + exceptionItems: { + items: [...fetchListItemsResult.data], + pagination: { + page: fetchListItemsResult.page, + perPage: fetchListItemsResult.per_page, + total: fetchListItemsResult.total, + }, + }, + }); + } + } catch (error) { + setRefresh(false); + if (isSubscribed) { + setExceptionList(null); + onError(error); + } + } } - } catch (error) { + if (isSubscribed) { - setExceptionList(null); - onError(error); + setLoading(false); } - } - if (isSubscribed) { - setLoading(false); - } - }; + }; - if (id != null) { - fetchData(id); - } - return (): void => { - isSubscribed = false; - abortCtrl.abort(); - }; - }, [http, id, onError]); + if (id != null) { + fetchData(id); + } + return (): void => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, // eslint-disable-next-line react-hooks/exhaustive-deps + [ + http, + id, + onError, + shouldRefresh, + pagination.page, + pagination.perPage, + filterOptions.filter, + tags, + ] + ); - return [loading, exceptionListAndItems]; + return [loading, exceptionListAndItems, refreshExceptionList]; }; diff --git a/x-pack/plugins/lists/public/exceptions/mock.ts b/x-pack/plugins/lists/public/exceptions/mock.ts index 6980051238973..fd06dac65c6fb 100644 --- a/x-pack/plugins/lists/public/exceptions/mock.ts +++ b/x-pack/plugins/lists/public/exceptions/mock.ts @@ -6,26 +6,8 @@ import { CreateExceptionListItemSchemaPartial, CreateExceptionListSchemaPartial, - ExceptionListItemSchema, - ExceptionListSchema, } from '../../common/schemas'; -export const mockExceptionList: ExceptionListSchema = { - _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', -}; - export const mockNewExceptionList: CreateExceptionListSchemaPartial = { _tags: ['endpoint', 'process', 'malware', 'os:linux'], description: 'This is a sample endpoint type exception', @@ -58,35 +40,3 @@ export const mockNewExceptionItem: CreateExceptionListItemSchemaPartial = { tags: ['user added string for a tag', 'malware'], type: 'simple', }; - -export const mockExceptionItem: ExceptionListItemSchema = { - _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.', - match_any: undefined, - operator: 'included', - }, - { - field: 'event.category', - match: undefined, - 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', -}; diff --git a/x-pack/plugins/lists/public/exceptions/types.ts b/x-pack/plugins/lists/public/exceptions/types.ts index fcf2108e7323a..cf6b6c3ec1c59 100644 --- a/x-pack/plugins/lists/public/exceptions/types.ts +++ b/x-pack/plugins/lists/public/exceptions/types.ts @@ -9,12 +9,28 @@ import { CreateExceptionListSchemaPartial, ExceptionListItemSchema, ExceptionListSchema, - FoundExceptionListItemSchema, + NamespaceType, } from '../../common/schemas'; import { HttpStart } from '../../../../../src/core/public'; +export interface FilterExceptionsOptions { + filter: string; + tags: string[]; +} + +export interface Pagination { + page: number; + perPage: number; + total: number; +} + +export interface ExceptionItemsAndPagination { + items: ExceptionListItemSchema[]; + pagination: Pagination; +} + export interface ExceptionListAndItems extends ExceptionListSchema { - exceptionItems: FoundExceptionListItemSchema; + exceptionItems: ExceptionItemsAndPagination; } export type AddExceptionList = ExceptionListSchema | CreateExceptionListSchemaPartial; @@ -27,20 +43,27 @@ export interface PersistHookProps { } export interface UseExceptionListProps { + filterOptions?: FilterExceptionsOptions; http: HttpStart; id: string | undefined; + namespaceType: NamespaceType; onError: (arg: Error) => void; + pagination?: Pagination; } export interface ApiCallByListIdProps { http: HttpStart; listId: string; + namespaceType: NamespaceType; + filterOptions?: FilterExceptionsOptions; + pagination?: Pagination; signal: AbortSignal; } export interface ApiCallByIdProps { http: HttpStart; id: string; + namespaceType: NamespaceType; signal: AbortSignal; } diff --git a/x-pack/plugins/lists/public/index.tsx b/x-pack/plugins/lists/public/index.tsx index b23f31abd4d87..fb4d5de06ae54 100644 --- a/x-pack/plugins/lists/public/index.tsx +++ b/x-pack/plugins/lists/public/index.tsx @@ -7,9 +7,4 @@ export { usePersistExceptionItem } from './exceptions/hooks/persist_exception_item'; export { usePersistExceptionList } from './exceptions/hooks/persist_exception_list'; export { useExceptionList } from './exceptions/hooks/use_exception_list'; -export { - mockExceptionItem, - mockExceptionList, - mockNewExceptionItem, - mockNewExceptionList, -} from './exceptions/mock'; +export { mockNewExceptionItem, mockNewExceptionList } from './exceptions/mock'; diff --git a/x-pack/plugins/lists/scripts/check_circular_deps.js b/x-pack/plugins/lists/scripts/check_circular_deps.js new file mode 100644 index 0000000000000..4ba7020d13465 --- /dev/null +++ b/x-pack/plugins/lists/scripts/check_circular_deps.js @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +require('../../../../src/setup_node_env'); +require('./check_circular_deps/run_check_circular_deps_cli'); diff --git a/x-pack/plugins/lists/scripts/check_circular_deps/run_check_circular_deps_cli.ts b/x-pack/plugins/lists/scripts/check_circular_deps/run_check_circular_deps_cli.ts new file mode 100644 index 0000000000000..430e4983882cb --- /dev/null +++ b/x-pack/plugins/lists/scripts/check_circular_deps/run_check_circular_deps_cli.ts @@ -0,0 +1,45 @@ +/* + * 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 { resolve } from 'path'; + +// @ts-ignore +import madge from 'madge'; +import { createFailError, run } from '@kbn/dev-utils'; + +run( + async ({ log }) => { + const result = await madge( + [resolve(__dirname, '../../public'), resolve(__dirname, '../../common')], + { + excludeRegExp: [ + 'test.ts$', + 'test.tsx$', + 'src/core/server/types.ts$', + 'src/core/server/saved_objects/types.ts$', + 'src/core/public/chrome/chrome_service.tsx$', + 'src/core/public/overlays/banners/banners_service.tsx$', + 'src/core/public/saved_objects/saved_objects_client.ts$', + 'src/plugins/data/public', + 'src/plugins/ui_actions/public', + ], + fileExtensions: ['ts', 'js', 'tsx'], + } + ); + + const circularFound = result.circular(); + if (circularFound.length !== 0) { + throw createFailError( + `Lists circular dependencies of imports has been found:\n - ${circularFound.join('\n - ')}` + ); + } else { + log.success('No circular deps 👍'); + } + }, + { + description: 'Check the Lists plugin for circular deps', + } +); diff --git a/x-pack/plugins/lists/server/get_user.test.ts b/x-pack/plugins/lists/server/get_user.test.ts index 0992e3c361fcf..a1c78f5ea4684 100644 --- a/x-pack/plugins/lists/server/get_user.test.ts +++ b/x-pack/plugins/lists/server/get_user.test.ts @@ -8,7 +8,6 @@ import { httpServerMock } from 'src/core/server/mocks'; import { KibanaRequest } from 'src/core/server'; import { securityMock } from '../../security/server/mocks'; -import { SecurityPluginSetup } from '../../security/server'; import { getUser } from './get_user'; @@ -24,42 +23,42 @@ describe('get_user', () => { }); test('it returns "bob" as the user given a security request with "bob"', () => { - const security: SecurityPluginSetup = securityMock.createSetup(); + const security = securityMock.createSetup(); security.authc.getCurrentUser = jest.fn().mockReturnValue({ username: 'bob' }); const user = getUser({ request, security }); expect(user).toEqual('bob'); }); test('it returns "alice" as the user given a security request with "alice"', () => { - const security: SecurityPluginSetup = securityMock.createSetup(); + const security = securityMock.createSetup(); security.authc.getCurrentUser = jest.fn().mockReturnValue({ username: 'alice' }); const user = getUser({ request, security }); expect(user).toEqual('alice'); }); test('it returns "elastic" as the user given null as the current user', () => { - const security: SecurityPluginSetup = securityMock.createSetup(); + const security = securityMock.createSetup(); security.authc.getCurrentUser = jest.fn().mockReturnValue(null); const user = getUser({ request, security }); expect(user).toEqual('elastic'); }); test('it returns "elastic" as the user given undefined as the current user', () => { - const security: SecurityPluginSetup = securityMock.createSetup(); + const security = securityMock.createSetup(); security.authc.getCurrentUser = jest.fn().mockReturnValue(undefined); const user = getUser({ request, security }); expect(user).toEqual('elastic'); }); test('it returns "elastic" as the user given undefined as the plugin', () => { - const security: SecurityPluginSetup = securityMock.createSetup(); + const security = securityMock.createSetup(); security.authc.getCurrentUser = jest.fn().mockReturnValue(undefined); const user = getUser({ request, security: undefined }); expect(user).toEqual('elastic'); }); test('it returns "elastic" as the user given null as the plugin', () => { - const security: SecurityPluginSetup = securityMock.createSetup(); + const security = securityMock.createSetup(); security.authc.getCurrentUser = jest.fn().mockReturnValue(undefined); const user = getUser({ request, security: null }); expect(user).toEqual('elastic'); 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/mocks.ts b/x-pack/plugins/lists/server/mocks.ts new file mode 100644 index 0000000000000..aad4a25a900a1 --- /dev/null +++ b/x-pack/plugins/lists/server/mocks.ts @@ -0,0 +1,23 @@ +/* + * 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 { ListPluginSetup } from './types'; +import { getListClientMock } from './services/lists/list_client.mock'; +import { getExceptionListClientMock } from './services/exception_lists/exception_list_client.mock'; + +const createSetupMock = (): jest.Mocked => { + const mock: jest.Mocked = { + getExceptionListClient: jest.fn().mockReturnValue(getExceptionListClientMock()), + getListClient: jest.fn().mockReturnValue(getListClientMock()), + }; + return mock; +}; + +export const listMock = { + createSetup: createSetupMock, + getExceptionList: getExceptionListClientMock, + getListClient: getListClientMock, +}; 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.mock.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts new file mode 100644 index 0000000000000..d0e238f8c5c40 --- /dev/null +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { savedObjectsClientMock } from 'src/core/server/mocks'; + +import { getFoundExceptionListSchemaMock } from '../../../common/schemas/response/found_exception_list_schema.mock'; +import { getFoundExceptionListItemSchemaMock } from '../../../common/schemas/response/found_exception_list_item_schema.mock'; +import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock'; +import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock'; + +import { ExceptionListClient } from './exception_list_client'; + +export class ExceptionListClientMock extends ExceptionListClient { + public getExceptionList = jest.fn().mockResolvedValue(getExceptionListSchemaMock()); + public getExceptionListItem = jest.fn().mockResolvedValue(getExceptionListItemSchemaMock()); + public createExceptionList = jest.fn().mockResolvedValue(getExceptionListSchemaMock()); + public updateExceptionList = jest.fn().mockResolvedValue(getExceptionListSchemaMock()); + public deleteExceptionList = jest.fn().mockResolvedValue(getExceptionListSchemaMock()); + public createExceptionListItem = jest.fn().mockResolvedValue(getExceptionListItemSchemaMock()); + public updateExceptionListItem = jest.fn().mockResolvedValue(getExceptionListItemSchemaMock()); + public deleteExceptionListItem = jest.fn().mockResolvedValue(getExceptionListItemSchemaMock()); + public findExceptionListItem = jest.fn().mockResolvedValue(getFoundExceptionListItemSchemaMock()); + public findExceptionList = jest.fn().mockResolvedValue(getFoundExceptionListSchemaMock()); +} + +export const getExceptionListClientMock = (): ExceptionListClient => { + const mock = new ExceptionListClientMock({ + savedObjectsClient: savedObjectsClientMock.create(), + user: 'elastic', + }); + return mock; +}; diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.test.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.test.ts new file mode 100644 index 0000000000000..f91331f5b4308 --- /dev/null +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.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 { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock'; +import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock'; + +import { getExceptionListClientMock } from './exception_list_client.mock'; + +describe('exception_list_client', () => { + describe('Mock client sanity checks', () => { + test('it returns the exception list as expected', async () => { + const mock = getExceptionListClientMock(); + const list = await mock.getExceptionList({ + id: '123', + listId: '123', + namespaceType: 'single', + }); + expect(list).toEqual(getExceptionListSchemaMock()); + }); + + test('it returns the the exception list item as expected', async () => { + const mock = getExceptionListClientMock(); + const listItem = await mock.getExceptionListItem({ + id: '123', + itemId: '123', + namespaceType: 'single', + }); + expect(listItem).toEqual(getExceptionListItemSchemaMock()); + }); + }); +}); 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.mock.ts b/x-pack/plugins/lists/server/services/lists/list_client.mock.ts new file mode 100644 index 0000000000000..43a01a3ca62dc --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/list_client.mock.ts @@ -0,0 +1,69 @@ +/* + * 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 { getFoundListItemSchemaMock } from '../../../common/schemas/response/found_list_item_schema.mock'; +import { getFoundListSchemaMock } from '../../../common/schemas/response/found_list_schema.mock'; +import { getListItemResponseMock } from '../../../common/schemas/response/list_item_schema.mock'; +import { getListResponseMock } from '../../../common/schemas/response/list_schema.mock'; +import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; +import { LIST_INDEX, LIST_ITEM_INDEX } from '../../../common/constants.mock'; + +import { ListClient } from './list_client'; + +export class ListClientMock extends ListClient { + public getListIndex = jest.fn().mockReturnValue(LIST_INDEX); + public getListItemIndex = jest.fn().mockReturnValue(LIST_ITEM_INDEX); + public getList = jest.fn().mockResolvedValue(getListResponseMock()); + public createList = jest.fn().mockResolvedValue(getListResponseMock()); + public createListIfItDoesNotExist = jest.fn().mockResolvedValue(getListResponseMock()); + public getListIndexExists = jest.fn().mockResolvedValue(true); + public getListItemIndexExists = jest.fn().mockResolvedValue(true); + public createListBootStrapIndex = jest.fn().mockResolvedValue({}); + public createListItemBootStrapIndex = jest.fn().mockResolvedValue({}); + public getListPolicyExists = jest.fn().mockResolvedValue(true); + public getListItemPolicyExists = jest.fn().mockResolvedValue(true); + public getListTemplateExists = jest.fn().mockResolvedValue(true); + public getListItemTemplateExists = jest.fn().mockResolvedValue(true); + public getListTemplate = jest.fn().mockResolvedValue({}); + public getListItemTemplate = jest.fn().mockResolvedValue({}); + public setListTemplate = jest.fn().mockResolvedValue({}); + public setListItemTemplate = jest.fn().mockResolvedValue({}); + public setListPolicy = jest.fn().mockResolvedValue({}); + public setListItemPolicy = jest.fn().mockResolvedValue({}); + public deleteListIndex = jest.fn().mockResolvedValue(true); + public deleteListItemIndex = jest.fn().mockResolvedValue(true); + public deleteListPolicy = jest.fn().mockResolvedValue({}); + public deleteListItemPolicy = jest.fn().mockResolvedValue({}); + public deleteListTemplate = jest.fn().mockResolvedValue({}); + public deleteListItemTemplate = jest.fn().mockResolvedValue({}); + public deleteListItem = jest.fn().mockResolvedValue(getListItemResponseMock()); + public deleteListItemByValue = jest.fn().mockResolvedValue(getListItemResponseMock()); + public deleteList = jest.fn().mockResolvedValue(getListResponseMock()); + public exportListItemsToStream = jest.fn().mockResolvedValue(undefined); + public importListItemsToStream = jest.fn().mockResolvedValue(undefined); + public getListItemByValue = jest.fn().mockResolvedValue([getListItemResponseMock()]); + public createListItem = jest.fn().mockResolvedValue(getListItemResponseMock()); + public updateListItem = jest.fn().mockResolvedValue(getListItemResponseMock()); + public updateList = jest.fn().mockResolvedValue(getListResponseMock()); + public getListItem = jest.fn().mockResolvedValue(getListItemResponseMock()); + public getListItemByValues = jest.fn().mockResolvedValue([getListItemResponseMock()]); + public findList = jest.fn().mockResolvedValue(getFoundListSchemaMock()); + public findListItem = jest.fn().mockResolvedValue(getFoundListItemSchemaMock()); +} + +export const getListClientMock = (): ListClient => { + const mock = new ListClientMock({ + callCluster: getCallClusterMock(), + config: { + enabled: true, + listIndex: LIST_INDEX, + listItemIndex: LIST_ITEM_INDEX, + }, + spaceId: 'default', + user: 'elastic', + }); + return mock; +}; diff --git a/x-pack/plugins/lists/server/services/lists/list_client.test.ts b/x-pack/plugins/lists/server/services/lists/list_client.test.ts new file mode 100644 index 0000000000000..0c3a58283ffe2 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/list_client.test.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getListItemResponseMock } from '../../../common/schemas/response/list_item_schema.mock'; +import { LIST_INDEX, LIST_ITEM_INDEX } from '../../../common/constants.mock'; + +import { getListClientMock } from './list_client.mock'; + +describe('list_client', () => { + describe('Mock client sanity checks', () => { + test('it returns the get list index as expected', () => { + const mock = getListClientMock(); + expect(mock.getListIndex()).toEqual(LIST_INDEX); + }); + + test('it returns the get list item index as expected', () => { + const mock = getListClientMock(); + expect(mock.getListItemIndex()).toEqual(LIST_ITEM_INDEX); + }); + + test('it returns a mock list item', async () => { + const mock = getListClientMock(); + const listItem = await mock.getListItem({ id: '123' }); + expect(listItem).toEqual(getListItemResponseMock()); + }); + }); +}); 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 70% 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..47419929c43a6 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'; + +export 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..2af501106d659 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/get_search_after_scroll.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { APICaller } from 'kibana/server'; + +import { Filter, SortFieldOrUndefined, SortOrderOrUndefined } from '../../../common/schemas'; +import { Scroll } from '../lists/types'; + +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..6b898a54bb9fe --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/scroll_to_start_page.ts @@ -0,0 +1,95 @@ +/* + * 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 { Scroll } from '../lists/types'; + +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/constants.ts b/x-pack/plugins/maps/common/constants.ts index 613e507459389..8fa44c512df4b 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -72,11 +72,12 @@ export enum FIELD_ORIGIN { } export const JOIN_FIELD_NAME_PREFIX = '__kbnjoin__'; -export const SOURCE_DATA_ID_ORIGIN = 'source'; -export const META_ID_ORIGIN_SUFFIX = 'meta'; -export const SOURCE_META_ID_ORIGIN = `${SOURCE_DATA_ID_ORIGIN}_${META_ID_ORIGIN_SUFFIX}`; -export const FORMATTERS_ID_ORIGIN_SUFFIX = 'formatters'; -export const SOURCE_FORMATTERS_ID_ORIGIN = `${SOURCE_DATA_ID_ORIGIN}_${FORMATTERS_ID_ORIGIN_SUFFIX}`; +export const META_DATA_REQUEST_ID_SUFFIX = 'meta'; +export const FORMATTERS_DATA_REQUEST_ID_SUFFIX = 'formatters'; +export const SOURCE_DATA_REQUEST_ID = 'source'; +export const SOURCE_META_DATA_REQUEST_ID = `${SOURCE_DATA_REQUEST_ID}_${META_DATA_REQUEST_ID_SUFFIX}`; +export const SOURCE_FORMATTERS_DATA_REQUEST_ID = `${SOURCE_DATA_REQUEST_ID}_${FORMATTERS_DATA_REQUEST_ID_SUFFIX}`; +export const SOURCE_BOUNDS_DATA_REQUEST_ID = `${SOURCE_DATA_REQUEST_ID}_bounds`; export const MIN_ZOOM = 0; export const MAX_ZOOM = 24; 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/data_request_actions.ts b/x-pack/plugins/maps/public/actions/data_request_actions.ts index d418214416900..13b658af6a0f3 100644 --- a/x-pack/plugins/maps/public/actions/data_request_actions.ts +++ b/x-pack/plugins/maps/public/actions/data_request_actions.ts @@ -6,12 +6,15 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ import { Dispatch } from 'redux'; +// @ts-ignore +import turf from 'turf'; import { FeatureCollection } from 'geojson'; import { MapStoreState } from '../reducers/store'; -import { LAYER_TYPE, SOURCE_DATA_ID_ORIGIN } from '../../common/constants'; +import { LAYER_TYPE, SOURCE_DATA_REQUEST_ID } from '../../common/constants'; import { getDataFilters, getDataRequestDescriptor, + getFittableLayers, getLayerById, getLayerList, } from '../selectors/map_selectors'; @@ -27,13 +30,15 @@ import { LAYER_DATA_LOAD_ENDED, LAYER_DATA_LOAD_ERROR, LAYER_DATA_LOAD_STARTED, + SET_GOTO, SET_LAYER_ERROR_STATUS, SET_LAYER_STYLE_META, UPDATE_LAYER_PROP, UPDATE_SOURCE_DATA_REQUEST, } from './map_action_constants'; import { ILayer } from '../classes/layers/layer'; -import { DataMeta, MapFilters } from '../../common/descriptor_types'; +import { DataMeta, MapExtent, MapFilters } from '../../common/descriptor_types'; +import { DataRequestAbortError } from '../classes/util/data_request'; export type DataRequestContext = { startLoading(dataId: string, requestToken: symbol, meta: DataMeta): void; @@ -269,7 +274,7 @@ export function updateSourceDataRequest(layerId: string, newData: unknown) { return (dispatch: Dispatch) => { dispatch({ type: UPDATE_SOURCE_DATA_REQUEST, - dataId: SOURCE_DATA_ID_ORIGIN, + dataId: SOURCE_DATA_REQUEST_ID, layerId, newData, }); @@ -277,3 +282,99 @@ export function updateSourceDataRequest(layerId: string, newData: unknown) { dispatch(updateStyleMeta(layerId)); }; } + +export function fitToLayerExtent(layerId: string) { + return async (dispatch: Dispatch, getState: () => MapStoreState) => { + const targetLayer = getLayerById(layerId, getState()); + + if (targetLayer) { + try { + const bounds = await targetLayer.getBounds( + getDataRequestContext(dispatch, getState, layerId) + ); + if (bounds) { + await dispatch(setGotoWithBounds(bounds)); + } + } catch (error) { + if (!(error instanceof DataRequestAbortError)) { + // eslint-disable-next-line no-console + console.warn( + 'Unhandled getBounds error for layer. Only DataRequestAbortError should be surfaced', + error + ); + } + // new fitToLayerExtent request has superseded this thread of execution. Results no longer needed. + return; + } + } + }; +} + +export function fitToDataBounds() { + return async (dispatch: Dispatch, getState: () => MapStoreState) => { + const layerList = getFittableLayers(getState()); + + if (!layerList.length) { + return; + } + + const boundsPromises = layerList.map(async (layer: ILayer) => { + return layer.getBounds(getDataRequestContext(dispatch, getState, layer.getId())); + }); + + let bounds; + try { + bounds = await Promise.all(boundsPromises); + } catch (error) { + if (!(error instanceof DataRequestAbortError)) { + // eslint-disable-next-line no-console + console.warn( + 'Unhandled getBounds error for layer. Only DataRequestAbortError should be surfaced', + error + ); + } + // new fitToDataBounds request has superseded this thread of execution. Results no longer needed. + return; + } + + const corners = []; + for (let i = 0; i < bounds.length; i++) { + const b = bounds[i]; + + // filter out undefined bounds (uses Infinity due to turf responses) + if ( + b === null || + b.minLon === Infinity || + b.maxLon === Infinity || + b.minLat === -Infinity || + b.maxLat === -Infinity + ) { + continue; + } + + corners.push([b.minLon, b.minLat]); + corners.push([b.maxLon, b.maxLat]); + } + + if (!corners.length) { + return; + } + + const turfUnionBbox = turf.bbox(turf.multiPoint(corners)); + const dataBounds = { + minLon: turfUnionBbox[0], + minLat: turfUnionBbox[1], + maxLon: turfUnionBbox[2], + maxLat: turfUnionBbox[3], + }; + + dispatch(setGotoWithBounds(dataBounds)); + }; +} + +function setGotoWithBounds(bounds: MapExtent) { + return { + type: SET_GOTO, + bounds, + }; +} diff --git a/x-pack/plugins/maps/public/actions/index.ts b/x-pack/plugins/maps/public/actions/index.ts index a2e90ff6e9f28..5b153e37da5a8 100644 --- a/x-pack/plugins/maps/public/actions/index.ts +++ b/x-pack/plugins/maps/public/actions/index.ts @@ -9,7 +9,12 @@ export * from './ui_actions'; export * from './map_actions'; export * from './map_action_constants'; export * from './layer_actions'; -export { cancelAllInFlightRequests, DataRequestContext } from './data_request_actions'; +export { + cancelAllInFlightRequests, + DataRequestContext, + fitToLayerExtent, + fitToDataBounds, +} from './data_request_actions'; export { closeOnClickTooltip, openOnClickTooltip, 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/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index 02842edabbd2e..75df8689a670e 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -12,11 +12,9 @@ import turfBooleanContains from '@turf/boolean-contains'; import { Filter, Query, TimeRange } from 'src/plugins/data/public'; import { MapStoreState } from '../reducers/store'; import { - getLayerById, getDataFilters, getWaitingForMapReadyLayerListRaw, getQuery, - getFittableLayers, } from '../selectors/map_selectors'; import { CLEAR_GOTO, @@ -184,76 +182,6 @@ export function disableScrollZoom() { return { type: SET_SCROLL_ZOOM, scrollZoom: false }; } -export function fitToLayerExtent(layerId: string) { - return async (dispatch: Dispatch, getState: () => MapStoreState) => { - const targetLayer = getLayerById(layerId, getState()); - - if (targetLayer) { - const dataFilters = getDataFilters(getState()); - const bounds = await targetLayer.getBounds(dataFilters); - if (bounds) { - await dispatch(setGotoWithBounds(bounds)); - } - } - }; -} - -export function fitToDataBounds() { - return async (dispatch: Dispatch, getState: () => MapStoreState) => { - const layerList = getFittableLayers(getState()); - - if (!layerList.length) { - return; - } - - const dataFilters = getDataFilters(getState()); - const boundsPromises = layerList.map(async (layer) => { - return layer.getBounds(dataFilters); - }); - - const bounds = await Promise.all(boundsPromises); - const corners = []; - for (let i = 0; i < bounds.length; i++) { - const b = bounds[i]; - - // filter out undefined bounds (uses Infinity due to turf responses) - if ( - b === null || - b.minLon === Infinity || - b.maxLon === Infinity || - b.minLat === -Infinity || - b.maxLat === -Infinity - ) { - continue; - } - - corners.push([b.minLon, b.minLat]); - corners.push([b.maxLon, b.maxLat]); - } - - if (!corners.length) { - return; - } - - const turfUnionBbox = turf.bbox(turf.multiPoint(corners)); - const dataBounds = { - minLon: turfUnionBbox[0], - minLat: turfUnionBbox[1], - maxLon: turfUnionBbox[2], - maxLat: turfUnionBbox[3], - }; - - dispatch(setGotoWithBounds(dataBounds)); - }; -} - -export function setGotoWithBounds(bounds: MapExtent) { - return { - type: SET_GOTO, - bounds, - }; -} - export function setGotoWithCenter({ lat, lon, zoom }: MapCenterAndZoom) { return { type: SET_GOTO, diff --git a/x-pack/plugins/maps/public/classes/joins/inner_join.js b/x-pack/plugins/maps/public/classes/joins/inner_join.js index 5f8bc7385d04c..76afe2430b818 100644 --- a/x-pack/plugins/maps/public/classes/joins/inner_join.js +++ b/x-pack/plugins/maps/public/classes/joins/inner_join.js @@ -6,7 +6,10 @@ import { ESTermSource } from '../sources/es_term_source'; import { getComputedFieldNamePrefix } from '../styles/vector/style_util'; -import { META_ID_ORIGIN_SUFFIX, FORMATTERS_ID_ORIGIN_SUFFIX } from '../../../common/constants'; +import { + META_DATA_REQUEST_ID_SUFFIX, + FORMATTERS_DATA_REQUEST_ID_SUFFIX, +} from '../../../common/constants'; export class InnerJoin { constructor(joinDescriptor, leftSource) { @@ -42,11 +45,11 @@ export class InnerJoin { } getSourceMetaDataRequestId() { - return `${this.getSourceDataRequestId()}_${META_ID_ORIGIN_SUFFIX}`; + return `${this.getSourceDataRequestId()}_${META_DATA_REQUEST_ID_SUFFIX}`; } getSourceFormattersDataRequestId() { - return `${this.getSourceDataRequestId()}_${FORMATTERS_ID_ORIGIN_SUFFIX}`; + return `${this.getSourceDataRequestId()}_${FORMATTERS_DATA_REQUEST_ID_SUFFIX}`; } getLeftField() { diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index 263e9888cd059..2250d5663378c 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -17,21 +17,16 @@ import { MAX_ZOOM, MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER, MIN_ZOOM, - SOURCE_DATA_ID_ORIGIN, + SOURCE_DATA_REQUEST_ID, } from '../../../common/constants'; import { copyPersistentState } from '../../reducers/util'; -import { - LayerDescriptor, - MapExtent, - MapFilters, - StyleDescriptor, -} from '../../../common/descriptor_types'; +import { LayerDescriptor, MapExtent, StyleDescriptor } from '../../../common/descriptor_types'; import { Attribution, ImmutableSourceProperty, ISource, SourceEditorArgs } from '../sources/source'; import { DataRequestContext } from '../../actions'; import { IStyle } from '../styles/style'; export interface ILayer { - getBounds(mapFilters: MapFilters): Promise; + getBounds(dataRequestContext: DataRequestContext): Promise; getDataRequest(id: string): DataRequest | undefined; getDisplayName(source?: ISource): Promise; getId(): string; @@ -80,6 +75,7 @@ export interface ILayer { getInFlightRequestTokens(): symbol[]; getPrevRequestToken(dataId: string): symbol | undefined; destroy: () => void; + isPreviewLayer: () => boolean; } export type Footnote = { icon: ReactElement; @@ -179,6 +175,10 @@ export class AbstractLayer implements ILayer { return this.getSource().isJoinable(); } + isPreviewLayer(): boolean { + return !!this._descriptor.__isPreviewLayer; + } + supportsElasticsearchFilters(): boolean { return this.getSource().isESSource(); } @@ -396,7 +396,7 @@ export class AbstractLayer implements ILayer { } getSourceDataRequest(): DataRequest | undefined { - return this.getDataRequest(SOURCE_DATA_ID_ORIGIN); + return this.getDataRequest(SOURCE_DATA_REQUEST_ID); } getDataRequest(id: string): DataRequest | undefined { @@ -450,13 +450,8 @@ export class AbstractLayer implements ILayer { return sourceDataRequest ? sourceDataRequest.hasData() : false; } - async getBounds(mapFilters: MapFilters): Promise { - return { - minLon: -180, - maxLon: 180, - minLat: -89, - maxLat: 89, - }; + async getBounds(dataRequestContext: DataRequestContext): Promise { + return null; } renderStyleEditor({ 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 { return ( dynamicStyleProp.getFieldOrigin() === FIELD_ORIGIN.SOURCE && @@ -470,7 +494,7 @@ export class VectorLayer extends AbstractLayer { layerName, style, dynamicStyleProps, - registerCancelCallback, + registerCancelCallback.bind(null, requestToken), nextMeta ); stopLoading(dataRequestId, requestToken, styleMeta, nextMeta); @@ -488,7 +512,7 @@ export class VectorLayer extends AbstractLayer { return this._syncFormatters({ source, - dataRequestId: SOURCE_FORMATTERS_ID_ORIGIN, + dataRequestId: SOURCE_FORMATTERS_DATA_REQUEST_ID, fields: style .getDynamicPropertiesArray() .filter((dynamicStyleProp) => { diff --git a/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.js b/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.js index 6f616afb64041..61ec02e72adf2 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.js +++ b/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.js @@ -6,7 +6,7 @@ import { TileLayer } from '../tile_layer/tile_layer'; import _ from 'lodash'; -import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE, LAYER_STYLE_TYPE } from '../../../../common/constants'; +import { SOURCE_DATA_REQUEST_ID, LAYER_TYPE, LAYER_STYLE_TYPE } from '../../../../common/constants'; import { isRetina } from '../../../meta'; import { addSpriteSheetToMapFromImageData, @@ -56,16 +56,16 @@ export class VectorTileLayer extends TileLayer { const requestToken = Symbol(`layer-source-refresh:${this.getId()} - source`); try { - startLoading(SOURCE_DATA_ID_ORIGIN, requestToken, dataFilters); + startLoading(SOURCE_DATA_REQUEST_ID, requestToken, dataFilters); const styleAndSprites = await this.getSource().getVectorStyleSheetAndSpriteMeta(isRetina()); const spriteSheetImageData = await loadSpriteSheetImageData(styleAndSprites.spriteMeta.png); const data = { ...styleAndSprites, spriteSheetImageData, }; - stopLoading(SOURCE_DATA_ID_ORIGIN, requestToken, data, nextMeta); + stopLoading(SOURCE_DATA_REQUEST_ID, requestToken, data, nextMeta); } catch (error) { - onLoadError(SOURCE_DATA_ID_ORIGIN, requestToken, error.message); + onLoadError(SOURCE_DATA_REQUEST_ID, requestToken, error.message); } } diff --git a/x-pack/plugins/maps/public/classes/sources/client_file_source/upload_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/client_file_source/upload_layer_wizard.tsx index d5ee354914e5c..3f4ec0d3f1268 100644 --- a/x-pack/plugins/maps/public/classes/sources/client_file_source/upload_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/client_file_source/upload_layer_wizard.tsx @@ -28,7 +28,7 @@ export const uploadLayerWizardConfig: LayerWizard = { icon: 'importAction', isIndexingSource: true, renderWizard: ({ - previewLayer, + previewLayers, mapColors, isIndexingTriggered, onRemove, @@ -38,13 +38,13 @@ export const uploadLayerWizardConfig: LayerWizard = { }: RenderWizardArguments) => { 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 84bdee2a64bd8..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 @@ -34,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; } @@ -93,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 8d7bf0d2af661..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 @@ -28,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; } @@ -64,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/es_source/es_source.js b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js index 072f952fb8a13..450894d81485c 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js @@ -145,11 +145,8 @@ export class AbstractESSource extends AbstractVectorSource { return searchSource; } - async getBoundsForFilters({ sourceQuery, query, timeFilters, filters, applyGlobalQuery }) { - const searchSource = await this.makeSearchSource( - { sourceQuery, query, timeFilters, filters, applyGlobalQuery }, - 0 - ); + async getBoundsForFilters(boundsFilters, registerCancelCallback) { + const searchSource = await this.makeSearchSource(boundsFilters, 0); searchSource.setField('aggs', { fitToBounds: { geo_bounds: { @@ -160,13 +157,19 @@ export class AbstractESSource extends AbstractVectorSource { let esBounds; try { - const esResp = await searchSource.fetch(); + const abortController = new AbortController(); + registerCancelCallback(() => abortController.abort()); + const esResp = await searchSource.fetch({ abortSignal: abortController.signal }); if (!esResp.aggregations.fitToBounds.bounds) { // aggregations.fitToBounds is empty object when there are no matching documents return null; } esBounds = esResp.aggregations.fitToBounds.bounds; } catch (error) { + if (error.name === 'AbortError') { + throw new DataRequestAbortError(); + } + return null; } 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/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts index 5f6061b38678c..86a1589a7a030 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import uuid from 'uuid/v4'; import { AbstractSource, ImmutableSourceProperty } from '../source'; -import { GeoJsonWithMeta, ITiledSingleLayerVectorSource } from '../vector_source'; +import { BoundsFilters, GeoJsonWithMeta, ITiledSingleLayerVectorSource } from '../vector_source'; import { MAX_ZOOM, MIN_ZOOM, SOURCE_TYPES } from '../../../../common/constants'; import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; import { IField } from '../../fields/field'; @@ -16,7 +16,6 @@ import { getDataSourceLabel, getUrlLabel } from '../../../../common/i18n_getters import { MapExtent, TiledSingleLayerVectorSourceDescriptor, - VectorSourceRequestMeta, VectorSourceSyncMeta, } from '../../../../common/descriptor_types'; import { MVTSingleLayerVectorSourceConfig } from './mvt_single_layer_vector_source_editor'; @@ -133,13 +132,11 @@ export class MVTSingleLayerVectorSource extends AbstractSource return this._descriptor.maxSourceZoom; } - getBoundsForFilters(searchFilters: VectorSourceRequestMeta): MapExtent { - return { - maxLat: 90, - maxLon: 180, - minLat: -90, - minLon: -180, - }; + getBoundsForFilters( + boundsFilters: BoundsFilters, + registerCancelCallback: (requestToken: symbol, callback: () => void) => void + ): MapExtent | null { + return null; } getFieldByName(fieldName: string): IField | null { diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts index 2dd6bcd858137..711b7d600d74d 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts @@ -6,11 +6,13 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ import { FeatureCollection } from 'geojson'; +import { Filter, TimeRange } from 'src/plugins/data/public'; import { AbstractSource, ISource } from '../source'; import { IField } from '../../fields/field'; import { ESSearchSourceResponseMeta, MapExtent, + MapQuery, VectorSourceRequestMeta, VectorSourceSyncMeta, } from '../../../../common/descriptor_types'; @@ -24,9 +26,20 @@ export type GeoJsonWithMeta = { meta?: GeoJsonFetchMeta; }; +export type BoundsFilters = { + applyGlobalQuery: boolean; + filters: Filter[]; + query: MapQuery; + sourceQuery: MapQuery; + timeFilters: TimeRange; +}; + export interface IVectorSource extends ISource { filterAndFormatPropertiesToHtml(properties: unknown): Promise; - getBoundsForFilters(searchFilters: VectorSourceRequestMeta): MapExtent; + getBoundsForFilters( + boundsFilters: BoundsFilters, + registerCancelCallback: (requestToken: symbol, callback: () => void) => void + ): MapExtent | null; getGeoJsonWithMeta( layerName: 'string', searchFilters: unknown[], @@ -42,7 +55,10 @@ export interface IVectorSource extends ISource { export class AbstractVectorSource extends AbstractSource implements IVectorSource { filterAndFormatPropertiesToHtml(properties: unknown): Promise; - getBoundsForFilters(searchFilters: VectorSourceRequestMeta): MapExtent; + getBoundsForFilters( + boundsFilters: BoundsFilters, + registerCancelCallback: (requestToken: symbol, callback: () => void) => void + ): MapExtent | null; getGeoJsonWithMeta( layerName: 'string', searchFilters: unknown[], 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/vector/properties/dynamic_style_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.js index 98d5d3feb60ea..15d0b3c4bf913 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.js @@ -7,7 +7,11 @@ import _ from 'lodash'; import { AbstractStyleProperty } from './style_property'; import { DEFAULT_SIGMA } from '../vector_style_defaults'; -import { STYLE_TYPE, SOURCE_META_ID_ORIGIN, FIELD_ORIGIN } from '../../../../../common/constants'; +import { + STYLE_TYPE, + SOURCE_META_DATA_REQUEST_ID, + FIELD_ORIGIN, +} from '../../../../../common/constants'; import React from 'react'; import { OrdinalFieldMetaPopover } from '../components/field_meta/ordinal_field_meta_popover'; import { CategoricalFieldMetaPopover } from '../components/field_meta/categorical_field_meta_popover'; @@ -30,7 +34,7 @@ export class DynamicStyleProperty extends AbstractStyleProperty { _getStyleMetaDataRequestId(fieldName) { if (this.getFieldOrigin() === FIELD_ORIGIN.SOURCE) { - return SOURCE_META_ID_ORIGIN; + return SOURCE_META_DATA_REQUEST_ID; } const join = this._layer.getValidJoins().find((join) => { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js index f3ed18bd1302e..989ac268c0552 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js @@ -13,7 +13,7 @@ import { GEO_JSON_TYPE, FIELD_ORIGIN, STYLE_TYPE, - SOURCE_FORMATTERS_ID_ORIGIN, + SOURCE_FORMATTERS_DATA_REQUEST_ID, LAYER_STYLE_TYPE, DEFAULT_ICON, VECTOR_STYLES, @@ -373,7 +373,7 @@ export class VectorStyle extends AbstractStyle { let dataRequestId; if (dynamicProp.getFieldOrigin() === FIELD_ORIGIN.SOURCE) { - dataRequestId = SOURCE_FORMATTERS_ID_ORIGIN; + dataRequestId = SOURCE_FORMATTERS_DATA_REQUEST_ID; } else { const join = this._layer.getValidJoins().find((join) => { return join.getRightJoinSource().hasMatchingMetricField(fieldName); 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..317c11eb7680c 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, @@ -54,7 +53,7 @@ import { import { getDefaultMapSettings } from './default_map_settings'; import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR } from './util'; -import { SOURCE_DATA_ID_ORIGIN } from '../../common/constants'; +import { SOURCE_DATA_REQUEST_ID } from '../../common/constants'; const getLayerIndex = (list, layerId) => list.findIndex(({ id }) => layerId === id); @@ -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, @@ -448,7 +443,7 @@ function updateSourceDataRequest(state, action) { return state; } const dataRequest = layerDescriptor.__dataRequests.find((dataRequest) => { - return dataRequest.dataId === SOURCE_DATA_ID_ORIGIN; + return dataRequest.dataId === SOURCE_DATA_REQUEST_ID; }); if (!dataRequest) { 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..467f1074e88e7 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts @@ -26,7 +26,7 @@ import { getSourceByType } from '../classes/sources/source_registry'; import { GeojsonFileSource } from '../classes/sources/client_file_source'; import { LAYER_TYPE, - SOURCE_DATA_ID_ORIGIN, + SOURCE_DATA_REQUEST_ID, STYLE_TYPE, VECTOR_STYLES, SPATIAL_FILTERS_LAYER_ID, @@ -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 : []; @@ -266,7 +263,7 @@ export const getSpatialFiltersLayer = createSelector( alpha: settings.spatialFiltersAlpa, __dataRequests: [ { - dataId: SOURCE_DATA_ID_ORIGIN, + dataId: SOURCE_DATA_REQUEST_ID, data: featureCollection, }, ], @@ -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/common/types/data_frame_analytics.ts b/x-pack/plugins/ml/common/types/data_frame_analytics.ts new file mode 100644 index 0000000000000..5ba7f9c191a7f --- /dev/null +++ b/x-pack/plugins/ml/common/types/data_frame_analytics.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CustomHttpResponseOptions, ResponseError } from 'kibana/server'; +export interface DeleteDataFrameAnalyticsWithIndexStatus { + success: boolean; + error?: CustomHttpResponseOptions; +} diff --git a/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx b/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx index 9cea47ded09b4..fd2b7902833a6 100644 --- a/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx +++ b/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx @@ -6,7 +6,7 @@ import React, { FC } from 'react'; -import { EuiSpacer, EuiInMemoryTable } from '@elastic/eui'; +import { EuiSpacer, EuiInMemoryTable, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; // @ts-ignore import { formatDate } from '@elastic/eui/lib/services/format'; import { i18n } from '@kbn/i18n'; @@ -21,16 +21,33 @@ interface JobMessagesProps { messages: JobMessage[]; loading: boolean; error: string; + refreshMessage?: React.MouseEventHandler; } /** * Component for rendering job messages for anomaly detection * and data frame analytics jobs. */ -export const JobMessages: FC = ({ messages, loading, error }) => { +export const JobMessages: FC = ({ messages, loading, error, refreshMessage }) => { const columns = [ { - name: '', + name: refreshMessage ? ( + + + + ) : ( + '' + ), render: (message: JobMessage) => , width: `${theme.euiSizeL}`, }, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx index 2ef1515726d1b..33217f127f998 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx @@ -5,20 +5,41 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; - +import { fireEvent, render } from '@testing-library/react'; import * as CheckPrivilige from '../../../../../capabilities/check_capabilities'; - -import { DeleteAction } from './action_delete'; - import mockAnalyticsListItem from './__mocks__/analytics_list_item.json'; +import { DeleteAction } from './action_delete'; +import { I18nProvider } from '@kbn/i18n/react'; +import { + coreMock as mockCoreServices, + i18nServiceMock, +} from '../../../../../../../../../../src/core/public/mocks'; jest.mock('../../../../../capabilities/check_capabilities', () => ({ checkPermission: jest.fn(() => false), createPermissionFailureMessage: jest.fn(), })); +jest.mock('../../../../../../application/util/dependency_cache', () => ({ + getToastNotifications: () => ({ addSuccess: jest.fn(), addDanger: jest.fn() }), +})); + +jest.mock('../../../../../contexts/kibana', () => ({ + useMlKibana: () => ({ + services: mockCoreServices.createStart(), + }), +})); +export const MockI18nService = i18nServiceMock.create(); +export const I18nServiceConstructor = jest.fn().mockImplementation(() => MockI18nService); +jest.doMock('@kbn/i18n', () => ({ + I18nService: I18nServiceConstructor, +})); + describe('DeleteAction', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + test('When canDeleteDataFrameAnalytics permission is false, button should be disabled.', () => { const { getByTestId } = render(); expect(getByTestId('mlAnalyticsJobDeleteButton')).toHaveAttribute('disabled'); @@ -46,4 +67,24 @@ describe('DeleteAction', () => { expect(getByTestId('mlAnalyticsJobDeleteButton')).toHaveAttribute('disabled'); }); + + describe('When delete model is open', () => { + test('should allow to delete target index by default.', () => { + const mock = jest.spyOn(CheckPrivilige, 'checkPermission'); + mock.mockImplementation((p) => p === 'canDeleteDataFrameAnalytics'); + const { getByTestId, queryByTestId } = render( + + + + ); + const deleteButton = getByTestId('mlAnalyticsJobDeleteButton'); + fireEvent.click(deleteButton); + expect(getByTestId('mlAnalyticsJobDeleteModal')).toBeInTheDocument(); + expect(getByTestId('mlAnalyticsJobDeleteIndexSwitch')).toBeInTheDocument(); + const mlAnalyticsJobDeleteIndexSwitch = getByTestId('mlAnalyticsJobDeleteIndexSwitch'); + expect(mlAnalyticsJobDeleteIndexSwitch).toHaveAttribute('aria-checked', 'true'); + expect(queryByTestId('mlAnalyticsJobDeleteIndexPatternSwitch')).toBeNull(); + mock.mockRestore(); + }); + }); }); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx index 2923938ae68ac..2d433f6b18484 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx @@ -4,24 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useState } from 'react'; +import React, { Fragment, FC, useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty, EuiConfirmModal, EuiOverlayMask, EuiToolTip, + EuiSwitch, + EuiFlexGroup, + EuiFlexItem, EUI_MODAL_CONFIRM_BUTTON, } from '@elastic/eui'; - -import { deleteAnalytics } from '../../services/analytics_service'; - +import { IIndexPattern } from 'src/plugins/data/common'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + deleteAnalytics, + deleteAnalyticsAndDestIndex, + canDeleteIndex, +} from '../../services/analytics_service'; import { checkPermission, createPermissionFailureMessage, } from '../../../../../capabilities/check_capabilities'; - +import { useMlKibana } from '../../../../../contexts/kibana'; import { isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common'; +import { extractErrorMessage } from '../../../../../util/error_utils'; interface DeleteActionProps { item: DataFrameAnalyticsListRow; @@ -29,17 +37,99 @@ interface DeleteActionProps { export const DeleteAction: FC = ({ item }) => { const disabled = isDataFrameAnalyticsRunning(item.stats.state); - const canDeleteDataFrameAnalytics: boolean = checkPermission('canDeleteDataFrameAnalytics'); const [isModalVisible, setModalVisible] = useState(false); + const [deleteTargetIndex, setDeleteTargetIndex] = useState(true); + const [deleteIndexPattern, setDeleteIndexPattern] = useState(true); + const [userCanDeleteIndex, setUserCanDeleteIndex] = useState(false); + const [indexPatternExists, setIndexPatternExists] = useState(false); + + const { savedObjects, notifications } = useMlKibana().services; + const savedObjectsClient = savedObjects.client; + + const indexName = item.config.dest.index; + + const checkIndexPatternExists = async () => { + try { + const response = await savedObjectsClient.find({ + type: 'index-pattern', + perPage: 10, + search: `"${indexName}"`, + searchFields: ['title'], + fields: ['title'], + }); + const ip = response.savedObjects.find( + (obj) => obj.attributes.title.toLowerCase() === indexName.toLowerCase() + ); + if (ip !== undefined) { + setIndexPatternExists(true); + } + } catch (e) { + const { toasts } = notifications; + const error = extractErrorMessage(e); + + toasts.addDanger( + i18n.translate( + 'xpack.ml.dataframe.analyticsList.errorWithCheckingIfIndexPatternExistsNotificationErrorMessage', + { + defaultMessage: + 'An error occurred checking if index pattern {indexPattern} exists: {error}', + values: { indexPattern: indexName, error }, + } + ) + ); + } + }; + const checkUserIndexPermission = () => { + try { + const userCanDelete = canDeleteIndex(indexName); + if (userCanDelete) { + setUserCanDeleteIndex(true); + } + } catch (e) { + const { toasts } = notifications; + const error = extractErrorMessage(e); + + toasts.addDanger( + i18n.translate( + 'xpack.ml.dataframe.analyticsList.errorWithCheckingIfUserCanDeleteIndexNotificationErrorMessage', + { + defaultMessage: + 'An error occurred checking if user can delete {destinationIndex}: {error}', + values: { destinationIndex: indexName, error }, + } + ) + ); + } + }; + + useEffect(() => { + // Check if an index pattern exists corresponding to current DFA job + // if pattern does exist, show it to user + checkIndexPatternExists(); + + // Check if an user has permission to delete the index & index pattern + checkUserIndexPermission(); + }, []); const closeModal = () => setModalVisible(false); const deleteAndCloseModal = () => { setModalVisible(false); - deleteAnalytics(item); + + if ((userCanDeleteIndex && deleteTargetIndex) || (userCanDeleteIndex && deleteIndexPattern)) { + deleteAnalyticsAndDestIndex( + item, + deleteTargetIndex, + indexPatternExists && deleteIndexPattern + ); + } else { + deleteAnalytics(item); + } }; const openModal = () => setModalVisible(true); + const toggleDeleteIndex = () => setDeleteTargetIndex(!deleteTargetIndex); + const toggleDeleteIndexPattern = () => setDeleteIndexPattern(!deleteIndexPattern); const buttonDeleteText = i18n.translate('xpack.ml.dataframe.analyticsList.deleteActionName', { defaultMessage: 'Delete', @@ -84,8 +174,9 @@ export const DeleteAction: FC = ({ item }) => { {deleteButton} {isModalVisible && ( - + = ({ item }) => { buttonColor="danger" >

    - {i18n.translate('xpack.ml.dataframe.analyticsList.deleteModalBody', { - defaultMessage: `Are you sure you want to delete this analytics job? The analytics job's destination index and optional Kibana index pattern will not be deleted.`, - })} +

    + + + + {userCanDeleteIndex && ( + + )} + + + {userCanDeleteIndex && indexPatternExists && ( + + )} + +
    )} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx index fc860251bf83d..0dd9eba172e1c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx @@ -22,7 +22,6 @@ export const ExpandedRowMessagesPane: FC = ({ analyticsId }) => { const getMessagesFactory = () => { let concurrentLoads = 0; - return async function getMessages() { try { concurrentLoads++; @@ -52,8 +51,14 @@ export const ExpandedRowMessagesPane: FC = ({ analyticsId }) => { } }; }; - useRefreshAnalyticsList({ onRefresh: getMessagesFactory() }); - return ; + return ( + + ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts index 7383f565bd673..26cefff0a3f59 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts @@ -3,17 +3,15 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - import { i18n } from '@kbn/i18n'; import { getToastNotifications } from '../../../../../util/dependency_cache'; import { ml } from '../../../../../services/ml_api_service'; - import { refreshAnalyticsList$, REFRESH_ANALYTICS_LIST_STATE } from '../../../../common'; - import { isDataFrameAnalyticsFailed, DataFrameAnalyticsListRow, } from '../../components/analytics_list/common'; +import { extractErrorMessage } from '../../../../../util/error_utils'; export const deleteAnalytics = async (d: DataFrameAnalyticsListRow) => { const toastNotifications = getToastNotifications(); @@ -24,18 +22,139 @@ export const deleteAnalytics = async (d: DataFrameAnalyticsListRow) => { await ml.dataFrameAnalytics.deleteDataFrameAnalytics(d.config.id); toastNotifications.addSuccess( i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsSuccessMessage', { - defaultMessage: 'Request to delete data frame analytics {analyticsId} acknowledged.', + defaultMessage: 'Request to delete data frame analytics job {analyticsId} acknowledged.', values: { analyticsId: d.config.id }, }) ); } catch (e) { + const error = extractErrorMessage(e); + toastNotifications.addDanger( i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage', { defaultMessage: - 'An error occurred deleting the data frame analytics {analyticsId}: {error}', - values: { analyticsId: d.config.id, error: JSON.stringify(e) }, + 'An error occurred deleting the data frame analytics job {analyticsId}: {error}', + values: { analyticsId: d.config.id, error }, }) ); } refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.REFRESH); }; + +export const deleteAnalyticsAndDestIndex = async ( + d: DataFrameAnalyticsListRow, + deleteDestIndex: boolean, + deleteDestIndexPattern: boolean +) => { + const toastNotifications = getToastNotifications(); + const destinationIndex = Array.isArray(d.config.dest.index) + ? d.config.dest.index[0] + : d.config.dest.index; + try { + if (isDataFrameAnalyticsFailed(d.stats.state)) { + await ml.dataFrameAnalytics.stopDataFrameAnalytics(d.config.id, true); + } + const status = await ml.dataFrameAnalytics.deleteDataFrameAnalyticsAndDestIndex( + d.config.id, + deleteDestIndex, + deleteDestIndexPattern + ); + if (status.analyticsJobDeleted?.success) { + toastNotifications.addSuccess( + i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsSuccessMessage', { + defaultMessage: 'Request to delete data frame analytics job {analyticsId} acknowledged.', + values: { analyticsId: d.config.id }, + }) + ); + } + if (status.analyticsJobDeleted?.error) { + const error = extractErrorMessage(status.analyticsJobDeleted.error); + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage', { + defaultMessage: + 'An error occurred deleting the data frame analytics job {analyticsId}: {error}', + values: { analyticsId: d.config.id, error }, + }) + ); + } + + if (status.destIndexDeleted?.success) { + toastNotifications.addSuccess( + i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexSuccessMessage', { + defaultMessage: 'Request to delete destination index {destinationIndex} acknowledged.', + values: { destinationIndex }, + }) + ); + } + if (status.destIndexDeleted?.error) { + const error = extractErrorMessage(status.destIndexDeleted.error); + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexErrorMessage', { + defaultMessage: + 'An error occurred deleting destination index {destinationIndex}: {error}', + values: { destinationIndex, error }, + }) + ); + } + + if (status.destIndexPatternDeleted?.success) { + toastNotifications.addSuccess( + i18n.translate( + 'xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexPatternSuccessMessage', + { + defaultMessage: 'Request to delete index pattern {destinationIndex} acknowledged.', + values: { destinationIndex }, + } + ) + ); + } + if (status.destIndexPatternDeleted?.error) { + const error = extractErrorMessage(status.destIndexPatternDeleted.error); + toastNotifications.addDanger( + i18n.translate( + 'xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexPatternErrorMessage', + { + defaultMessage: 'An error occurred deleting index pattern {destinationIndex}: {error}', + values: { destinationIndex, error }, + } + ) + ); + } + } catch (e) { + const error = extractErrorMessage(e); + + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage', { + defaultMessage: + 'An error occurred deleting the data frame analytics job {analyticsId}: {error}', + values: { analyticsId: d.config.id, error }, + }) + ); + } + refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.REFRESH); +}; + +export const canDeleteIndex = async (indexName: string) => { + const toastNotifications = getToastNotifications(); + try { + const privilege = await ml.hasPrivileges({ + index: [ + { + names: [indexName], // uses wildcard + privileges: ['delete_index'], + }, + ], + }); + if (!privilege) { + return false; + } + return privilege.securityDisabled === true || privilege.has_all_requested === true; + } catch (e) { + const error = extractErrorMessage(e); + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsPrivilegeErrorMessage', { + defaultMessage: 'User does not have permission to delete index {indexName}: {error}', + values: { indexName, error }, + }) + ); + } +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/index.ts index 0d1a87e7c4c1f..68aa58e7e1f19 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/index.ts @@ -3,8 +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. */ - export { getAnalyticsFactory } from './get_analytics'; -export { deleteAnalytics } from './delete_analytics'; +export { deleteAnalytics, deleteAnalyticsAndDestIndex, canDeleteIndex } from './delete_analytics'; export { startAnalytics } from './start_analytics'; export { stopAnalytics } from './stop_analytics'; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx index fbb64db94cd56..486de90d2299c 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx @@ -4,12 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useEffect, useState } from 'react'; - +import React, { FC, useCallback, useEffect, useState } from 'react'; import { ml } from '../../../../services/ml_api_service'; import { JobMessages } from '../../../../components/job_messages'; import { JobMessage } from '../../../../../../common/types/audit_message'; - interface JobMessagesPaneProps { jobId: string; } @@ -32,9 +30,18 @@ export const JobMessagesPane: FC = ({ jobId }) => { } }; + const refreshMessage = useCallback(fetchMessages, [jobId]); + useEffect(() => { fetchMessages(); }, []); - return ; + return ( + + ); }; diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts index 89950a659f609..7cdd5478e3983 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts @@ -10,6 +10,7 @@ import { basePath } from './index'; import { DataFrameAnalyticsStats } from '../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; import { DataFrameAnalyticsConfig } from '../../data_frame_analytics/common'; import { DeepPartial } from '../../../../common/types/common'; +import { DeleteDataFrameAnalyticsWithIndexStatus } from '../../../../common/types/data_frame_analytics'; export interface GetDataFrameAnalyticsStatsResponseOk { node_failures?: object; @@ -32,6 +33,13 @@ interface GetDataFrameAnalyticsResponse { data_frame_analytics: DataFrameAnalyticsConfig[]; } +interface DeleteDataFrameAnalyticsWithIndexResponse { + acknowledged: boolean; + analyticsJobDeleted: DeleteDataFrameAnalyticsWithIndexStatus; + destIndexDeleted: DeleteDataFrameAnalyticsWithIndexStatus; + destIndexPatternDeleted: DeleteDataFrameAnalyticsWithIndexStatus; +} + export const dataFrameAnalytics = { getDataFrameAnalytics(analyticsId?: string) { const analyticsIdString = analyticsId !== undefined ? `/${analyticsId}` : ''; @@ -86,6 +94,17 @@ export const dataFrameAnalytics = { method: 'DELETE', }); }, + deleteDataFrameAnalyticsAndDestIndex( + analyticsId: string, + deleteDestIndex: boolean, + deleteDestIndexPattern: boolean + ) { + return http({ + path: `${basePath()}/data_frame/analytics/${analyticsId}`, + query: { deleteDestIndex, deleteDestIndexPattern }, + method: 'DELETE', + }); + }, startDataFrameAnalytics(analyticsId: string) { return http({ path: `${basePath()}/data_frame/analytics/${analyticsId}/_start`, diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts index 16e25067fd91e..e2569f6217b34 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts @@ -95,7 +95,6 @@ export const jobs = { body, }); }, - closeJobs(jobIds: string[]) { const body = JSON.stringify({ jobIds }); return http({ diff --git a/x-pack/plugins/ml/public/application/util/error_utils.ts b/x-pack/plugins/ml/public/application/util/error_utils.ts new file mode 100644 index 0000000000000..2ce8f4ffc583a --- /dev/null +++ b/x-pack/plugins/ml/public/application/util/error_utils.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 { CustomHttpResponseOptions, ResponseError } from 'kibana/server'; + +export const extractErrorMessage = ( + error: CustomHttpResponseOptions | undefined | string +): string | undefined => { + if (typeof error === 'string') { + return error; + } + + if (error?.body) { + if (typeof error.body === 'string') { + return error.body; + } + if (typeof error.body === 'object' && 'message' in error.body) { + if (typeof error.body.message === 'string') { + return error.body.message; + } + // @ts-ignore + if (typeof (error.body.message?.msg === 'string')) { + // @ts-ignore + return error.body.message?.msg; + } + } + } + return undefined; +}; diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/index_patterns.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/index_patterns.ts new file mode 100644 index 0000000000000..d1a4df768a6ae --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/index_patterns.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 { SavedObjectsClientContract } from 'kibana/server'; +import { IIndexPattern } from 'src/plugins/data/server'; + +export class IndexPatternHandler { + constructor(private savedObjectsClient: SavedObjectsClientContract) {} + // returns a id based on an index pattern name + async getIndexPatternId(indexName: string) { + const response = await this.savedObjectsClient.find({ + type: 'index-pattern', + perPage: 10, + search: `"${indexName}"`, + searchFields: ['title'], + fields: ['title'], + }); + + const ip = response.saved_objects.find( + (obj) => obj.attributes.title.toLowerCase() === indexName.toLowerCase() + ); + + return ip?.id; + } + + async deleteIndexPatternById(indexId: string) { + return await this.savedObjectsClient.delete('index-pattern', indexId); + } +} diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 969b74194148b..b167214cc33cf 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -133,7 +133,7 @@ export class MlServerPlugin implements Plugin { + if (!mlLicense.isSecurityEnabled()) { + return true; + } + const privilege = await context.ml!.mlClient.callAsCurrentUser('ml.privilegeCheck', { + body: { + index: [ + { + names: [destinationIndex], // uses wildcard + privileges: ['delete_index'], + }, + ], + }, + }); + if (!privilege) { + return false; + } + return privilege.has_all_requested === true; + } + /** * @apiGroup DataFrameAnalytics * @@ -277,6 +314,7 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat path: '/api/ml/data_frame/analytics/{analyticsId}', validate: { params: analyticsIdSchema, + query: deleteDataFrameAnalyticsJobSchema, }, options: { tags: ['access:ml:canDeleteDataFrameAnalytics'], @@ -285,12 +323,78 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { analyticsId } = request.params; - const results = await context.ml!.mlClient.callAsCurrentUser( - 'ml.deleteDataFrameAnalytics', - { + const { deleteDestIndex, deleteDestIndexPattern } = request.query; + let destinationIndex: string | undefined; + const analyticsJobDeleted: DeleteDataFrameAnalyticsWithIndexStatus = { success: false }; + const destIndexDeleted: DeleteDataFrameAnalyticsWithIndexStatus = { success: false }; + const destIndexPatternDeleted: DeleteDataFrameAnalyticsWithIndexStatus = { + success: false, + }; + + // Check if analyticsId is valid and get destination index + if (deleteDestIndex || deleteDestIndexPattern) { + try { + const dfa = await context.ml!.mlClient.callAsCurrentUser('ml.getDataFrameAnalytics', { + analyticsId, + }); + if (Array.isArray(dfa.data_frame_analytics) && dfa.data_frame_analytics.length > 0) { + destinationIndex = dfa.data_frame_analytics[0].dest.index; + } + } catch (e) { + return response.customError(wrapError(e)); + } + + // If user checks box to delete the destinationIndex associated with the job + if (destinationIndex && deleteDestIndex) { + // Verify if user has privilege to delete the destination index + const userCanDeleteDestIndex = await userCanDeleteIndex(context, destinationIndex); + // If user does have privilege to delete the index, then delete the index + if (userCanDeleteDestIndex) { + try { + await context.ml!.mlClient.callAsCurrentUser('indices.delete', { + index: destinationIndex, + }); + destIndexDeleted.success = true; + } catch (deleteIndexError) { + destIndexDeleted.error = wrapError(deleteIndexError); + } + } else { + return response.forbidden(); + } + } + + // Delete the index pattern if there's an index pattern that matches the name of dest index + if (destinationIndex && deleteDestIndexPattern) { + try { + const indexPatternId = await getIndexPatternId(context, destinationIndex); + if (indexPatternId) { + await deleteDestIndexPatternById(context, indexPatternId); + } + destIndexPatternDeleted.success = true; + } catch (deleteDestIndexPatternError) { + destIndexPatternDeleted.error = wrapError(deleteDestIndexPatternError); + } + } + } + // Grab the target index from the data frame analytics job id + // Delete the data frame analytics + + try { + await context.ml!.mlClient.callAsCurrentUser('ml.deleteDataFrameAnalytics', { analyticsId, + }); + analyticsJobDeleted.success = true; + } catch (deleteDFAError) { + analyticsJobDeleted.error = wrapError(deleteDFAError); + if (analyticsJobDeleted.error.statusCode === 404) { + return response.notFound(); } - ); + } + const results = { + analyticsJobDeleted, + destIndexDeleted, + destIndexPatternDeleted, + }; return response.ok({ body: results, }); 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/routes/schemas/data_analytics_schema.ts b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts index f1d4947a7abc5..0b2469c103578 100644 --- a/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts @@ -60,6 +60,14 @@ export const analyticsIdSchema = schema.object({ analyticsId: schema.string(), }); +export const deleteDataFrameAnalyticsJobSchema = schema.object({ + /** + * Analytics Destination Index + */ + deleteDestIndex: schema.maybe(schema.boolean()), + deleteDestIndexPattern: schema.maybe(schema.boolean()), +}); + export const stopsDataFrameAnalyticsJobQuerySchema = schema.object({ force: schema.maybe(schema.boolean()), }); 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/monitoring/kibana.json b/x-pack/plugins/monitoring/kibana.json index 115cc08871ea4..4ed693464712d 100644 --- a/x-pack/plugins/monitoring/kibana.json +++ b/x-pack/plugins/monitoring/kibana.json @@ -4,7 +4,7 @@ "kibanaVersion": "kibana", "configPath": ["monitoring"], "requiredPlugins": ["licensing", "features", "data", "navigation", "kibanaLegacy"], - "optionalPlugins": ["alerting", "actions", "infra", "telemetryCollectionManager", "usageCollection", "home"], + "optionalPlugins": ["alerts", "actions", "infra", "telemetryCollectionManager", "usageCollection", "home"], "server": true, "ui": true } diff --git a/x-pack/plugins/monitoring/public/components/alerts/status.tsx b/x-pack/plugins/monitoring/public/components/alerts/status.tsx index cdddbf1031303..6f72168f5069b 100644 --- a/x-pack/plugins/monitoring/public/components/alerts/status.tsx +++ b/x-pack/plugins/monitoring/public/components/alerts/status.tsx @@ -18,7 +18,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { Legacy } from '../../legacy_shims'; -import { Alert, BASE_ALERT_API_PATH } from '../../../../../plugins/alerting/common'; +import { Alert, BASE_ALERT_API_PATH } from '../../../../alerts/common'; import { getSetupModeState, addSetupModeCallback, toggleSetupMode } from '../../lib/setup_mode'; import { NUMBER_OF_MIGRATED_ALERTS, ALERT_TYPE_PREFIX } from '../../../common/constants'; import { AlertsConfiguration } from './configuration'; diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_state.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_state.test.ts index bcc1a8abe5cb0..6262036037712 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_state.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_state.test.ts @@ -10,7 +10,7 @@ import { AlertCommonParams, AlertCommonState, AlertClusterStatePerClusterState } import { getPreparedAlert } from '../lib/alerts/get_prepared_alert'; import { executeActions } from '../lib/alerts/cluster_state.lib'; import { AlertClusterStateState } from './enums'; -import { alertsMock, AlertServicesMock } from '../../../alerting/server/mocks'; +import { alertsMock, AlertServicesMock } from '../../../alerts/server/mocks'; jest.mock('../lib/alerts/cluster_state.lib', () => ({ executeActions: jest.fn(), diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_state.ts b/x-pack/plugins/monitoring/server/alerts/cluster_state.ts index 6567d1c6def31..5b6521179002a 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_state.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_state.ts @@ -8,7 +8,7 @@ import moment from 'moment-timezone'; import { i18n } from '@kbn/i18n'; import { Logger, ICustomClusterClient, UiSettingsServiceStart } from 'src/core/server'; import { ALERT_TYPE_CLUSTER_STATE } from '../../common/constants'; -import { AlertType } from '../../../alerting/server'; +import { AlertType } from '../../../alerts/server'; import { executeActions, getUiMessage } from '../lib/alerts/cluster_state.lib'; import { AlertCommonExecutorOptions, diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts index 6ffe937679f4c..fb8d10884fdc7 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration.test.ts @@ -16,7 +16,7 @@ import { } from './types'; import { executeActions } from '../lib/alerts/license_expiration.lib'; import { PreparedAlert, getPreparedAlert } from '../lib/alerts/get_prepared_alert'; -import { alertsMock, AlertServicesMock } from '../../../alerting/server/mocks'; +import { alertsMock, AlertServicesMock } from '../../../alerts/server/mocks'; jest.mock('../lib/alerts/license_expiration.lib', () => ({ executeActions: jest.fn(), diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration.ts index 00402bca57a7e..d57f1a7655b18 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration.ts @@ -8,7 +8,7 @@ import moment from 'moment-timezone'; import { Logger, ICustomClusterClient, UiSettingsServiceStart } from 'src/core/server'; import { i18n } from '@kbn/i18n'; import { ALERT_TYPE_LICENSE_EXPIRATION } from '../../common/constants'; -import { AlertType } from '../../../../plugins/alerting/server'; +import { AlertType } from '../../../alerts/server'; import { fetchLicenses } from '../lib/alerts/fetch_licenses'; import { AlertCommonState, diff --git a/x-pack/plugins/monitoring/server/alerts/types.d.ts b/x-pack/plugins/monitoring/server/alerts/types.d.ts index b689d008b51a7..67c74635b4e36 100644 --- a/x-pack/plugins/monitoring/server/alerts/types.d.ts +++ b/x-pack/plugins/monitoring/server/alerts/types.d.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { Moment } from 'moment'; -import { AlertExecutorOptions } from '../../../alerting/server'; +import { AlertExecutorOptions } from '../../../alerts/server'; import { AlertClusterStateState, AlertCommonPerClusterMessageTokenType } from './enums'; export interface AlertLicense { diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js b/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js index 7817a2ced4b82..6035837bac85d 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js @@ -50,15 +50,15 @@ export class BulkUploader { this._usageInterval = TELEMETRY_COLLECTION_INTERVAL; this._log = log; - this._cluster = elasticsearch.createClient('admin', { + this._cluster = elasticsearch.legacy.createClient('admin', { plugins: [monitoringBulk], }); if (hasMonitoringCluster(config.elasticsearch)) { this._log.info(`Detected direct connection to monitoring cluster`); this._hasDirectConnectionToMonitoringCluster = true; - this._cluster = elasticsearch.createClient('monitoring-direct', config.elasticsearch); - elasticsearch.adminClient.callAsInternalUser('info').then((data) => { + this._cluster = elasticsearch.legacy.createClient('monitoring-direct', config.elasticsearch); + elasticsearch.legacy.client.callAsInternalUser('info').then((data) => { this._productionClusterUuid = get(data, 'cluster_uuid'); }); } diff --git a/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.ts b/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.ts index ae66d603507ca..c4553d87980da 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/cluster_state.lib.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { AlertInstance } from '../../../../alerting/server'; +import { AlertInstance } from '../../../../alerts/server'; import { AlertCommonCluster, AlertCommonPerClusterMessage, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts index 7a6c38865ebe8..614658baf5c79 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts @@ -6,7 +6,7 @@ import moment from 'moment'; import { Logger } from '../../../../../../src/core/server'; import { AlertCommonPerClusterState } from '../../alerts/types'; -import { AlertsClient } from '../../../../alerting/server'; +import { AlertsClient } from '../../../../alerts/server'; export async function fetchStatus( alertsClient: AlertsClient, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.ts b/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.ts index 83a9e26e4c589..cfaaeb36535a0 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/get_prepared_alert.ts @@ -6,7 +6,7 @@ import { Logger, ICustomClusterClient, UiSettingsServiceStart } from 'kibana/server'; import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; -import { AlertServices } from '../../../../alerting/server'; +import { AlertServices } from '../../../../alerts/server'; import { AlertCommonCluster } from '../../alerts/types'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../common/constants'; import { fetchAvailableCcs } from './fetch_available_ccs'; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts b/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts index cfe9f02b9bd6a..97ef2790b516d 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts @@ -5,7 +5,7 @@ */ import { Moment } from 'moment-timezone'; import { i18n } from '@kbn/i18n'; -import { AlertInstance } from '../../../../alerting/server'; +import { AlertInstance } from '../../../../alerts/server'; import { AlertCommonPerClusterMessageLinkToken, AlertCommonPerClusterMessageTimeToken, diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index a45e80ac71d65..f4f38f70b1ccb 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -47,7 +47,7 @@ import { MonitoringLicenseService } from './types'; import { PluginStartContract as AlertingPluginStartContract, PluginSetupContract as AlertingPluginSetupContract, -} from '../../alerting/server'; +} from '../../alerts/server'; import { getLicenseExpiration } from './alerts/license_expiration'; import { getClusterState } from './alerts/cluster_state'; import { InfraPluginSetup } from '../../infra/server'; @@ -61,12 +61,12 @@ interface PluginsSetup { usageCollection?: UsageCollectionSetup; licensing: LicensingPluginSetup; features: FeaturesPluginSetupContract; - alerting: AlertingPluginSetupContract; + alerts: AlertingPluginSetupContract; infra: InfraPluginSetup; } interface PluginsStart { - alerting: AlertingPluginStartContract; + alerts: AlertingPluginStartContract; } interface MonitoringCoreConfig { @@ -131,7 +131,7 @@ export class Plugin { this.legacyShimDependencies = { router: core.http.createRouter(), instanceUuid: core.uuid.getInstanceUuid(), - esDataClient: core.elasticsearch.dataClient, + esDataClient: core.elasticsearch.legacy.client, kibanaStatsCollector: plugins.usageCollection?.getCollectorByType( KIBANA_STATS_TYPE_MONITORING ), @@ -142,7 +142,7 @@ export class Plugin { const cluster = (this.cluster = instantiateClient( config.ui.elasticsearch, this.log, - core.elasticsearch.createClient + core.elasticsearch.legacy.createClient )); // Start our license service which will ensure @@ -156,7 +156,7 @@ export class Plugin { await this.licenseService.refresh(); if (KIBANA_ALERTING_ENABLED) { - plugins.alerting.registerType( + plugins.alerts.registerType( getLicenseExpiration( async () => { const coreStart = (await core.getStartServices())[0]; @@ -167,7 +167,7 @@ export class Plugin { config.ui.ccs.enabled ) ); - plugins.alerting.registerType( + plugins.alerts.registerType( getClusterState( async () => { const coreStart = (await core.getStartServices())[0]; @@ -357,7 +357,7 @@ export class Plugin { payload: req.body, getKibanaStatsCollector: () => this.legacyShimDependencies.kibanaStatsCollector, getUiSettingsService: () => context.core.uiSettings.client, - getAlertsClient: () => plugins.alerting.getAlertsClientWithRequest(req), + getAlertsClient: () => plugins.alerts.getAlertsClientWithRequest(req), server: { config: legacyConfigWrapper, newPlatform: { diff --git a/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts b/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts index 346cc75bb9b24..b15ead36a75f6 100644 --- a/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts +++ b/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts @@ -6,6 +6,8 @@ import { Observable } from 'rxjs'; import _, { countBy, groupBy, mapValues } from 'lodash'; +import { first } from 'rxjs/operators'; + import { APICaller, IClusterClient } from 'src/core/server'; import { getNextMidnight } from '../../get_next_midnight'; import { TaskInstance } from '../../../../../task_manager/server'; @@ -80,7 +82,7 @@ export function visualizationsTaskRunner( let error; try { - const index = (await config.toPromise()).kibana.index; + const index = (await config.pipe(first()).toPromise()).kibana.index; stats = await getStats((await esClientPromise).callAsInternalUser, index); } catch (err) { if (err.constructor === Error) { diff --git a/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts index cbfbbfd2d61f3..d28e95834ca0b 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts @@ -28,7 +28,7 @@ describe('ADD remote clusters', () => { { licenseCheckResult = { valid: true }, apiResponses = [], asserts, payload }: TestOptions ) => { test(description, async () => { - const { adminClient: elasticsearchMock } = elasticsearchServiceMock.createSetup(); + const elasticsearchMock = elasticsearchServiceMock.createClusterClient(); const mockRouteDependencies = { router: httpServiceMock.createRouter(), diff --git a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts index b9328cb61e967..d1e3cf89e94d9 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts @@ -30,7 +30,7 @@ describe('DELETE remote clusters', () => { { licenseCheckResult = { valid: true }, apiResponses = [], asserts, params }: TestOptions ) => { test(description, async () => { - const { adminClient: elasticsearchMock } = elasticsearchServiceMock.createSetup(); + const elasticsearchMock = elasticsearchServiceMock.createClusterClient(); const mockRouteDependencies = { router: httpServiceMock.createRouter(), diff --git a/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts index f0444162e80b9..24e469c9ec9b2 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts @@ -29,7 +29,7 @@ describe('GET remote clusters', () => { { licenseCheckResult = { valid: true }, apiResponses = [], asserts }: TestOptions ) => { test(description, async () => { - const { adminClient: elasticsearchMock } = elasticsearchServiceMock.createSetup(); + const elasticsearchMock = elasticsearchServiceMock.createClusterClient(); const mockRouteDependencies = { router: httpServiceMock.createRouter(), diff --git a/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts index 7f8acb2b018d9..9669c98e1349e 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts @@ -37,7 +37,7 @@ describe('UPDATE remote clusters', () => { }: TestOptions ) => { test(description, async () => { - const { adminClient: elasticsearchMock } = elasticsearchServiceMock.createSetup(); + const elasticsearchMock = elasticsearchServiceMock.createClusterClient(); const mockRouteDependencies = { router: httpServiceMock.createRouter(), diff --git a/x-pack/plugins/reporting/kibana.json b/x-pack/plugins/reporting/kibana.json index d068711b87c9d..e44bd92c42391 100644 --- a/x-pack/plugins/reporting/kibana.json +++ b/x-pack/plugins/reporting/kibana.json @@ -13,7 +13,8 @@ "uiActions", "embeddable", "share", - "kibanaLegacy" + "kibanaLegacy", + "licensing" ], "server": true, "ui": true 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..3e30ff9447f3e 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'], @@ -38,9 +39,7 @@ describe('Security Plugin', () => { mockCoreSetup.http.isTlsEnabled = true; mockClusterClient = elasticsearchServiceMock.createCustomClusterClient(); - mockCoreSetup.elasticsearch.createClient.mockReturnValue( - (mockClusterClient as unknown) as jest.Mocked - ); + mockCoreSetup.elasticsearch.legacy.createClient.mockReturnValue(mockClusterClient); mockDependencies = { licensing: { license$: of({}) } } as PluginSetupDependencies; }); @@ -50,9 +49,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], @@ -111,8 +112,8 @@ describe('Security Plugin', () => { it('properly creates cluster client instance', async () => { await plugin.setup(mockCoreSetup, mockDependencies); - expect(mockCoreSetup.elasticsearch.createClient).toHaveBeenCalledTimes(1); - expect(mockCoreSetup.elasticsearch.createClient).toHaveBeenCalledWith('security', { + expect(mockCoreSetup.elasticsearch.legacy.createClient).toHaveBeenCalledTimes(1); + expect(mockCoreSetup.elasticsearch.legacy.createClient).toHaveBeenCalledWith('security', { plugins: [elasticsearchClientPlugin], }); }); diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 89cffde92d564..bdda0be9b15a7 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. @@ -126,7 +109,7 @@ export class Plugin { .pipe(first()) .toPromise(); - this.clusterClient = core.elasticsearch.createClient('security', { + this.clusterClient = core.elasticsearch.legacy.createClient('security', { plugins: [elasticsearchClientPlugin], }); @@ -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/server/lib/detection_engine/routes/schemas/response/schemas.ts b/x-pack/plugins/siem/common/detection_engine/schemas/common/schemas.ts similarity index 99% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/common/schemas.ts index faae1dde83545..9eb2d9abccbd3 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/common/schemas.ts @@ -27,7 +27,7 @@ export const filters = t.array(t.unknown); // Filters are not easily type-able y /** * Params is an "object", since it is a type of AlertActionParams which is action templates. - * @see x-pack/plugins/alerting/common/alert.ts + * @see x-pack/plugins/alerts/common/alert.ts */ export const action_group = t.string; export const action_id = t.string; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/__mocks__/utils.ts similarity index 100% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/__mocks__/utils.ts diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/error_schema.test.ts similarity index 86% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/error_schema.test.ts index 9bbde3d5236db..2a4d75522d010 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/error_schema.test.ts @@ -9,19 +9,10 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { getErrorPayload } from './__mocks__/utils'; import { errorSchema, ErrorSchema } from './error_schema'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; -import { exactCheck } from '../../../../../../common/exact_check'; -import { getPaths, foldLeftRight } from '../../../../../../common/test_utils'; +import { exactCheck } from '../../../exact_check'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('error_schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - test('it should validate an error with a UUID given for id', () => { const error = getErrorPayload(); const decoded = errorSchema.decode(getErrorPayload()); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/error_schema.ts similarity index 92% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/error_schema.ts index f9c776e3b3cdc..986d3ad87ec85 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/error_schema.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; /* eslint-disable @typescript-eslint/camelcase */ -import { rule_id, status_code, message } from './schemas'; +import { rule_id, status_code, message } from '../common/schemas'; /* eslint-enable @typescript-eslint/camelcase */ // We use id: t.string intentionally and _never_ the id from global schemas as diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/find_rules_schema.test.ts similarity index 93% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/find_rules_schema.test.ts index 1b7d7994462c7..51163c3d76ed6 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/find_rules_schema.test.ts @@ -9,19 +9,10 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { getFindResponseSingle, getBaseResponsePayload } from './__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; import { RulesSchema } from './rules_schema'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; -import { getPaths, foldLeftRight } from '../../../../../../common/test_utils'; -import { exactCheck } from '../../../../../../common/exact_check'; +import { exactCheck } from '../../../exact_check'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('find_rules_schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - test('it should validate a typical single find rules response', () => { const payload = getFindResponseSingle(); const decoded = findRulesSchema.decode(payload); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/find_rules_schema.ts similarity index 89% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/find_rules_schema.ts index d7e8a246cfe01..77077ce2e22ac 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/find_rules_schema.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; import { rulesSchema } from './rules_schema'; -import { page, perPage, total } from './schemas'; +import { page, perPage, total } from '../common/schemas'; export const findRulesSchema = t.exact( t.type({ diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/import_rules_schema.test.ts similarity index 92% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/import_rules_schema.test.ts index 18e17a319883a..d7efe4b30af11 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/import_rules_schema.test.ts @@ -8,20 +8,11 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left, Either } from 'fp-ts/lib/Either'; import { ImportRulesSchema, importRulesSchema } from './import_rules_schema'; import { ErrorSchema } from './error_schema'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; import { Errors } from 'io-ts'; -import { exactCheck } from '../../../../../../common/exact_check'; -import { foldLeftRight, getPaths } from '../../../../../../common/test_utils'; +import { exactCheck } from '../../../exact_check'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('import_rules_schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - test('it should validate an empty import response with no errors', () => { const payload: ImportRulesSchema = { success: true, success_count: 0, errors: [] }; const decoded = importRulesSchema.decode(payload); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/import_rules_schema.ts similarity index 91% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/import_rules_schema.ts index dec32b18e2b24..adea77e7b933f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/import_rules_schema.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; /* eslint-disable @typescript-eslint/camelcase */ -import { success, success_count } from './schemas'; +import { success, success_count } from '../common/schemas'; import { errorSchema } from './error_schema'; /* eslint-enable @typescript-eslint/camelcase */ diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_schema.test.ts similarity index 90% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_schema.test.ts index 2d3fd75914822..fc3f89996daf1 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_schema.test.ts @@ -7,19 +7,10 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { PrePackagedRulesSchema, prePackagedRulesSchema } from './prepackaged_rules_schema'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; -import { exactCheck } from '../../../../../../common/exact_check'; -import { getPaths, foldLeftRight } from '../../../../../../common/test_utils'; +import { exactCheck } from '../../../exact_check'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('prepackaged_rules_schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - test('it should validate an empty prepackaged response with defaults', () => { const payload: PrePackagedRulesSchema = { rules_installed: 0, rules_updated: 0 }; const decoded = prePackagedRulesSchema.decode(payload); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_schema.ts similarity index 89% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_schema.ts index f0eff0ba19753..3b0107c91fee0 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_schema.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; /* eslint-disable @typescript-eslint/camelcase */ -import { rules_installed, rules_updated } from './schemas'; +import { rules_installed, rules_updated } from '../common/schemas'; /* eslint-enable @typescript-eslint/camelcase */ export const prePackagedRulesSchema = t.exact( diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_status_schema.test.ts similarity index 92% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_status_schema.test.ts index abe601a546111..eeae72209829e 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_status_schema.test.ts @@ -10,19 +10,10 @@ import { PrePackagedRulesStatusSchema, prePackagedRulesStatusSchema, } from './prepackaged_rules_status_schema'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; -import { exactCheck } from '../../../../../../common/exact_check'; -import { foldLeftRight, getPaths } from '../../../../../../common/test_utils'; +import { exactCheck } from '../../../exact_check'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('prepackaged_rules_schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - test('it should validate an empty prepackaged response with defaults', () => { const payload: PrePackagedRulesStatusSchema = { rules_installed: 0, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_status_schema.ts similarity index 96% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_status_schema.ts index 72e5821eb4697..ee8e7b48a58bc 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/prepackaged_rules_status_schema.ts @@ -12,7 +12,7 @@ import { rules_custom_installed, rules_not_installed, rules_not_updated, -} from './schemas'; +} from '../common/schemas'; /* eslint-enable @typescript-eslint/camelcase */ export const prePackagedRulesStatusSchema = t.exact( diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/rules_bulk_schema.test.ts similarity index 93% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/rules_bulk_schema.test.ts index 98cb2ef058485..04cf012f36dba 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/rules_bulk_schema.test.ts @@ -11,19 +11,10 @@ import { getBaseResponsePayload, getErrorPayload } from './__mocks__/utils'; import { RulesBulkSchema, rulesBulkSchema } from './rules_bulk_schema'; import { RulesSchema } from './rules_schema'; import { ErrorSchema } from './error_schema'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; -import { exactCheck } from '../../../../../../common/exact_check'; -import { foldLeftRight, getPaths } from '../../../../../../common/test_utils'; +import { exactCheck } from '../../../exact_check'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('prepackaged_rule_schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - test('it should validate a regular message and and error together with a uuid', () => { const payload: RulesBulkSchema = [getBaseResponsePayload(), getErrorPayload()]; const decoded = rulesBulkSchema.decode(payload); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/rules_bulk_schema.ts similarity index 100% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/rules_bulk_schema.ts diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/rules_schema.test.ts similarity index 70% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/rules_schema.test.ts index 0b0d3bf43b1e9..8ed9c30507f4f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/rules_schema.test.ts @@ -4,32 +4,209 @@ * you may not use this file except in compliance with the Elastic License. */ +import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import * as t from 'io-ts'; import { + rulesSchema, + RulesSchema, checkTypeDependents, getDependents, addSavedId, - addTimelineTitle, addQueryFields, + addTimelineTitle, addMlFields, -} from './check_type_dependents'; +} from './rules_schema'; import { getBaseResponsePayload, getMlRuleResponsePayload } from './__mocks__/utils'; -import { left } from 'fp-ts/lib/Either'; -import { RulesSchema } from './rules_schema'; +import { exactCheck } from '../../../exact_check'; +import { foldLeftRight, getPaths } from '../../../test_utils'; import { TypeAndTimelineOnly } from './type_timeline_only_schema'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; -import { exactCheck } from '../../../../../../common/exact_check'; -import { foldLeftRight, getPaths } from '../../../../../../common/test_utils'; -describe('check_type_dependents', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); +export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z'; + +describe('rules_schema', () => { + test('it should validate a type of "query" without anything extra', () => { + const payload = getBaseResponsePayload(); + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = getBaseResponsePayload(); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + + test('it should NOT validate a type of "query" when it has extra data', () => { + const payload: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); + payload.invalid_extra_data = 'invalid_extra_data'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate invalid_data for the type', () => { + const payload: Omit & { type: string } = getBaseResponsePayload(); + payload.type = 'invalid_data'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "invalid_data" supplied to "type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "query" with a saved_id together', () => { + const payload = getBaseResponsePayload(); + payload.type = 'query'; + payload.saved_id = 'save id 123'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "saved_id"']); + expect(message.schema).toEqual({}); + }); + + test('it should validate a type of "saved_query" with a "saved_id" dependent', () => { + const payload = getBaseResponsePayload(); + payload.type = 'saved_query'; + payload.saved_id = 'save id 123'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = getBaseResponsePayload(); + + expected.type = 'saved_query'; + expected.saved_id = 'save id 123'; + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + + test('it should NOT validate a type of "saved_query" without a "saved_id" dependent', () => { + const payload = getBaseResponsePayload(); + payload.type = 'saved_query'; + delete payload.saved_id; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "saved_id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "saved_query" when it has extra data', () => { + const payload: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); + payload.type = 'saved_query'; + payload.saved_id = 'save id 123'; + payload.invalid_extra_data = 'invalid_extra_data'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); + expect(message.schema).toEqual({}); + }); + + test('it should validate a type of "timeline_id" if there is a "timeline_title" dependent', () => { + const payload = getBaseResponsePayload(); + payload.timeline_id = 'some timeline id'; + payload.timeline_title = 'some timeline title'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = getBaseResponsePayload(); + expected.timeline_id = 'some timeline id'; + expected.timeline_title = 'some timeline title'; + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); }); - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); + test('it should NOT validate a type of "timeline_id" if there is "timeline_title" dependent when it has extra invalid data', () => { + const payload: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); + payload.timeline_id = 'some timeline id'; + payload.timeline_title = 'some timeline title'; + payload.invalid_extra_data = 'invalid_extra_data'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "timeline_id" if there is NOT a "timeline_title" dependent', () => { + const payload = getBaseResponsePayload(); + payload.timeline_id = 'some timeline id'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "timeline_title"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "timeline_title" if there is NOT a "timeline_id" dependent', () => { + const payload = getBaseResponsePayload(); + payload.timeline_title = 'some timeline title'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "timeline_title"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "saved_query" with a "saved_id" dependent and a "timeline_title" but there is NOT a "timeline_id"', () => { + const payload = getBaseResponsePayload(); + payload.saved_id = 'some saved id'; + payload.type = 'saved_query'; + payload.timeline_title = 'some timeline title'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "timeline_title"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "saved_query" with a "saved_id" dependent and a "timeline_id" but there is NOT a "timeline_title"', () => { + const payload = getBaseResponsePayload(); + payload.saved_id = 'some saved id'; + payload.type = 'saved_query'; + payload.timeline_id = 'some timeline id'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "timeline_title"', + ]); + expect(message.schema).toEqual({}); }); describe('checkTypeDependents', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/rules_schema.ts similarity index 54% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/rules_schema.ts index fb1ee8e670e31..a7a31ec9e1b59 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/rules_schema.ts @@ -7,10 +7,11 @@ /* eslint-disable @typescript-eslint/camelcase */ import * as t from 'io-ts'; import { isObject } from 'lodash/fp'; -import { Either, fold, right, left } from 'fp-ts/lib/Either'; - +import { Either, left, fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { checkTypeDependents } from './check_type_dependents'; +import { typeAndTimelineOnlySchema, TypeAndTimelineOnly } from './type_timeline_only_schema'; +import { isMlRule } from '../../../machine_learning/helpers'; + import { actions, anomaly_threshold, @@ -54,9 +55,8 @@ import { filters, meta, note, -} from './schemas'; +} from '../common/schemas'; import { ListsDefaultArray } from '../types/lists_default_array'; -import { hasListsFeature } from '../../../feature_flags'; /** * This is the required fields for the rules schema response. Put all required properties on @@ -155,32 +155,92 @@ export const rulesSchema = new t.Type< 'RulesSchema', (input: unknown): input is RulesWithoutTypeDependentsSchema => isObject(input), (input): Either => { - const output = checkTypeDependents(input); - if (!hasListsFeature()) { - // TODO: (LIST-FEATURE) Remove this after the lists feature is an accepted feature for a particular release - return removeList(output); - } else { - return output; - } + return checkTypeDependents(input); }, t.identity ); -// TODO: (LIST-FEATURE) Remove this after the lists feature is an accepted feature for a particular release -export const removeList = ( - decoded: Either -): Either => { - const onLeft = (errors: t.Errors): Either => left(errors); - const onRight = (decodedValue: RequiredRulesSchema): Either => { - delete decodedValue.exceptions_list; - return right(decodedValue); - }; - const folded = fold(onLeft, onRight); - return pipe(decoded, folded); -}; - /** * This is the correct type you want to use for Rules that are outputted from the * REST interface. This has all base and all optional properties merged together. */ export type RulesSchema = t.TypeOf; + +export const addSavedId = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { + if (typeAndTimelineOnly.type === 'saved_query') { + return [t.exact(t.type({ saved_id: dependentRulesSchema.props.saved_id }))]; + } else { + return []; + } +}; + +export const addTimelineTitle = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { + if (typeAndTimelineOnly.timeline_id != null) { + return [ + t.exact(t.type({ timeline_title: dependentRulesSchema.props.timeline_title })), + t.exact(t.type({ timeline_id: dependentRulesSchema.props.timeline_id })), + ]; + } else { + return []; + } +}; + +export const addQueryFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { + if (typeAndTimelineOnly.type === 'query' || typeAndTimelineOnly.type === 'saved_query') { + return [ + t.exact(t.type({ query: dependentRulesSchema.props.query })), + t.exact(t.type({ language: dependentRulesSchema.props.language })), + ]; + } else { + return []; + } +}; + +export const addMlFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { + if (isMlRule(typeAndTimelineOnly.type)) { + return [ + t.exact(t.type({ anomaly_threshold: dependentRulesSchema.props.anomaly_threshold })), + t.exact( + t.type({ machine_learning_job_id: dependentRulesSchema.props.machine_learning_job_id }) + ), + ]; + } else { + return []; + } +}; + +export const getDependents = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed => { + const dependents: t.Mixed[] = [ + t.exact(requiredRulesSchema), + t.exact(partialRulesSchema), + ...addSavedId(typeAndTimelineOnly), + ...addTimelineTitle(typeAndTimelineOnly), + ...addQueryFields(typeAndTimelineOnly), + ...addMlFields(typeAndTimelineOnly), + ]; + + if (dependents.length > 1) { + // This unsafe cast is because t.intersection does not use an array but rather a set of + // tuples and really does not look like they expected us to ever dynamically build up + // intersections, but here we are doing that. Looking at their code, although they limit + // the array elements to 5, it looks like you have N number of intersections + const unsafeCast: [t.Mixed, t.Mixed] = dependents as [t.Mixed, t.Mixed]; + return t.intersection(unsafeCast); + } else { + // We are not allowed to call t.intersection with a single value so we return without + // it here normally. + return dependents[0]; + } +}; + +export const checkTypeDependents = (input: unknown): Either => { + const typeOnlyDecoded = typeAndTimelineOnlySchema.decode(input); + const onLeft = (errors: t.Errors): Either => left(errors); + const onRight = ( + typeAndTimelineOnly: TypeAndTimelineOnly + ): Either => { + const intersections = getDependents(typeAndTimelineOnly); + return intersections.decode(input); + }; + return pipe(typeOnlyDecoded, fold(onLeft, onRight)); +}; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/type_timeline_only_schema.test.ts similarity index 85% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/type_timeline_only_schema.test.ts index 8f06e2c6e49b0..c7335ffd62f02 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/type_timeline_only_schema.test.ts @@ -8,19 +8,10 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import { TypeAndTimelineOnly, typeAndTimelineOnlySchema } from './type_timeline_only_schema'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; -import { exactCheck } from '../../../../../../common/exact_check'; -import { foldLeftRight, getPaths } from '../../../../../../common/test_utils'; +import { exactCheck } from '../../../exact_check'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('prepackaged_rule_schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - test('it should validate a a type and timeline_id together', () => { const payload: TypeAndTimelineOnly = { type: 'query', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.ts b/x-pack/plugins/siem/common/detection_engine/schemas/response/type_timeline_only_schema.ts similarity index 92% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/response/type_timeline_only_schema.ts index 6d11ff03563d1..d23d4ad2e83d4 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/response/type_timeline_only_schema.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; /* eslint-disable @typescript-eslint/camelcase */ -import { timeline_id, type } from './schemas'; +import { timeline_id, type } from '../common/schemas'; /* eslint-enable @typescript-eslint/camelcase */ /** diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/iso_date_string.test.ts similarity index 95% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/iso_date_string.test.ts index 9f9181359d44a..e8bce3f38f4b3 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/types/iso_date_string.test.ts @@ -7,7 +7,7 @@ import { IsoDateString } from './iso_date_string'; import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { getPaths, foldLeftRight } from '../../../../../../common/test_utils'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('ios_date_string', () => { test('it should validate a iso string', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/iso_date_string.ts similarity index 100% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/iso_date_string.ts diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/lists_default_array.test.ts similarity index 98% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/lists_default_array.test.ts index dc0bd6cacf0d6..31e0a8e5c2c73 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/types/lists_default_array.test.ts @@ -7,7 +7,7 @@ import { ListsDefaultArray } from './lists_default_array'; import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { getPaths, foldLeftRight } from '../../../../../../common/test_utils'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('lists_default_array', () => { test('it should validate an empty array', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/lists_default_array.ts similarity index 97% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/lists_default_array.ts index 743914ad070a2..8244f4a29e193 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/types/lists_default_array.ts @@ -11,7 +11,7 @@ import { list_and as listAnd, list_values as listValues, list_values_operator as listOperator, -} from '../response/schemas'; +} from '../common/schemas'; export type ListsDefaultArrayC = t.Type; export type List = t.TypeOf; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/positive_integer.ts similarity index 100% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/positive_integer.ts diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/positive_integer_greater_than_zero.test.ts similarity index 95% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/positive_integer_greater_than_zero.test.ts index a3338c878bd71..821eb066a6531 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/types/positive_integer_greater_than_zero.test.ts @@ -7,7 +7,7 @@ import { PositiveIntegerGreaterThanZero } from './positive_integer_greater_than_zero'; import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { getPaths, foldLeftRight } from '../../../../../../common/test_utils'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('positive_integer_greater_than_zero', () => { test('it should validate a positive number', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/positive_integer_greater_than_zero.ts similarity index 100% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/positive_integer_greater_than_zero.ts diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/postive_integer.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/postive_integer.test.ts similarity index 95% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/postive_integer.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/postive_integer.test.ts index 48ea2025b9b12..ea00ecf5efe0d 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/postive_integer.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/types/postive_integer.test.ts @@ -7,7 +7,7 @@ import { PositiveInteger } from './positive_integer'; import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../../../../common/test_utils'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('positive_integer_greater_than_zero', () => { test('it should validate a positive number', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/references_default_array.test.ts similarity index 95% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/references_default_array.test.ts index 3aaff7e00ad51..43e2dbdac1fe1 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/types/references_default_array.test.ts @@ -7,7 +7,7 @@ import { ReferencesDefaultArray } from './references_default_array'; import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { getPaths, foldLeftRight } from '../../../../../../common/test_utils'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('references_default_array', () => { test('it should validate an empty array', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/references_default_array.ts similarity index 100% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/references_default_array.ts diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/risk_score.test.ts similarity index 96% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/risk_score.test.ts index 41c0faf4d608d..cf849f28a0963 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/types/risk_score.test.ts @@ -7,7 +7,7 @@ import { RiskScore } from './risk_score'; import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../../../../common/test_utils'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('risk_score', () => { test('it should validate a positive number', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/risk_score.ts similarity index 100% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/risk_score.ts diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.test.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/uuid.test.ts similarity index 94% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.test.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/uuid.test.ts index b640b449e6b8a..d3a68a7575487 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.test.ts +++ b/x-pack/plugins/siem/common/detection_engine/schemas/types/uuid.test.ts @@ -7,7 +7,7 @@ import { UUID } from './uuid'; import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../../../../common/test_utils'; +import { foldLeftRight, getPaths } from '../../../test_utils'; describe('uuid', () => { test('it should validate a uuid', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.ts b/x-pack/plugins/siem/common/detection_engine/schemas/types/uuid.ts similarity index 100% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.ts rename to x-pack/plugins/siem/common/detection_engine/schemas/types/uuid.ts diff --git a/x-pack/plugins/siem/common/detection_engine/transform_actions.ts b/x-pack/plugins/siem/common/detection_engine/transform_actions.ts index 4ce3823575833..7c15bc143e0fd 100644 --- a/x-pack/plugins/siem/common/detection_engine/transform_actions.ts +++ b/x-pack/plugins/siem/common/detection_engine/transform_actions.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertAction } from '../../../alerting/common'; +import { AlertAction } from '../../../alerts/common'; import { RuleAlertAction } from './types'; export const transformRuleToAlertAction = ({ diff --git a/x-pack/plugins/siem/common/detection_engine/types.ts b/x-pack/plugins/siem/common/detection_engine/types.ts index 5a91cfd4809c6..431d716a9f205 100644 --- a/x-pack/plugins/siem/common/detection_engine/types.ts +++ b/x-pack/plugins/siem/common/detection_engine/types.ts @@ -5,7 +5,7 @@ */ import * as t from 'io-ts'; -import { AlertAction } from '../../../alerting/common'; +import { AlertAction } from '../../../alerts/common'; export type RuleAlertAction = Omit & { action_type_id: string; diff --git a/x-pack/plugins/siem/cypress/integration/signal_detection_rules_custom.spec.ts b/x-pack/plugins/siem/cypress/integration/signal_detection_rules_custom.spec.ts index 4b5e12124dd40..04762bbf352d2 100644 --- a/x-pack/plugins/siem/cypress/integration/signal_detection_rules_custom.spec.ts +++ b/x-pack/plugins/siem/cypress/integration/signal_detection_rules_custom.spec.ts @@ -41,7 +41,7 @@ import { import { createAndActivateRule, fillAboutRuleAndContinue, - fillDefineCustomRuleAndContinue, + fillDefineCustomRuleWithImportedQueryAndContinue, } from '../tasks/create_new_rule'; import { goToManageSignalDetectionRules, @@ -66,11 +66,11 @@ import { DETECTIONS } from '../urls/navigation'; describe('Signal detection rules, custom', () => { before(() => { - esArchiverLoad('prebuilt_rules_loaded'); + esArchiverLoad('custom_rule_with_timeline'); }); after(() => { - esArchiverUnload('prebuilt_rules_loaded'); + esArchiverUnload('custom_rule_with_timeline'); }); it('Creates and activates a new custom rule', () => { @@ -80,7 +80,7 @@ describe('Signal detection rules, custom', () => { goToManageSignalDetectionRules(); waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); goToCreateNewRule(); - fillDefineCustomRuleAndContinue(newRule); + fillDefineCustomRuleWithImportedQueryAndContinue(newRule); fillAboutRuleAndContinue(newRule); createAndActivateRule(); diff --git a/x-pack/plugins/siem/cypress/objects/rule.ts b/x-pack/plugins/siem/cypress/objects/rule.ts index 7ce8aa69f3339..d750fe212002d 100644 --- a/x-pack/plugins/siem/cypress/objects/rule.ts +++ b/x-pack/plugins/siem/cypress/objects/rule.ts @@ -28,6 +28,7 @@ export interface CustomRule { falsePositivesExamples: string[]; mitre: Mitre[]; note: string; + timelineId: string; } export interface MachineLearningRule { @@ -56,7 +57,7 @@ const mitre2: Mitre = { }; export const newRule: CustomRule = { - customQuery: 'hosts.name: *', + customQuery: 'host.name: *', name: 'New Rule Test', description: 'The new rule description.', severity: 'High', @@ -66,6 +67,7 @@ export const newRule: CustomRule = { falsePositivesExamples: ['False1', 'False2'], mitre: [mitre1, mitre2], note: '# test markdown', + timelineId: '352c6110-9ffb-11ea-b3d8-857d6042d9bd', }; export const machineLearningRule: MachineLearningRule = { diff --git a/x-pack/plugins/siem/cypress/screens/create_new_rule.ts b/x-pack/plugins/siem/cypress/screens/create_new_rule.ts index db9866cdf7f63..bc0740554bc52 100644 --- a/x-pack/plugins/siem/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/siem/cypress/screens/create_new_rule.ts @@ -24,6 +24,9 @@ export const CUSTOM_QUERY_INPUT = '[data-test-subj="queryInput"]'; export const DEFINE_CONTINUE_BUTTON = '[data-test-subj="define-continue"]'; +export const IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK = + '[data-test-subj="importQueryFromSavedTimeline"]'; + export const INVESTIGATION_NOTES_TEXTAREA = '[data-test-subj="detectionEngineStepAboutRuleNote"] textarea'; diff --git a/x-pack/plugins/siem/cypress/screens/timeline.ts b/x-pack/plugins/siem/cypress/screens/timeline.ts index 58d2568084f7c..ed1dc97454fb3 100644 --- a/x-pack/plugins/siem/cypress/screens/timeline.ts +++ b/x-pack/plugins/siem/cypress/screens/timeline.ts @@ -21,6 +21,10 @@ export const SEARCH_OR_FILTER_CONTAINER = export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count"]'; +export const TIMELINE = (id: string) => { + return `[data-test-subj="title-${id}"]`; +}; + export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]'; export const TIMELINE_DATA_PROVIDERS_EMPTY = diff --git a/x-pack/plugins/siem/cypress/tasks/create_new_rule.ts b/x-pack/plugins/siem/cypress/tasks/create_new_rule.ts index 6324b42f3783a..eca5885e7b3d9 100644 --- a/x-pack/plugins/siem/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/siem/cypress/tasks/create_new_rule.ts @@ -14,6 +14,7 @@ import { CUSTOM_QUERY_INPUT, DEFINE_CONTINUE_BUTTON, FALSE_POSITIVES_INPUT, + IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK, INVESTIGATION_NOTES_TEXTAREA, MACHINE_LEARNING_DROPDOWN, MACHINE_LEARNING_LIST, @@ -30,6 +31,7 @@ import { SEVERITY_DROPDOWN, TAGS_INPUT, } from '../screens/create_new_rule'; +import { TIMELINE } from '../screens/timeline'; export const createAndActivateRule = () => { cy.get(SCHEDULE_CONTINUE_BUTTON).click({ force: true }); @@ -86,6 +88,15 @@ export const fillDefineCustomRuleAndContinue = (rule: CustomRule) => { cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); }; +export const fillDefineCustomRuleWithImportedQueryAndContinue = (rule: CustomRule) => { + cy.get(IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK).click(); + cy.get(TIMELINE(rule.timelineId)).click(); + cy.get(CUSTOM_QUERY_INPUT).should('have.attr', 'value', rule.customQuery); + cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); + + cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); +}; + export const fillDefineMachineLearningRuleAndContinue = (rule: MachineLearningRule) => { cy.get(MACHINE_LEARNING_DROPDOWN).click({ force: true }); cy.contains(MACHINE_LEARNING_LIST, rule.machineLearningJob).click(); diff --git a/x-pack/plugins/siem/kibana.json b/x-pack/plugins/siem/kibana.json index 1106781fd45e4..df40ad4650f45 100644 --- a/x-pack/plugins/siem/kibana.json +++ b/x-pack/plugins/siem/kibana.json @@ -5,7 +5,7 @@ "configPath": ["xpack", "siem"], "requiredPlugins": [ "actions", - "alerting", + "alerts", "data", "dataEnhanced", "embeddable", @@ -24,7 +24,8 @@ "newsfeed", "security", "spaces", - "usageCollection" + "usageCollection", + "lists" ], "server": true, "ui": true diff --git a/x-pack/plugins/siem/public/alerts/components/rules/description_step/actions_description.tsx b/x-pack/plugins/siem/public/alerts/components/rules/description_step/actions_description.tsx index be96ab10bd2b5..43416abe6e288 100644 --- a/x-pack/plugins/siem/public/alerts/components/rules/description_step/actions_description.tsx +++ b/x-pack/plugins/siem/public/alerts/components/rules/description_step/actions_description.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { startCase } from 'lodash/fp'; -import { AlertAction } from '../../../../../../alerting/common'; +import { AlertAction } from '../../../../../../alerts/common'; const ActionsDescription = ({ actions }: { actions: AlertAction[] }) => { if (!actions.length) return null; diff --git a/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_field/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_field/index.tsx index d8064eb4ad391..5823612faac13 100644 --- a/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_field/index.tsx +++ b/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_field/index.tsx @@ -18,7 +18,7 @@ import { ActionType, loadActionTypes, } from '../../../../../../triggers_actions_ui/public'; -import { AlertAction } from '../../../../../../alerting/common'; +import { AlertAction } from '../../../../../../alerts/common'; import { useKibana } from '../../../../common/lib/kibana'; import { FORM_ERRORS_TITLE } from './translations'; diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/index.tsx index 119f851ecdfe4..fc875908bd4ef 100644 --- a/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/index.tsx @@ -203,7 +203,10 @@ const StepDefineRuleComponent: FC = ({ config={{ ...schema.queryBar, labelAppend: ( - + {i18n.IMPORT_TIMELINE_QUERY} ), diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/types.ts b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/types.ts index 897568cdbf16e..ab9b88fb81fa7 100644 --- a/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/types.ts @@ -10,7 +10,7 @@ import { RuleTypeSchema } from '../../../../../common/detection_engine/types'; /** * Params is an "record", since it is a type of AlertActionParams which is action templates. - * @see x-pack/plugins/alerting/common/alert.ts + * @see x-pack/plugins/alerts/common/alert.ts */ export const action = t.exact( t.type({ diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/types.ts b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/types.ts index 92c9780a11722..5f81409010a28 100644 --- a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/types.ts @@ -5,7 +5,7 @@ */ import { RuleAlertAction, RuleType } from '../../../../../common/detection_engine/types'; -import { AlertAction } from '../../../../../../alerting/common'; +import { AlertAction } from '../../../../../../alerts/common'; import { Filter } from '../../../../../../../../src/plugins/data/common'; import { FormData, FormHook } from '../../../../shared_imports'; import { FieldValueQueryBar } from '../../../components/rules/query_bar'; diff --git a/x-pack/plugins/siem/public/app/home/home_navigations.tsx b/x-pack/plugins/siem/public/app/home/home_navigations.tsx index 2eed64a2b26e5..bb9e99326182f 100644 --- a/x-pack/plugins/siem/public/app/home/home_navigations.tsx +++ b/x-pack/plugins/siem/public/app/home/home_navigations.tsx @@ -14,6 +14,7 @@ import { } from '../../common/components/link_to'; import * as i18n from './translations'; import { SiemPageName, SiemNavTab } from '../types'; +import { getManagementUrl } from '../../management'; export const navTabs: SiemNavTab = { [SiemPageName.overview]: { @@ -58,4 +59,11 @@ export const navTabs: SiemNavTab = { disabled: false, urlKey: 'case', }, + [SiemPageName.management]: { + id: SiemPageName.management, + name: i18n.MANAGEMENT, + href: getManagementUrl({ name: 'default' }), + disabled: false, + urlKey: SiemPageName.management, + }, }; diff --git a/x-pack/plugins/siem/public/app/home/translations.ts b/x-pack/plugins/siem/public/app/home/translations.ts index f2bcaa07b1a25..0cce45b4cef27 100644 --- a/x-pack/plugins/siem/public/app/home/translations.ts +++ b/x-pack/plugins/siem/public/app/home/translations.ts @@ -29,3 +29,7 @@ export const TIMELINES = i18n.translate('xpack.siem.navigation.timelines', { export const CASE = i18n.translate('xpack.siem.navigation.case', { defaultMessage: 'Cases', }); + +export const MANAGEMENT = i18n.translate('xpack.siem.navigation.management', { + defaultMessage: 'Management', +}); diff --git a/x-pack/plugins/siem/public/app/types.ts b/x-pack/plugins/siem/public/app/types.ts index 1fcbc5ba25f8f..444e0066c3c7b 100644 --- a/x-pack/plugins/siem/public/app/types.ts +++ b/x-pack/plugins/siem/public/app/types.ts @@ -5,7 +5,6 @@ */ import { Reducer, AnyAction, Middleware, Dispatch } from 'redux'; - import { NavTab } from '../common/components/navigation/types'; import { HostsState } from '../hosts/store'; import { NetworkState } from '../network/store'; @@ -15,7 +14,7 @@ import { Immutable } from '../../common/endpoint/types'; import { AlertListState } from '../../common/endpoint_alerts/types'; import { AppAction } from '../common/store/actions'; import { HostState } from '../endpoint_hosts/types'; -import { PolicyDetailsState, PolicyListState } from '../endpoint_policy/types'; +import { ManagementState } from '../management/store/types'; export enum SiemPageName { overview = 'overview', @@ -24,6 +23,7 @@ export enum SiemPageName { detections = 'detections', timelines = 'timelines', case = 'case', + management = 'management', } export type SiemNavTabKey = @@ -32,14 +32,15 @@ export type SiemNavTabKey = | SiemPageName.network | SiemPageName.detections | SiemPageName.timelines - | SiemPageName.case; + | SiemPageName.case + | SiemPageName.management; export type SiemNavTab = Record; export interface SecuritySubPluginStore { initialState: Record; reducer: Record>; - middleware?: Middleware<{}, State, Dispatch>>; + middleware?: Array>>>; } export interface SecuritySubPlugin { @@ -52,8 +53,7 @@ type SecuritySubPluginKeyStore = | 'timeline' | 'hostList' | 'alertList' - | 'policyDetails' - | 'policyList'; + | 'management'; export interface SecuritySubPluginWithStore extends SecuritySubPlugin { store: SecuritySubPluginStore; @@ -67,8 +67,7 @@ export interface SecuritySubPlugins extends SecuritySubPlugin { timeline: TimelineState; alertList: Immutable; hostList: Immutable; - policyDetails: Immutable; - policyList: Immutable; + management: ManagementState; }; reducer: { hosts: Reducer; @@ -76,8 +75,7 @@ export interface SecuritySubPlugins extends SecuritySubPlugin { timeline: Reducer; alertList: ImmutableReducer; hostList: ImmutableReducer; - policyDetails: ImmutableReducer; - policyList: ImmutableReducer; + management: ImmutableReducer; }; middlewares: Array>>>; }; diff --git a/x-pack/plugins/siem/public/cases/components/callout/index.test.tsx b/x-pack/plugins/siem/public/cases/components/callout/index.test.tsx index c830a6f5e10d5..ee3faeb2ceeb5 100644 --- a/x-pack/plugins/siem/public/cases/components/callout/index.test.tsx +++ b/x-pack/plugins/siem/public/cases/components/callout/index.test.tsx @@ -60,6 +60,43 @@ describe('CaseCallOut ', () => { ).toBeTruthy(); }); + it('it applies the correct color to button', () => { + const props = { + ...defaultProps, + messages: [ + { + ...defaultProps, + description:

    {'one'}

    , + errorType: 'danger' as 'primary' | 'success' | 'warning' | 'danger', + }, + { + ...defaultProps, + description:

    {'two'}

    , + errorType: 'success' as 'primary' | 'success' | 'warning' | 'danger', + }, + { + ...defaultProps, + description:

    {'three'}

    , + errorType: 'primary' as 'primary' | 'success' | 'warning' | 'danger', + }, + ], + }; + + const wrapper = mount(); + + expect(wrapper.find(`[data-test-subj="callout-dismiss-danger"]`).first().prop('color')).toBe( + 'danger' + ); + + expect(wrapper.find(`[data-test-subj="callout-dismiss-success"]`).first().prop('color')).toBe( + 'secondary' + ); + + expect(wrapper.find(`[data-test-subj="callout-dismiss-primary"]`).first().prop('color')).toBe( + 'primary' + ); + }); + it('Dismisses callout', () => { const props = { ...defaultProps, diff --git a/x-pack/plugins/siem/public/cases/components/callout/index.tsx b/x-pack/plugins/siem/public/cases/components/callout/index.tsx index 470b03637dc00..171c0508b9d92 100644 --- a/x-pack/plugins/siem/public/cases/components/callout/index.tsx +++ b/x-pack/plugins/siem/public/cases/components/callout/index.tsx @@ -66,7 +66,7 @@ const CaseCallOutComponent = ({ title, message, messages }: CaseCallOutProps) => )} {i18n.DISMISS_CALLOUT} diff --git a/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.test.tsx b/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.test.tsx index e3e627e3a136e..4391db1a0a0a1 100644 --- a/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.test.tsx +++ b/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.test.tsx @@ -123,7 +123,27 @@ describe('usePushToService', () => { }); }); - it('Displays message when user does not have a connector configured', async () => { + it('Displays message when user does not have any connector configured', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + usePushToService({ + ...defaultArgs, + connectors: [], + caseConnectorId: 'none', + }), + { + wrapper: ({ children }) => {children}, + } + ); + await waitForNextUpdate(); + const errorsMsg = result.current.pushCallouts?.props.messages; + expect(errorsMsg).toHaveLength(1); + expect(errorsMsg[0].title).toEqual(i18n.PUSH_DISABLE_BY_NO_CONFIG_TITLE); + }); + }); + + it('Displays message when user does have a connector but is configured to none', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( () => @@ -162,6 +182,27 @@ describe('usePushToService', () => { }); }); + it('Displays message when connector is deleted with empty connectors', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + usePushToService({ + ...defaultArgs, + connectors: [], + caseConnectorId: 'not-exist', + isValidConnector: false, + }), + { + wrapper: ({ children }) => {children}, + } + ); + await waitForNextUpdate(); + const errorsMsg = result.current.pushCallouts?.props.messages; + expect(errorsMsg).toHaveLength(1); + expect(errorsMsg[0].title).toEqual(i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE); + }); + }); + it('Displays message when case is closed', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( diff --git a/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.tsx b/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.tsx index 5d238b623eb4a..63b808eed3c92 100644 --- a/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.tsx +++ b/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.tsx @@ -75,7 +75,7 @@ export const usePushToService = ({ if (actionLicense != null && !actionLicense.enabledInLicense) { errors = [...errors, getLicenseError()]; } - if (connectors.length === 0 && !loadingLicense) { + if (connectors.length === 0 && caseConnectorId === 'none' && !loadingLicense) { errors = [ ...errors, { diff --git a/x-pack/plugins/siem/public/common/components/endpoint/__snapshots__/page_view.test.tsx.snap b/x-pack/plugins/siem/public/common/components/endpoint/__snapshots__/page_view.test.tsx.snap index 35a42acf7e1fb..5d077dba447fa 100644 --- a/x-pack/plugins/siem/public/common/components/endpoint/__snapshots__/page_view.test.tsx.snap +++ b/x-pack/plugins/siem/public/common/components/endpoint/__snapshots__/page_view.test.tsx.snap @@ -21,6 +21,10 @@ exports[`PageView component should display body header custom element 1`] = ` background: none; } +.c0 .endpoint-navTabs { + margin-left: 24px; +} + @@ -112,6 +116,10 @@ exports[`PageView component should display body header wrapped in EuiTitle 1`] = background: none; } +.c0 .endpoint-navTabs { + margin-left: 24px; +} + @@ -383,6 +399,10 @@ exports[`PageView component should display only header left 1`] = ` background: none; } +.c0 .endpoint-navTabs { + margin-left: 24px; +} + diff --git a/x-pack/plugins/siem/public/common/components/endpoint/page_view.tsx b/x-pack/plugins/siem/public/common/components/endpoint/page_view.tsx index ecc480fc97293..759274e3a4ffa 100644 --- a/x-pack/plugins/siem/public/common/components/endpoint/page_view.tsx +++ b/x-pack/plugins/siem/public/common/components/endpoint/page_view.tsx @@ -14,10 +14,13 @@ import { EuiPageHeader, EuiPageHeaderSection, EuiPageProps, + EuiTab, + EuiTabs, EuiTitle, } from '@elastic/eui'; -import React, { memo, ReactNode } from 'react'; +import React, { memo, MouseEventHandler, ReactNode, useMemo } from 'react'; import styled from 'styled-components'; +import { EuiTabProps } from '@elastic/eui/src/components/tabs/tab'; const StyledEuiPage = styled(EuiPage)` &.endpoint--isListView { @@ -39,6 +42,9 @@ const StyledEuiPage = styled(EuiPage)` background: none; } } + .endpoint-navTabs { + margin-left: ${(props) => props.theme.eui.euiSizeL}; + } `; const isStringOrNumber = /(string|number)/; @@ -74,69 +80,94 @@ export const PageViewBodyHeaderTitle = memo<{ children: ReactNode }>( ); PageViewBodyHeaderTitle.displayName = 'PageViewBodyHeaderTitle'; +export type PageViewProps = EuiPageProps & { + /** + * The type of view + */ + viewType: 'list' | 'details'; + /** + * content to be placed on the left side of the header. If a `string` is used, then it will + * be wrapped with `

    `, else it will just be used as is. + */ + headerLeft?: ReactNode; + /** Content for the right side of the header */ + headerRight?: ReactNode; + /** + * body (sub-)header section. If a `string` is used, then it will be wrapped with + * `

    ` + */ + bodyHeader?: ReactNode; + /** + * The list of tab navigation items + */ + tabs?: Array< + EuiTabProps & { + name: ReactNode; + id: string; + href?: string; + onClick?: MouseEventHandler; + } + >; + children?: ReactNode; +}; + /** * Page View layout for use in Endpoint */ -export const PageView = memo< - EuiPageProps & { - /** - * The type of view - */ - viewType: 'list' | 'details'; - /** - * content to be placed on the left side of the header. If a `string` is used, then it will - * be wrapped with `

    `, else it will just be used as is. - */ - headerLeft?: ReactNode; - /** Content for the right side of the header */ - headerRight?: ReactNode; - /** - * body (sub-)header section. If a `string` is used, then it will be wrapped with - * `

    ` - */ - bodyHeader?: ReactNode; - children?: ReactNode; - } ->(({ viewType, children, headerLeft, headerRight, bodyHeader, ...otherProps }) => { - return ( - - - {(headerLeft || headerRight) && ( - - - {isStringOrNumber.test(typeof headerLeft) ? ( - {headerLeft} - ) : ( - headerLeft - )} - - {headerRight && ( - - {headerRight} - - )} - - )} - - {bodyHeader && ( - - - {isStringOrNumber.test(typeof bodyHeader) ? ( - {bodyHeader} +export const PageView = memo( + ({ viewType, children, headerLeft, headerRight, bodyHeader, tabs, ...otherProps }) => { + const tabComponents = useMemo(() => { + if (!tabs) { + return []; + } + return tabs.map(({ name, id, ...otherEuiTabProps }) => ( + + {name} + + )); + }, [tabs]); + + return ( + + + {(headerLeft || headerRight) && ( + + + {isStringOrNumber.test(typeof headerLeft) ? ( + {headerLeft} ) : ( - bodyHeader + headerLeft )} - - + + {headerRight && ( + + {headerRight} + + )} + )} - {children} - - - - ); -}); + {tabs && {tabComponents}} + + {bodyHeader && ( + + + {isStringOrNumber.test(typeof bodyHeader) ? ( + {bodyHeader} + ) : ( + bodyHeader + )} + + + )} + {children} + + + + ); + } +); PageView.displayName = 'PageView'; diff --git a/x-pack/plugins/siem/public/common/components/header_global/index.test.tsx b/x-pack/plugins/siem/public/common/components/header_global/index.test.tsx index 0f6c5c2e139a7..809f0eeb811f4 100644 --- a/x-pack/plugins/siem/public/common/components/header_global/index.test.tsx +++ b/x-pack/plugins/siem/public/common/components/header_global/index.test.tsx @@ -18,6 +18,7 @@ jest.mock('react-router-dom', () => ({ state: '', }), withRouter: () => jest.fn(), + generatePath: jest.fn(), })); // Test will fail because we will to need to mock some core services to make the test work diff --git a/x-pack/plugins/siem/public/common/components/link_to/link_to.tsx b/x-pack/plugins/siem/public/common/components/link_to/link_to.tsx index 77636af8bc4a4..8151291679e32 100644 --- a/x-pack/plugins/siem/public/common/components/link_to/link_to.tsx +++ b/x-pack/plugins/siem/public/common/components/link_to/link_to.tsx @@ -27,6 +27,7 @@ import { } from './redirect_to_case'; import { DetectionEngineTab } from '../../../alerts/pages/detection_engine/types'; import { TimelineType } from '../../../../common/types/timeline'; +import { RedirectToManagementPage } from './redirect_to_management'; interface LinkToPageProps { match: RouteMatch<{}>; @@ -120,6 +121,10 @@ export const LinkToPage = React.memo(({ match }) => ( component={RedirectToTimelinesPage} path={`${match.url}/:pageName(${SiemPageName.timelines})/:tabName(${TimelineType.default}|${TimelineType.template})`} /> + )); diff --git a/x-pack/plugins/siem/public/common/components/link_to/redirect_to_management.tsx b/x-pack/plugins/siem/public/common/components/link_to/redirect_to_management.tsx new file mode 100644 index 0000000000000..595c203993bb7 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/link_to/redirect_to_management.tsx @@ -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 React, { memo } from 'react'; +import { RedirectWrapper } from './redirect_wrapper'; +import { SiemPageName } from '../../../app/types'; + +export const RedirectToManagementPage = memo(() => { + return ; +}); + +RedirectToManagementPage.displayName = 'RedirectToManagementPage'; diff --git a/x-pack/plugins/siem/public/common/components/navigation/index.test.tsx b/x-pack/plugins/siem/public/common/components/navigation/index.test.tsx index ff3f9ba0694a9..fd96885e5bc10 100644 --- a/x-pack/plugins/siem/public/common/components/navigation/index.test.tsx +++ b/x-pack/plugins/siem/public/common/components/navigation/index.test.tsx @@ -72,6 +72,13 @@ describe('SIEM Navigation', () => { name: 'Cases', urlKey: 'case', }, + management: { + disabled: false, + href: '#/management', + id: 'management', + name: 'Management', + urlKey: 'management', + }, detections: { disabled: false, href: '#/link-to/detections', @@ -111,6 +118,7 @@ describe('SIEM Navigation', () => { pageName: 'hosts', pathName: '/hosts', search: '', + state: undefined, tabName: 'authentications', query: { query: '', language: 'kuery' }, filters: [], @@ -179,6 +187,13 @@ describe('SIEM Navigation', () => { name: 'Hosts', urlKey: 'host', }, + management: { + disabled: false, + href: '#/management', + id: 'management', + name: 'Management', + urlKey: 'management', + }, network: { disabled: false, href: '#/link-to/network', diff --git a/x-pack/plugins/siem/public/common/components/url_state/constants.ts b/x-pack/plugins/siem/public/common/components/url_state/constants.ts index b6ef3c8ccd4e9..1faff2594ce80 100644 --- a/x-pack/plugins/siem/public/common/components/url_state/constants.ts +++ b/x-pack/plugins/siem/public/common/components/url_state/constants.ts @@ -12,6 +12,7 @@ export enum CONSTANTS { filters = 'filters', hostsDetails = 'hosts.details', hostsPage = 'hosts.page', + management = 'management', networkDetails = 'network.details', networkPage = 'network.page', overviewPage = 'overview.page', @@ -22,4 +23,11 @@ export enum CONSTANTS { unknown = 'unknown', } -export type UrlStateType = 'case' | 'detections' | 'host' | 'network' | 'overview' | 'timeline'; +export type UrlStateType = + | 'case' + | 'detections' + | 'host' + | 'network' + | 'overview' + | 'timeline' + | 'management'; diff --git a/x-pack/plugins/siem/public/common/components/url_state/types.ts b/x-pack/plugins/siem/public/common/components/url_state/types.ts index 56578d84e12e4..8881a82e5cd1c 100644 --- a/x-pack/plugins/siem/public/common/components/url_state/types.ts +++ b/x-pack/plugins/siem/public/common/components/url_state/types.ts @@ -8,10 +8,10 @@ import ApolloClient from 'apollo-client'; import * as H from 'history'; import { ActionCreator } from 'typescript-fsa'; import { - IIndexPattern, - Query, Filter, FilterManager, + IIndexPattern, + Query, SavedQueryService, } from 'src/plugins/data/public'; @@ -46,6 +46,7 @@ export const URL_STATE_KEYS: Record = { CONSTANTS.timerange, CONSTANTS.timeline, ], + management: [], network: [ CONSTANTS.appQuery, CONSTANTS.filters, diff --git a/x-pack/plugins/siem/public/common/mock/endpoint/app_context_render.tsx b/x-pack/plugins/siem/public/common/mock/endpoint/app_context_render.tsx index 9a7048efd4d0e..e62f36c2ec782 100644 --- a/x-pack/plugins/siem/public/common/mock/endpoint/app_context_render.tsx +++ b/x-pack/plugins/siem/public/common/mock/endpoint/app_context_render.tsx @@ -15,11 +15,10 @@ import { depsStartMock } from './dependencies_start_mock'; import { MiddlewareActionSpyHelper, createSpyMiddleware } from '../../store/test_utils'; import { apolloClientObservable } from '../test_providers'; import { createStore, State, substateMiddlewareFactory } from '../../store'; -import { hostMiddlewareFactory } from '../../../endpoint_hosts/store/middleware'; -import { policyListMiddlewareFactory } from '../../../endpoint_policy/store/policy_list/middleware'; -import { policyDetailsMiddlewareFactory } from '../../../endpoint_policy/store/policy_details/middleware'; +import { hostMiddlewareFactory } from '../../../endpoint_hosts/store'; import { alertMiddlewareFactory } from '../../../endpoint_alerts/store/middleware'; import { AppRootProvider } from './app_root_provider'; +import { managementMiddlewareFactory } from '../../../management/store'; import { SUB_PLUGINS_REDUCER, mockGlobalState } from '..'; type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult; @@ -63,18 +62,11 @@ export const createAppRootMockRenderer = (): AppContextTestRender => { (globalState) => globalState.hostList, hostMiddlewareFactory(coreStart, depsStart) ), - substateMiddlewareFactory( - (globalState) => globalState.policyList, - policyListMiddlewareFactory(coreStart, depsStart) - ), - substateMiddlewareFactory( - (globalState) => globalState.policyDetails, - policyDetailsMiddlewareFactory(coreStart, depsStart) - ), substateMiddlewareFactory( (globalState) => globalState.alertList, alertMiddlewareFactory(coreStart, depsStart) ), + ...managementMiddlewareFactory(coreStart, depsStart), middlewareSpy.actionSpyMiddleware, ]); diff --git a/x-pack/plugins/siem/public/common/mock/global_state.ts b/x-pack/plugins/siem/public/common/mock/global_state.ts index da49ebe6552f3..c96f67a39dbfe 100644 --- a/x-pack/plugins/siem/public/common/mock/global_state.ts +++ b/x-pack/plugins/siem/public/common/mock/global_state.ts @@ -25,15 +25,13 @@ import { } from '../../../common/constants'; import { networkModel } from '../../network/store'; import { TimelineType, TimelineStatus } from '../../../common/types/timeline'; -import { initialPolicyListState } from '../../endpoint_policy/store/policy_list/reducer'; import { initialAlertListState } from '../../endpoint_alerts/store/reducer'; -import { initialPolicyDetailsState } from '../../endpoint_policy/store/policy_details/reducer'; import { initialHostListState } from '../../endpoint_hosts/store/reducer'; +import { getManagementInitialState } from '../../management/store'; -const policyList = initialPolicyListState(); const alertList = initialAlertListState(); -const policyDetails = initialPolicyDetailsState(); const hostList = initialHostListState(); +const management = getManagementInitialState(); export const mockGlobalState: State = { app: { @@ -237,6 +235,5 @@ export const mockGlobalState: State = { }, alertList, hostList, - policyList, - policyDetails, + management, }; diff --git a/x-pack/plugins/siem/public/common/mock/utils.ts b/x-pack/plugins/siem/public/common/mock/utils.ts index 68c52e493898f..532637acab767 100644 --- a/x-pack/plugins/siem/public/common/mock/utils.ts +++ b/x-pack/plugins/siem/public/common/mock/utils.ts @@ -9,8 +9,7 @@ import { networkReducer } from '../../network/store'; import { timelineReducer } from '../../timelines/store/timeline/reducer'; import { hostListReducer } from '../../endpoint_hosts/store'; import { alertListReducer } from '../../endpoint_alerts/store'; -import { policyListReducer } from '../../endpoint_policy/store/policy_list'; -import { policyDetailsReducer } from '../../endpoint_policy/store/policy_details'; +import { managementReducer } from '../../management/store'; interface Global extends NodeJS.Global { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -25,6 +24,5 @@ export const SUB_PLUGINS_REDUCER = { timeline: timelineReducer, hostList: hostListReducer, alertList: alertListReducer, - policyList: policyListReducer, - policyDetails: policyDetailsReducer, + management: managementReducer, }; diff --git a/x-pack/plugins/siem/public/common/store/actions.ts b/x-pack/plugins/siem/public/common/store/actions.ts index a51b075dc7514..58e4e2f363e92 100644 --- a/x-pack/plugins/siem/public/common/store/actions.ts +++ b/x-pack/plugins/siem/public/common/store/actions.ts @@ -6,8 +6,8 @@ import { HostAction } from '../../endpoint_hosts/store/action'; import { AlertAction } from '../../endpoint_alerts/store/action'; -import { PolicyListAction } from '../../endpoint_policy/store/policy_list'; -import { PolicyDetailsAction } from '../../endpoint_policy/store/policy_details'; +import { PolicyListAction } from '../../management/pages/policy/store/policy_list'; +import { PolicyDetailsAction } from '../../management/pages/policy/store/policy_details'; export { appActions } from './app'; export { dragAndDropActions } from './drag_and_drop'; diff --git a/x-pack/plugins/siem/public/common/store/reducer.ts b/x-pack/plugins/siem/public/common/store/reducer.ts index 570e851a3aa5e..e06543b8d7181 100644 --- a/x-pack/plugins/siem/public/common/store/reducer.ts +++ b/x-pack/plugins/siem/public/common/store/reducer.ts @@ -18,14 +18,8 @@ import { EndpointAlertsPluginReducer, } from '../../endpoint_alerts/store'; import { EndpointHostsPluginState, EndpointHostsPluginReducer } from '../../endpoint_hosts/store'; -import { - EndpointPolicyDetailsStatePluginState, - EndpointPolicyDetailsStatePluginReducer, -} from '../../endpoint_policy/store/policy_details'; -import { - EndpointPolicyListStatePluginState, - EndpointPolicyListStatePluginReducer, -} from '../../endpoint_policy/store/policy_list'; + +import { ManagementPluginReducer, ManagementPluginState } from '../../management/store/types'; export interface State extends HostsPluginState, @@ -33,8 +27,7 @@ export interface State TimelinePluginState, EndpointAlertsPluginState, EndpointHostsPluginState, - EndpointPolicyDetailsStatePluginState, - EndpointPolicyListStatePluginState { + ManagementPluginState { app: AppState; dragAndDrop: DragAndDropState; inputs: InputsState; @@ -51,15 +44,14 @@ type SubPluginsInitState = HostsPluginState & TimelinePluginState & EndpointAlertsPluginState & EndpointHostsPluginState & - EndpointPolicyDetailsStatePluginState & - EndpointPolicyListStatePluginState; + ManagementPluginState; + export type SubPluginsInitReducer = HostsPluginReducer & NetworkPluginReducer & TimelinePluginReducer & EndpointAlertsPluginReducer & EndpointHostsPluginReducer & - EndpointPolicyDetailsStatePluginReducer & - EndpointPolicyListStatePluginReducer; + ManagementPluginReducer; export const createInitialState = (pluginsInitState: SubPluginsInitState): State => ({ ...initialState, diff --git a/x-pack/plugins/siem/public/common/store/types.ts b/x-pack/plugins/siem/public/common/store/types.ts index 0a1010ea87fca..a4bfdeb30b438 100644 --- a/x-pack/plugins/siem/public/common/store/types.ts +++ b/x-pack/plugins/siem/public/common/store/types.ts @@ -61,6 +61,17 @@ export type ImmutableMiddlewareFactory = ( depsStart: Pick ) => ImmutableMiddleware; +/** + * Takes application-standard middleware dependencies + * and returns an array of redux middleware. + * Middleware will be of the `ImmutableMiddleware` variety. Not able to directly + * change actions or state. + */ +export type ImmutableMultipleMiddlewareFactory = ( + coreStart: CoreStart, + depsStart: Pick +) => Array>; + /** * Simple type for a redux selector. */ diff --git a/x-pack/plugins/siem/public/endpoint_alerts/index.ts b/x-pack/plugins/siem/public/endpoint_alerts/index.ts index 8b7e13c118fd0..6380edbde6958 100644 --- a/x-pack/plugins/siem/public/endpoint_alerts/index.ts +++ b/x-pack/plugins/siem/public/endpoint_alerts/index.ts @@ -22,10 +22,12 @@ export class EndpointAlerts { plugins: StartPlugins ): SecuritySubPluginWithStore<'alertList', Immutable> { const { data, ingestManager } = plugins; - const middleware = substateMiddlewareFactory( - (globalState) => globalState.alertList, - alertMiddlewareFactory(core, { data, ingestManager }) - ); + const middleware = [ + substateMiddlewareFactory( + (globalState) => globalState.alertList, + alertMiddlewareFactory(core, { data, ingestManager }) + ), + ]; return { routes: getEndpointAlertsRoutes(), diff --git a/x-pack/plugins/siem/public/endpoint_hosts/index.ts b/x-pack/plugins/siem/public/endpoint_hosts/index.ts index c86078ef4b475..1c2649ec5cf91 100644 --- a/x-pack/plugins/siem/public/endpoint_hosts/index.ts +++ b/x-pack/plugins/siem/public/endpoint_hosts/index.ts @@ -22,10 +22,12 @@ export class EndpointHosts { plugins: StartPlugins ): SecuritySubPluginWithStore<'hostList', Immutable> { const { data, ingestManager } = plugins; - const middleware = substateMiddlewareFactory( - (globalState) => globalState.hostList, - hostMiddlewareFactory(core, { data, ingestManager }) - ); + const middleware = [ + substateMiddlewareFactory( + (globalState) => globalState.hostList, + hostMiddlewareFactory(core, { data, ingestManager }) + ), + ]; return { routes: getEndpointHostsRoutes(), store: { diff --git a/x-pack/plugins/siem/public/endpoint_policy/details.ts b/x-pack/plugins/siem/public/endpoint_policy/details.ts deleted file mode 100644 index 1375d851067b4..0000000000000 --- a/x-pack/plugins/siem/public/endpoint_policy/details.ts +++ /dev/null @@ -1,41 +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 { SecuritySubPluginWithStore } from '../app/types'; -import { getPolicyDetailsRoutes } from './routes'; -import { PolicyDetailsState } from './types'; -import { Immutable } from '../../common/endpoint/types'; -import { initialPolicyDetailsState, policyDetailsReducer } from './store/policy_details/reducer'; -import { policyDetailsMiddlewareFactory } from './store/policy_details/middleware'; -import { CoreStart } from '../../../../../src/core/public'; -import { StartPlugins } from '../types'; -import { substateMiddlewareFactory } from '../common/store'; - -export class EndpointPolicyDetails { - public setup() {} - - public start( - core: CoreStart, - plugins: StartPlugins - ): SecuritySubPluginWithStore<'policyDetails', Immutable> { - const { data, ingestManager } = plugins; - const middleware = substateMiddlewareFactory( - (globalState) => globalState.policyDetails, - policyDetailsMiddlewareFactory(core, { data, ingestManager }) - ); - - return { - routes: getPolicyDetailsRoutes(), - store: { - initialState: { - policyDetails: initialPolicyDetailsState(), - }, - reducer: { policyDetails: policyDetailsReducer }, - middleware, - }, - }; - } -} diff --git a/x-pack/plugins/siem/public/endpoint_policy/list.ts b/x-pack/plugins/siem/public/endpoint_policy/list.ts deleted file mode 100644 index 5dad5fac895e0..0000000000000 --- a/x-pack/plugins/siem/public/endpoint_policy/list.ts +++ /dev/null @@ -1,41 +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 { SecuritySubPluginWithStore } from '../app/types'; -import { getPolicyListRoutes } from './routes'; -import { PolicyListState } from './types'; -import { Immutable } from '../../common/endpoint/types'; -import { initialPolicyListState, policyListReducer } from './store/policy_list/reducer'; -import { policyListMiddlewareFactory } from './store/policy_list/middleware'; -import { CoreStart } from '../../../../../src/core/public'; -import { StartPlugins } from '../types'; -import { substateMiddlewareFactory } from '../common/store'; - -export class EndpointPolicyList { - public setup() {} - - public start( - core: CoreStart, - plugins: StartPlugins - ): SecuritySubPluginWithStore<'policyList', Immutable> { - const { data, ingestManager } = plugins; - const middleware = substateMiddlewareFactory( - (globalState) => globalState.policyList, - policyListMiddlewareFactory(core, { data, ingestManager }) - ); - - return { - routes: getPolicyListRoutes(), - store: { - initialState: { - policyList: initialPolicyListState(), - }, - reducer: { policyList: policyListReducer }, - middleware, - }, - }; - } -} diff --git a/x-pack/plugins/siem/public/endpoint_policy/routes.tsx b/x-pack/plugins/siem/public/endpoint_policy/routes.tsx deleted file mode 100644 index be820f3f2c5dc..0000000000000 --- a/x-pack/plugins/siem/public/endpoint_policy/routes.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { Route } from 'react-router-dom'; - -import { PolicyList, PolicyDetails } from './view'; - -export const getPolicyListRoutes = () => [ - , -]; - -export const getPolicyDetailsRoutes = () => [ - , -]; diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_hooks.ts b/x-pack/plugins/siem/public/endpoint_policy/view/policy_hooks.ts deleted file mode 100644 index 9fadba85c5245..0000000000000 --- a/x-pack/plugins/siem/public/endpoint_policy/view/policy_hooks.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { useSelector } from 'react-redux'; -import { PolicyListState, PolicyDetailsState } from '../types'; -import { State } from '../../common/store'; - -export function usePolicyListSelector(selector: (state: PolicyListState) => TSelected) { - return useSelector((state: State) => selector(state.policyList as PolicyListState)); -} - -export function usePolicyDetailsSelector( - selector: (state: PolicyDetailsState) => TSelected -) { - return useSelector((state: State) => selector(state.policyDetails as PolicyDetailsState)); -} diff --git a/x-pack/plugins/siem/public/lists_plugin_deps.ts b/x-pack/plugins/siem/public/lists_plugin_deps.ts index b99e5015c716a..d2ee5ae56b7d9 100644 --- a/x-pack/plugins/siem/public/lists_plugin_deps.ts +++ b/x-pack/plugins/siem/public/lists_plugin_deps.ts @@ -8,8 +8,6 @@ export { useExceptionList, usePersistExceptionItem, usePersistExceptionList, - mockExceptionItem, - mockExceptionList, mockNewExceptionItem, mockNewExceptionList, } from '../../lists/public'; diff --git a/x-pack/plugins/siem/public/management/common/constants.ts b/x-pack/plugins/siem/public/management/common/constants.ts new file mode 100644 index 0000000000000..9ec6817c0bfce --- /dev/null +++ b/x-pack/plugins/siem/public/management/common/constants.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { SiemPageName } from '../../app/types'; +import { ManagementStoreGlobalNamespace, ManagementSubTab } from '../types'; + +// --[ ROUTING ]--------------------------------------------------------------------------- +export const MANAGEMENT_ROUTING_ROOT_PATH = `/:pageName(${SiemPageName.management})`; +export const MANAGEMENT_ROUTING_ENDPOINTS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.endpoints})`; +export const MANAGEMENT_ROUTING_POLICIES_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.policies})`; +export const MANAGEMENT_ROUTING_POLICY_DETAILS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.policies})/:policyId`; + +// --[ STORE ]--------------------------------------------------------------------------- +/** The SIEM global store namespace where the management state will be mounted */ +export const MANAGEMENT_STORE_GLOBAL_NAMESPACE: ManagementStoreGlobalNamespace = 'management'; +/** Namespace within the Management state where policy list state is maintained */ +export const MANAGEMENT_STORE_POLICY_LIST_NAMESPACE = 'policyList'; +/** Namespace within the Management state where policy details state is maintained */ +export const MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE = 'policyDetails'; diff --git a/x-pack/plugins/siem/public/management/common/routing.ts b/x-pack/plugins/siem/public/management/common/routing.ts new file mode 100644 index 0000000000000..e64fcf0c5f68a --- /dev/null +++ b/x-pack/plugins/siem/public/management/common/routing.ts @@ -0,0 +1,70 @@ +/* + * 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 { generatePath } from 'react-router-dom'; +import { + MANAGEMENT_ROUTING_ENDPOINTS_PATH, + MANAGEMENT_ROUTING_POLICIES_PATH, + MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, + MANAGEMENT_ROUTING_ROOT_PATH, +} from './constants'; +import { ManagementSubTab } from '../types'; +import { SiemPageName } from '../../app/types'; + +export type GetManagementUrlProps = { + /** + * Exclude the URL prefix (everything to the left of where the router was mounted. + * This may be needed when interacting with react-router (ex. to do `history.push()` or + * validations against matched path) + */ + excludePrefix?: boolean; +} & ( + | { name: 'default' } + | { name: 'endpointList' } + | { name: 'policyList' } + | { name: 'policyDetails'; policyId: string } +); + +// Prefix is (almost) everything to the left of where the Router was mounted. In SIEM, since +// we're using Hash router, thats the `#`. +const URL_PREFIX = '#'; + +/** + * Returns a URL string for a given Management page view + * @param props + */ +export const getManagementUrl = (props: GetManagementUrlProps): string => { + let url = props.excludePrefix ? '' : URL_PREFIX; + + switch (props.name) { + case 'default': + url += generatePath(MANAGEMENT_ROUTING_ROOT_PATH, { + pageName: SiemPageName.management, + }); + break; + case 'endpointList': + url += generatePath(MANAGEMENT_ROUTING_ENDPOINTS_PATH, { + pageName: SiemPageName.management, + tabName: ManagementSubTab.endpoints, + }); + break; + case 'policyList': + url += generatePath(MANAGEMENT_ROUTING_POLICIES_PATH, { + pageName: SiemPageName.management, + tabName: ManagementSubTab.policies, + }); + break; + case 'policyDetails': + url += generatePath(MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, { + pageName: SiemPageName.management, + tabName: ManagementSubTab.policies, + policyId: props.policyId, + }); + break; + } + + return url; +}; diff --git a/x-pack/plugins/siem/public/management/components/management_page_view.tsx b/x-pack/plugins/siem/public/management/components/management_page_view.tsx new file mode 100644 index 0000000000000..13d8525e15e15 --- /dev/null +++ b/x-pack/plugins/siem/public/management/components/management_page_view.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { useParams } from 'react-router-dom'; +import { PageView, PageViewProps } from '../../common/components/endpoint/page_view'; +import { ManagementSubTab } from '../types'; +import { getManagementUrl } from '..'; + +export const ManagementPageView = memo>((options) => { + const { tabName } = useParams<{ tabName: ManagementSubTab }>(); + const tabs = useMemo((): PageViewProps['tabs'] => { + return [ + { + name: i18n.translate('xpack.siem.managementTabs.endpoints', { + defaultMessage: 'Endpoints', + }), + id: ManagementSubTab.endpoints, + isSelected: tabName === ManagementSubTab.endpoints, + href: getManagementUrl({ name: 'endpointList' }), + }, + { + name: i18n.translate('xpack.siem.managementTabs.policies', { defaultMessage: 'Policies' }), + id: ManagementSubTab.policies, + isSelected: tabName === ManagementSubTab.policies, + href: getManagementUrl({ name: 'policyList' }), + }, + ]; + }, [tabName]); + return ; +}); + +ManagementPageView.displayName = 'ManagementPageView'; diff --git a/x-pack/plugins/siem/public/management/index.ts b/x-pack/plugins/siem/public/management/index.ts new file mode 100644 index 0000000000000..86522df110dfb --- /dev/null +++ b/x-pack/plugins/siem/public/management/index.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreStart } from 'kibana/public'; +import { managementReducer, getManagementInitialState, managementMiddlewareFactory } from './store'; +import { getManagementRoutes } from './routes'; +import { StartPlugins } from '../types'; +import { MANAGEMENT_STORE_GLOBAL_NAMESPACE } from './common/constants'; +import { SecuritySubPluginWithStore } from '../app/types'; +import { Immutable } from '../../common/endpoint/types'; +import { ManagementStoreGlobalNamespace } from './types'; +import { ManagementState } from './store/types'; + +export { getManagementUrl } from './common/routing'; + +export class Management { + public setup() {} + + public start( + core: CoreStart, + plugins: StartPlugins + ): SecuritySubPluginWithStore> { + return { + routes: getManagementRoutes(), + store: { + initialState: { + [MANAGEMENT_STORE_GLOBAL_NAMESPACE]: getManagementInitialState(), + }, + reducer: { + [MANAGEMENT_STORE_GLOBAL_NAMESPACE]: managementReducer, + }, + middleware: managementMiddlewareFactory(core, plugins), + }, + }; + } +} diff --git a/x-pack/plugins/siem/public/management/pages/index.tsx b/x-pack/plugins/siem/public/management/pages/index.tsx new file mode 100644 index 0000000000000..aba482db86519 --- /dev/null +++ b/x-pack/plugins/siem/public/management/pages/index.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo } from 'react'; +import { Redirect, Route, Switch } from 'react-router-dom'; +import { SpyRoute } from '../../common/utils/route/spy_routes'; +import { PolicyContainer } from './policy'; +import { + MANAGEMENT_ROUTING_ENDPOINTS_PATH, + MANAGEMENT_ROUTING_POLICIES_PATH, + MANAGEMENT_ROUTING_ROOT_PATH, +} from '../common/constants'; +import { ManagementPageView } from '../components/management_page_view'; +import { NotFoundPage } from '../../app/404'; + +const TmpEndpoints = () => { + return ( + +

    {'Endpoints will go here'}

    + +
    + ); +}; + +export const ManagementContainer = memo(() => { + return ( + + + + } + /> + + + ); +}); + +ManagementContainer.displayName = 'ManagementContainer'; diff --git a/x-pack/plugins/siem/public/management/pages/policy/index.tsx b/x-pack/plugins/siem/public/management/pages/policy/index.tsx new file mode 100644 index 0000000000000..5122bbcd5d55d --- /dev/null +++ b/x-pack/plugins/siem/public/management/pages/policy/index.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo } from 'react'; +import { Route, Switch } from 'react-router-dom'; +import { PolicyDetails, PolicyList } from './view'; +import { + MANAGEMENT_ROUTING_POLICIES_PATH, + MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, +} from '../../common/constants'; +import { NotFoundPage } from '../../../app/404'; + +export const PolicyContainer = memo(() => { + return ( + + + + + + ); +}); + +PolicyContainer.displayName = 'PolicyContainer'; diff --git a/x-pack/plugins/siem/public/endpoint_policy/models/policy_details_config.ts b/x-pack/plugins/siem/public/management/pages/policy/models/policy_details_config.ts similarity index 96% rename from x-pack/plugins/siem/public/endpoint_policy/models/policy_details_config.ts rename to x-pack/plugins/siem/public/management/pages/policy/models/policy_details_config.ts index 44be5ddcc003f..7c67dffb8a663 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/models/policy_details_config.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/models/policy_details_config.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UIPolicyConfig } from '../../../common/endpoint/types'; +import { UIPolicyConfig } from '../../../../../common/endpoint/types'; /** * A typed Object.entries() function where the keys and values are typed based on the given object diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/action.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/action.ts similarity index 87% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_details/action.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_details/action.ts index ceb62a9f9ace9..f729dfbd9a29a 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/action.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/action.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { GetAgentStatusResponse } from '../../../../../ingest_manager/common/types/rest_spec'; -import { PolicyData, UIPolicyConfig } from '../../../../common/endpoint/types'; -import { ServerApiError } from '../../../common/types'; +import { GetAgentStatusResponse } from '../../../../../../../ingest_manager/common/types/rest_spec'; +import { PolicyData, UIPolicyConfig } from '../../../../../../common/endpoint/types'; +import { ServerApiError } from '../../../../../common/types'; import { PolicyDetailsState } from '../../types'; interface ServerReturnedPolicyDetailsData { diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/index.test.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/index.test.ts similarity index 96% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_details/index.test.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_details/index.test.ts index 01a824ecc7b8e..469b71854dfcc 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/index.test.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/index.test.ts @@ -9,7 +9,7 @@ import { createStore, Dispatch, Store } from 'redux'; import { policyDetailsReducer, PolicyDetailsAction } from './index'; import { policyConfig } from './selectors'; import { clone } from '../../models/policy_details_config'; -import { factory as policyConfigFactory } from '../../../../common/endpoint/models/policy_config'; +import { factory as policyConfigFactory } from '../../../../../../common/endpoint/models/policy_config'; describe('policy details: ', () => { let store: Store; diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/index.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/index.ts similarity index 77% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_details/index.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_details/index.ts index 88f090301cfa3..9ccc47f250e4e 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/index.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/index.ts @@ -5,9 +5,9 @@ */ import { PolicyDetailsState } from '../../types'; -import { ImmutableReducer } from '../../../common/store'; -import { AppAction } from '../../../common/store/actions'; -import { Immutable } from '../../../../common/endpoint/types'; +import { ImmutableReducer } from '../../../../../common/store'; +import { AppAction } from '../../../../../common/store/actions'; +import { Immutable } from '../../../../../../common/endpoint/types'; export { policyDetailsMiddlewareFactory } from './middleware'; export { PolicyDetailsAction } from './action'; diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/middleware.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/middleware.ts similarity index 93% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_details/middleware.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_details/middleware.ts index 883d8e780ea67..97cdcac0fcae9 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/middleware.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/middleware.ts @@ -16,9 +16,9 @@ import { sendGetFleetAgentStatusForConfig, sendPutDatasource, } from '../policy_list/services/ingest'; -import { NewPolicyData, PolicyData, Immutable } from '../../../../common/endpoint/types'; -import { factory as policyConfigFactory } from '../../../../common/endpoint/models/policy_config'; -import { ImmutableMiddlewareFactory } from '../../../common/store'; +import { NewPolicyData, PolicyData, Immutable } from '../../../../../../common/endpoint/types'; +import { factory as policyConfigFactory } from '../../../../../../common/endpoint/models/policy_config'; +import { ImmutableMiddlewareFactory } from '../../../../../common/store'; export const policyDetailsMiddlewareFactory: ImmutableMiddlewareFactory { return { diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/selectors.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/selectors.ts similarity index 87% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_details/selectors.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_details/selectors.ts index 3c943986a72e4..d2a5c1b7e14a3 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_details/selectors.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_details/selectors.ts @@ -5,14 +5,17 @@ */ import { createSelector } from 'reselect'; +import { matchPath } from 'react-router-dom'; import { PolicyDetailsState } from '../../types'; import { Immutable, NewPolicyData, PolicyConfig, UIPolicyConfig, -} from '../../../../common/endpoint/types'; -import { factory as policyConfigFactory } from '../../../../common/endpoint/models/policy_config'; +} from '../../../../../../common/endpoint/types'; +import { factory as policyConfigFactory } from '../../../../../../common/endpoint/models/policy_config'; +import { MANAGEMENT_ROUTING_POLICY_DETAILS_PATH } from '../../../../common/constants'; +import { ManagementRoutePolicyDetailsParams } from '../../../../types'; /** Returns the policy details */ export const policyDetails = (state: Immutable) => state.policyItem; @@ -31,22 +34,24 @@ export const policyDetailsForUpdate: ( /** Returns a boolean of whether the user is on the policy details page or not */ export const isOnPolicyDetailsPage = (state: Immutable) => { - if (state.location) { - const pathnameParts = state.location.pathname.split('/'); - return pathnameParts[1] === 'policy' && pathnameParts[2]; - } else { - return false; - } + return ( + matchPath(state.location?.pathname ?? '', { + path: MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, + exact: true, + }) !== null + ); }; /** Returns the policyId from the url */ export const policyIdFromParams: (state: Immutable) => string = createSelector( (state) => state.location, (location: PolicyDetailsState['location']) => { - if (location) { - return location.pathname.split('/')[2]; - } - return ''; + return ( + matchPath(location?.pathname ?? '', { + path: MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, + exact: true, + })?.params?.policyId ?? '' + ); } ); diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/action.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/action.ts similarity index 83% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_list/action.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_list/action.ts index bedbcdae3306f..6866bcbf31f89 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/action.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/action.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PolicyData } from '../../../../common/endpoint/types'; -import { ServerApiError } from '../../../common/types'; +import { PolicyData } from '../../../../../../common/endpoint/types'; +import { ServerApiError } from '../../../../../common/types'; interface ServerReturnedPolicyListData { type: 'serverReturnedPolicyListData'; diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/index.test.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/index.test.ts similarity index 90% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_list/index.test.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_list/index.test.ts index 9b56062879583..c796edff8aabc 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/index.test.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/index.test.ts @@ -7,19 +7,24 @@ import { PolicyListState } from '../../types'; import { Store, applyMiddleware, createStore } from 'redux'; -import { coreMock } from '../../../../../../../src/core/public/mocks'; -import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../../ingest_manager/common'; +import { coreMock } from '../../../../../../../../../src/core/public/mocks'; +import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../../../../ingest_manager/common'; import { policyListReducer, initialPolicyListState } from './reducer'; import { policyListMiddlewareFactory } from './middleware'; import { isOnPolicyListPage, selectIsLoading, urlSearchParams } from './selectors'; -import { DepsStartMock, depsStartMock } from '../../../common/mock/endpoint'; +import { DepsStartMock, depsStartMock } from '../../../../../common/mock/endpoint'; import { setPolicyListApiMockImplementation } from './test_mock_utils'; import { INGEST_API_DATASOURCES } from './services/ingest'; -import { createSpyMiddleware, MiddlewareActionSpyHelper } from '../../../common/store/test_utils'; +import { + createSpyMiddleware, + MiddlewareActionSpyHelper, +} from '../../../../../common/store/test_utils'; +import { getManagementUrl } from '../../../../common/routing'; describe('policy list store concerns', () => { + const policyListPathUrl = getManagementUrl({ name: 'policyList', excludePrefix: true }); let fakeCoreStart: ReturnType; let depsStart: DepsStartMock; let store: Store; @@ -57,7 +62,7 @@ describe('policy list store concerns', () => { store.dispatch({ type: 'userChangedUrl', payload: { - pathname: '/policy', + pathname: policyListPathUrl, search: '', hash: '', }, @@ -70,7 +75,7 @@ describe('policy list store concerns', () => { store.dispatch({ type: 'userChangedUrl', payload: { - pathname: '/policy', + pathname: policyListPathUrl, search: '', hash: '', }, @@ -84,7 +89,7 @@ describe('policy list store concerns', () => { store.dispatch({ type: 'userChangedUrl', payload: { - pathname: '/policy', + pathname: policyListPathUrl, search: '', hash: '', }, @@ -112,7 +117,7 @@ describe('policy list store concerns', () => { store.dispatch({ type: 'userChangedUrl', payload: { - pathname: '/policy', + pathname: policyListPathUrl, search: '', hash: '', }, @@ -132,7 +137,7 @@ describe('policy list store concerns', () => { store.dispatch({ type: 'userChangedUrl', payload: { - pathname: '/policy', + pathname: policyListPathUrl, search: searchParams, hash: '', }, diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/index.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/index.ts similarity index 76% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_list/index.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_list/index.ts index a4f51fcf0ec66..e09f80883d888 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/index.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/index.ts @@ -5,9 +5,9 @@ */ import { PolicyListState } from '../../types'; -import { ImmutableReducer } from '../../../common/store'; -import { AppAction } from '../../../common/store/actions'; -import { Immutable } from '../../../../common/endpoint/types'; +import { ImmutableReducer } from '../../../../../common/store'; +import { AppAction } from '../../../../../common/store/actions'; +import { Immutable } from '../../../../../../common/endpoint/types'; export { policyListReducer } from './reducer'; export { PolicyListAction } from './action'; export { policyListMiddlewareFactory } from './middleware'; diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/middleware.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/middleware.ts similarity index 91% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_list/middleware.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_list/middleware.ts index 8602ab8170565..6054ec34b2d01 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/middleware.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/middleware.ts @@ -7,8 +7,8 @@ import { GetPolicyListResponse, PolicyListState } from '../../types'; import { sendGetEndpointSpecificDatasources } from './services/ingest'; import { isOnPolicyListPage, urlSearchParams } from './selectors'; -import { ImmutableMiddlewareFactory } from '../../../common/store'; -import { Immutable } from '../../../../common/endpoint/types'; +import { ImmutableMiddlewareFactory } from '../../../../../common/store'; +import { Immutable } from '../../../../../../common/endpoint/types'; export const policyListMiddlewareFactory: ImmutableMiddlewareFactory> = ( coreStart diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/reducer.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/reducer.ts similarity index 89% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_list/reducer.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_list/reducer.ts index 80e890602c921..028e46936b293 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/reducer.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/reducer.ts @@ -6,9 +6,9 @@ import { PolicyListState } from '../../types'; import { isOnPolicyListPage } from './selectors'; -import { ImmutableReducer } from '../../../common/store'; -import { AppAction } from '../../../common/store/actions'; -import { Immutable } from '../../../../common/endpoint/types'; +import { ImmutableReducer } from '../../../../../common/store'; +import { AppAction } from '../../../../../common/store/actions'; +import { Immutable } from '../../../../../../common/endpoint/types'; export const initialPolicyListState = (): PolicyListState => { return { diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/selectors.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/selectors.ts similarity index 87% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_list/selectors.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_list/selectors.ts index cd6230a6ed3be..c900ceb186f69 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/selectors.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/selectors.ts @@ -6,8 +6,10 @@ import { createSelector } from 'reselect'; import { parse } from 'query-string'; +import { matchPath } from 'react-router-dom'; import { PolicyListState, PolicyListUrlSearchParams } from '../../types'; -import { Immutable } from '../../../../common/endpoint/types'; +import { Immutable } from '../../../../../../common/endpoint/types'; +import { MANAGEMENT_ROUTING_POLICIES_PATH } from '../../../../common/constants'; const PAGE_SIZES = Object.freeze([10, 20, 50]); @@ -24,7 +26,12 @@ export const selectIsLoading = (state: Immutable) => state.isLo export const selectApiError = (state: Immutable) => state.apiError; export const isOnPolicyListPage = (state: Immutable) => { - return state.location?.pathname === '/policy'; + return ( + matchPath(state.location?.pathname ?? '', { + path: MANAGEMENT_ROUTING_POLICIES_PATH, + exact: true, + }) !== null + ); }; const routeLocation = (state: Immutable) => state.location; diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/services/ingest.test.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/services/ingest.test.ts similarity index 94% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_list/services/ingest.test.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_list/services/ingest.test.ts index df61bbe893c58..cbbc5c3c6fdbe 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/services/ingest.test.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/services/ingest.test.ts @@ -5,8 +5,8 @@ */ import { sendGetDatasource, sendGetEndpointSpecificDatasources } from './ingest'; -import { httpServiceMock } from '../../../../../../../../src/core/public/mocks'; -import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../../../ingest_manager/common'; +import { httpServiceMock } from '../../../../../../../../../../src/core/public/mocks'; +import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../../../../../ingest_manager/common'; describe('ingest service', () => { let http: ReturnType; diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/services/ingest.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/services/ingest.ts similarity index 95% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_list/services/ingest.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_list/services/ingest.ts index 312a3f7491ab2..db482e2a6bdb6 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/services/ingest.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/services/ingest.ts @@ -9,9 +9,9 @@ import { GetDatasourcesRequest, GetAgentStatusResponse, DATASOURCE_SAVED_OBJECT_TYPE, -} from '../../../../../../ingest_manager/common'; +} from '../../../../../../../../ingest_manager/common'; import { GetPolicyListResponse, GetPolicyResponse, UpdatePolicyResponse } from '../../../types'; -import { NewPolicyData } from '../../../../../common/endpoint/types'; +import { NewPolicyData } from '../../../../../../../common/endpoint/types'; const INGEST_API_ROOT = `/api/ingest_manager`; export const INGEST_API_DATASOURCES = `${INGEST_API_ROOT}/datasources`; diff --git a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/test_mock_utils.ts b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/test_mock_utils.ts similarity index 93% rename from x-pack/plugins/siem/public/endpoint_policy/store/policy_list/test_mock_utils.ts rename to x-pack/plugins/siem/public/management/pages/policy/store/policy_list/test_mock_utils.ts index b8fac21b76a26..2c495202dc75b 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/store/policy_list/test_mock_utils.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/store/policy_list/test_mock_utils.ts @@ -6,7 +6,7 @@ import { HttpStart } from 'kibana/public'; import { INGEST_API_DATASOURCES } from './services/ingest'; -import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; +import { EndpointDocGenerator } from '../../../../../../common/endpoint/generate_data'; import { GetPolicyListResponse } from '../../types'; const generator = new EndpointDocGenerator('policy-list'); diff --git a/x-pack/plugins/siem/public/endpoint_policy/types.ts b/x-pack/plugins/siem/public/management/pages/policy/types.ts similarity index 96% rename from x-pack/plugins/siem/public/endpoint_policy/types.ts rename to x-pack/plugins/siem/public/management/pages/policy/types.ts index ba42140589789..f8cc0d5cd0508 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/types.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/types.ts @@ -10,14 +10,14 @@ import { MalwareFields, UIPolicyConfig, AppLocation, -} from '../../common/endpoint/types'; -import { ServerApiError } from '../common/types'; +} from '../../../../common/endpoint/types'; +import { ServerApiError } from '../../../common/types'; import { GetAgentStatusResponse, GetDatasourcesResponse, GetOneDatasourceResponse, UpdateDatasourceResponse, -} from '../../../ingest_manager/common'; +} from '../../../../../ingest_manager/common'; /** * Policy list store state diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/agents_summary.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/agents_summary.tsx similarity index 100% rename from x-pack/plugins/siem/public/endpoint_policy/view/agents_summary.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/agents_summary.tsx diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/index.ts b/x-pack/plugins/siem/public/management/pages/policy/view/index.ts similarity index 100% rename from x-pack/plugins/siem/public/endpoint_policy/view/index.ts rename to x-pack/plugins/siem/public/management/pages/policy/view/index.ts diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_details.test.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_details.test.tsx similarity index 90% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_details.test.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_details.test.tsx index 5d736da4e5635..01e12e6c767a6 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/view/policy_details.test.tsx +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_details.test.tsx @@ -8,12 +8,20 @@ import React from 'react'; import { mount } from 'enzyme'; import { PolicyDetails } from './policy_details'; -import { EndpointDocGenerator } from '../../../common/endpoint/generate_data'; -import { createAppRootMockRenderer } from '../../common/mock/endpoint'; +import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; +import { createAppRootMockRenderer } from '../../../../common/mock/endpoint'; +import { getManagementUrl } from '../../../common/routing'; describe('Policy Details', () => { type FindReactWrapperResponse = ReturnType['find']>; + const policyDetailsPathUrl = getManagementUrl({ + name: 'policyDetails', + policyId: '1', + excludePrefix: true, + }); + const policyListPathUrl = getManagementUrl({ name: 'policyList', excludePrefix: true }); + const policyListPathUrlWithPrefix = getManagementUrl({ name: 'policyList' }); const sleep = (ms = 100) => new Promise((wakeup) => setTimeout(wakeup, ms)); const generator = new EndpointDocGenerator(); const { history, AppWrapper, coreStart } = createAppRootMockRenderer(); @@ -33,7 +41,7 @@ describe('Policy Details', () => { describe('when displayed with invalid id', () => { beforeEach(() => { http.get.mockReturnValue(Promise.reject(new Error('policy not found'))); - history.push('/policy/1'); + history.push(policyDetailsPathUrl); policyView = render(); }); @@ -77,7 +85,7 @@ describe('Policy Details', () => { return Promise.reject(new Error('unknown API call!')); }); - history.push('/policy/1'); + history.push(policyDetailsPathUrl); policyView = render(); }); @@ -89,7 +97,7 @@ describe('Policy Details', () => { const backToListButton = pageHeaderLeft.find('EuiButtonEmpty'); expect(backToListButton.prop('iconType')).toBe('arrowLeft'); - expect(backToListButton.prop('href')).toBe('/mock/app/endpoint/policy'); + expect(backToListButton.prop('href')).toBe(policyListPathUrlWithPrefix); expect(backToListButton.text()).toBe('Back to policy list'); const pageTitle = pageHeaderLeft.find('[data-test-subj="pageViewHeaderLeftTitle"]'); @@ -101,9 +109,9 @@ describe('Policy Details', () => { const backToListButton = policyView.find( 'EuiPageHeaderSection[data-test-subj="pageViewHeaderLeft"] EuiButtonEmpty' ); - expect(history.location.pathname).toEqual('/policy/1'); + expect(history.location.pathname).toEqual(policyDetailsPathUrl); backToListButton.simulate('click', { button: 0 }); - expect(history.location.pathname).toEqual('/policy'); + expect(history.location.pathname).toEqual(policyListPathUrl); }); it('should display agent stats', async () => { await asyncActions; @@ -130,9 +138,9 @@ describe('Policy Details', () => { const cancelbutton = policyView.find( 'EuiButtonEmpty[data-test-subj="policyDetailsCancelButton"]' ); - expect(history.location.pathname).toEqual('/policy/1'); + expect(history.location.pathname).toEqual(policyDetailsPathUrl); cancelbutton.simulate('click', { button: 0 }); - expect(history.location.pathname).toEqual('/policy'); + expect(history.location.pathname).toEqual(policyListPathUrl); }); it('should display save button', async () => { await asyncActions; diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_details.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_details.tsx similarity index 90% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_details.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_details.tsx index c928a374502a5..bddbd378f9427 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/view/policy_details.tsx +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_details.tsx @@ -28,18 +28,21 @@ import { isLoading, apiError, } from '../store/policy_details/selectors'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { AgentsSummary } from './agents_summary'; import { VerticalDivider } from './vertical_divider'; import { WindowsEvents, MacEvents, LinuxEvents } from './policy_forms/events'; import { MalwareProtections } from './policy_forms/protections/malware'; -import { AppAction } from '../../common/store/actions'; -import { useNavigateByRouterEventHandler } from '../../common/hooks/endpoint/use_navigate_by_router_event_handler'; -import { PageView, PageViewHeaderTitle } from '../../common/components/endpoint/page_view'; +import { AppAction } from '../../../../common/store/actions'; +import { useNavigateByRouterEventHandler } from '../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; +import { PageViewHeaderTitle } from '../../../../common/components/endpoint/page_view'; +import { ManagementPageView } from '../../../components/management_page_view'; +import { SpyRoute } from '../../../../common/utils/route/spy_routes'; +import { getManagementUrl } from '../../../common/routing'; export const PolicyDetails = React.memo(() => { const dispatch = useDispatch<(action: AppAction) => void>(); - const { notifications, services } = useKibana(); + const { notifications } = useKibana(); // Store values const policyItem = usePolicyDetailsSelector(policyDetails); @@ -81,7 +84,9 @@ export const PolicyDetails = React.memo(() => { } }, [notifications.toasts, policyName, policyUpdateStatus]); - const handleBackToListOnClick = useNavigateByRouterEventHandler('/policy'); + const handleBackToListOnClick = useNavigateByRouterEventHandler( + getManagementUrl({ name: 'policyList', excludePrefix: true }) + ); const handleSaveOnClick = useCallback(() => { setShowConfirm(true); @@ -103,7 +108,7 @@ export const PolicyDetails = React.memo(() => { // Else, if we have an error, then show error on the page. if (!policyItem) { return ( - + {isPolicyLoading ? ( ) : policyApiError ? ( @@ -111,7 +116,8 @@ export const PolicyDetails = React.memo(() => { {policyApiError?.message} ) : null} - + + ); } @@ -122,7 +128,7 @@ export const PolicyDetails = React.memo(() => { iconType="arrowLeft" contentProps={{ style: { paddingLeft: '0' } }} onClick={handleBackToListOnClick} - href={`${services.http.basePath.get()}/app/endpoint/policy`} + href={getManagementUrl({ name: 'policyList' })} > { onConfirm={handleSaveConfirmation} /> )} - { - + + ); }); diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/config_form.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/config_form.tsx similarity index 100% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/config_form.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/config_form.tsx diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/checkbox.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/checkbox.tsx similarity index 95% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/checkbox.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/checkbox.tsx index fe062526c8d3c..e5f3b2c7e8b7e 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/checkbox.tsx +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/checkbox.tsx @@ -11,7 +11,7 @@ import { useDispatch } from 'react-redux'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { policyConfig } from '../../../store/policy_details/selectors'; import { PolicyDetailsAction } from '../../../store/policy_details'; -import { UIPolicyConfig } from '../../../../../common/endpoint/types'; +import { UIPolicyConfig } from '../../../../../../../common/endpoint/types'; export const EventsCheckbox = React.memo(function ({ name, diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/index.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/index.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/index.tsx diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/linux.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/linux.tsx similarity index 97% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/linux.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/linux.tsx index ff7296ad5a44e..a4f5bb83b6ef3 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/linux.tsx +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/linux.tsx @@ -14,7 +14,7 @@ import { usePolicyDetailsSelector } from '../../policy_hooks'; import { selectedLinuxEvents, totalLinuxEvents } from '../../../store/policy_details/selectors'; import { ConfigForm } from '../config_form'; import { getIn, setIn } from '../../../models/policy_details_config'; -import { UIPolicyConfig } from '../../../../../common/endpoint/types'; +import { UIPolicyConfig } from '../../../../../../../common/endpoint/types'; export const LinuxEvents = React.memo(() => { const selected = usePolicyDetailsSelector(selectedLinuxEvents); diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/mac.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/mac.tsx similarity index 97% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/mac.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/mac.tsx index 1c6d96e555cef..af28a4803518c 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/mac.tsx +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/mac.tsx @@ -14,7 +14,7 @@ import { usePolicyDetailsSelector } from '../../policy_hooks'; import { selectedMacEvents, totalMacEvents } from '../../../store/policy_details/selectors'; import { ConfigForm } from '../config_form'; import { getIn, setIn } from '../../../models/policy_details_config'; -import { UIPolicyConfig } from '../../../../../common/endpoint/types'; +import { UIPolicyConfig } from '../../../../../../../common/endpoint/types'; export const MacEvents = React.memo(() => { const selected = usePolicyDetailsSelector(selectedMacEvents); diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/windows.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/windows.tsx similarity index 98% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/windows.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/windows.tsx index 8add5bed23a29..feddf78cd9c5f 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/events/windows.tsx +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/events/windows.tsx @@ -14,7 +14,7 @@ import { usePolicyDetailsSelector } from '../../policy_hooks'; import { selectedWindowsEvents, totalWindowsEvents } from '../../../store/policy_details/selectors'; import { ConfigForm } from '../config_form'; import { setIn, getIn } from '../../../models/policy_details_config'; -import { UIPolicyConfig, Immutable } from '../../../../../common/endpoint/types'; +import { UIPolicyConfig, Immutable } from '../../../../../../../common/endpoint/types'; export const WindowsEvents = React.memo(() => { const selected = usePolicyDetailsSelector(selectedWindowsEvents); diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/protections/malware.tsx similarity index 98% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/protections/malware.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/protections/malware.tsx index 69c0faf6e800e..e60713ca32d5b 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/view/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_forms/protections/malware.tsx @@ -11,7 +11,7 @@ import { EuiRadio, EuiSwitch, EuiTitle, EuiSpacer, htmlIdGenerator } from '@elas import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Immutable, ProtectionModes } from '../../../../../common/endpoint/types'; +import { Immutable, ProtectionModes } from '../../../../../../../common/endpoint/types'; import { OS, MalwareProtectionOSes } from '../../../types'; import { ConfigForm } from '../config_form'; import { policyConfig } from '../../../store/policy_details/selectors'; diff --git a/x-pack/plugins/siem/public/management/pages/policy/view/policy_hooks.ts b/x-pack/plugins/siem/public/management/pages/policy/view/policy_hooks.ts new file mode 100644 index 0000000000000..97436064eebe2 --- /dev/null +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_hooks.ts @@ -0,0 +1,44 @@ +/* + * 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 { useSelector } from 'react-redux'; +import { PolicyListState, PolicyDetailsState } from '../types'; +import { State } from '../../../../common/store'; +import { + MANAGEMENT_STORE_GLOBAL_NAMESPACE, + MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE, + MANAGEMENT_STORE_POLICY_LIST_NAMESPACE, +} from '../../../common/constants'; + +/** + * Narrows global state down to the PolicyListState before calling the provided Policy List Selector + * @param selector + */ +export function usePolicyListSelector(selector: (state: PolicyListState) => TSelected) { + return useSelector((state: State) => { + return selector( + state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][ + MANAGEMENT_STORE_POLICY_LIST_NAMESPACE + ] as PolicyListState + ); + }); +} + +/** + * Narrows global state down to the PolicyDetailsState before calling the provided Policy Details Selector + * @param selector + */ +export function usePolicyDetailsSelector( + selector: (state: PolicyDetailsState) => TSelected +) { + return useSelector((state: State) => + selector( + state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][ + MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE + ] as PolicyDetailsState + ) + ); +} diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/policy_list.tsx b/x-pack/plugins/siem/public/management/pages/policy/view/policy_list.tsx similarity index 84% rename from x-pack/plugins/siem/public/endpoint_policy/view/policy_list.tsx rename to x-pack/plugins/siem/public/management/pages/policy/view/policy_list.tsx index a9aea57239ed1..3a8004aa2ec6d 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/view/policy_list.tsx +++ b/x-pack/plugins/siem/public/management/pages/policy/view/policy_list.tsx @@ -20,11 +20,13 @@ import { } from '../store/policy_list/selectors'; import { usePolicyListSelector } from './policy_hooks'; import { PolicyListAction } from '../store/policy_list'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { Immutable, PolicyData } from '../../../common/endpoint/types'; -import { useNavigateByRouterEventHandler } from '../../common/hooks/endpoint/use_navigate_by_router_event_handler'; -import { PageView } from '../../common/components/endpoint/page_view'; -import { LinkToApp } from '../../common/components/endpoint/link_to_app'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { Immutable, PolicyData } from '../../../../../common/endpoint/types'; +import { useNavigateByRouterEventHandler } from '../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; +import { LinkToApp } from '../../../../common/components/endpoint/link_to_app'; +import { ManagementPageView } from '../../../components/management_page_view'; +import { SpyRoute } from '../../../../common/utils/route/spy_routes'; +import { getManagementUrl } from '../../../common/routing'; interface TableChangeCallbackArguments { page: { index: number; size: number }; @@ -93,14 +95,13 @@ export const PolicyList = React.memo(() => { }), // eslint-disable-next-line react/display-name render: (value: string, item: Immutable) => { - const routeUri = `/policy/${item.id}`; - return ( - - ); + const routePath = getManagementUrl({ + name: 'policyDetails', + policyId: item.id, + excludePrefix: true, + }); + const routeUrl = getManagementUrl({ name: 'policyDetails', policyId: item.id }); + return ; }, truncateText: true, }, @@ -150,7 +151,7 @@ export const PolicyList = React.memo(() => { ); return ( - { onChange={handleTableChange} data-test-subj="policyTable" /> - + + ); }); diff --git a/x-pack/plugins/siem/public/endpoint_policy/view/vertical_divider.ts b/x-pack/plugins/siem/public/management/pages/policy/view/vertical_divider.ts similarity index 92% rename from x-pack/plugins/siem/public/endpoint_policy/view/vertical_divider.ts rename to x-pack/plugins/siem/public/management/pages/policy/view/vertical_divider.ts index dd74980add7e0..6a3aecb4a6503 100644 --- a/x-pack/plugins/siem/public/endpoint_policy/view/vertical_divider.ts +++ b/x-pack/plugins/siem/public/management/pages/policy/view/vertical_divider.ts @@ -5,7 +5,7 @@ */ import styled from 'styled-components'; -import { EuiTheme } from '../../../../../legacy/common/eui_styled_components'; +import { EuiTheme } from '../../../../../../../legacy/common/eui_styled_components'; type SpacingOptions = keyof EuiTheme['eui']['spacerSizes']; diff --git a/x-pack/plugins/siem/public/management/routes.tsx b/x-pack/plugins/siem/public/management/routes.tsx new file mode 100644 index 0000000000000..fbcea37c76962 --- /dev/null +++ b/x-pack/plugins/siem/public/management/routes.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { Route } from 'react-router-dom'; +import { ManagementContainer } from './pages'; +import { MANAGEMENT_ROUTING_ROOT_PATH } from './common/constants'; + +/** + * Returns the React Router Routes for the management area + */ +export const getManagementRoutes = () => [ + // Mounts the Management interface on `/management` + , +]; diff --git a/x-pack/plugins/siem/public/management/store/index.ts b/x-pack/plugins/siem/public/management/store/index.ts new file mode 100644 index 0000000000000..50049f9828082 --- /dev/null +++ b/x-pack/plugins/siem/public/management/store/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { managementReducer, getManagementInitialState } from './reducer'; +export { managementMiddlewareFactory } from './middleware'; diff --git a/x-pack/plugins/siem/public/management/store/middleware.ts b/x-pack/plugins/siem/public/management/store/middleware.ts new file mode 100644 index 0000000000000..f73736e04a5b7 --- /dev/null +++ b/x-pack/plugins/siem/public/management/store/middleware.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ImmutableMultipleMiddlewareFactory, substateMiddlewareFactory } from '../../common/store'; +import { policyListMiddlewareFactory } from '../pages/policy/store/policy_list'; +import { policyDetailsMiddlewareFactory } from '../pages/policy/store/policy_details'; +import { + MANAGEMENT_STORE_GLOBAL_NAMESPACE, + MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE, + MANAGEMENT_STORE_POLICY_LIST_NAMESPACE, +} from '../common/constants'; + +// @ts-ignore +export const managementMiddlewareFactory: ImmutableMultipleMiddlewareFactory = ( + coreStart, + depsStart +) => { + return [ + substateMiddlewareFactory( + (globalState) => + globalState[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_POLICY_LIST_NAMESPACE], + policyListMiddlewareFactory(coreStart, depsStart) + ), + substateMiddlewareFactory( + (globalState) => + globalState[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE], + policyDetailsMiddlewareFactory(coreStart, depsStart) + ), + ]; +}; diff --git a/x-pack/plugins/siem/public/management/store/reducer.ts b/x-pack/plugins/siem/public/management/store/reducer.ts new file mode 100644 index 0000000000000..ba7927684ad3d --- /dev/null +++ b/x-pack/plugins/siem/public/management/store/reducer.ts @@ -0,0 +1,45 @@ +/* + * 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 { combineReducers as reduxCombineReducers } from 'redux'; +import { + initialPolicyDetailsState, + policyDetailsReducer, +} from '../pages/policy/store/policy_details/reducer'; +import { + initialPolicyListState, + policyListReducer, +} from '../pages/policy/store/policy_list/reducer'; +import { + MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE, + MANAGEMENT_STORE_POLICY_LIST_NAMESPACE, +} from '../common/constants'; +import { ImmutableCombineReducers } from '../../common/store'; +import { AppAction } from '../../common/store/actions'; +import { ManagementState } from './types'; + +// Change the type of `combinerReducers` locally +const combineReducers: ImmutableCombineReducers = reduxCombineReducers; + +/** + * Returns the initial state of the store for the SIEM Management section + */ +export const getManagementInitialState = (): ManagementState => { + return { + [MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: initialPolicyListState(), + [MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: initialPolicyDetailsState(), + }; +}; + +/** + * Redux store reducer for the SIEM Management section + */ +export const managementReducer = combineReducers({ + // @ts-ignore + [MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: policyListReducer, + // @ts-ignore + [MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: policyDetailsReducer, +}); diff --git a/x-pack/plugins/siem/public/management/store/types.ts b/x-pack/plugins/siem/public/management/store/types.ts new file mode 100644 index 0000000000000..884724982fa8f --- /dev/null +++ b/x-pack/plugins/siem/public/management/store/types.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Immutable } from '../../../common/endpoint/types'; +import { PolicyDetailsState, PolicyListState } from '../pages/policy/types'; +import { ImmutableReducer } from '../../common/store'; +import { AppAction } from '../../common/store/actions'; + +/** + * Redux store state for the Management section + */ +export interface ManagementState { + policyDetails: Immutable; + policyList: Immutable; +} + +export interface ManagementPluginState { + management: ManagementState; +} + +export interface ManagementPluginReducer { + management: ImmutableReducer; +} diff --git a/x-pack/plugins/siem/public/management/types.ts b/x-pack/plugins/siem/public/management/types.ts new file mode 100644 index 0000000000000..5ee16bcd434e3 --- /dev/null +++ b/x-pack/plugins/siem/public/management/types.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SiemPageName } from '../app/types'; + +/** + * The type for the management store global namespace. Used mostly internally to reference + * the type while defining more complex interfaces/types + */ +export type ManagementStoreGlobalNamespace = 'management'; + +/** + * The management list of sub-tabs. Changes to these will impact the Router routes. + */ +export enum ManagementSubTab { + endpoints = 'endpoints', + policies = 'policy', +} + +/** + * The URL route params for the Management Policy List section + */ +export interface ManagementRoutePolicyListParams { + pageName: SiemPageName.management; + tabName: ManagementSubTab.policies; +} + +/** + * The URL route params for the Management Policy Details section + */ +export interface ManagementRoutePolicyDetailsParams extends ManagementRoutePolicyListParams { + policyId: string; +} diff --git a/x-pack/plugins/siem/public/plugin.tsx b/x-pack/plugins/siem/public/plugin.tsx index 9bea776220720..4b8fc078fc016 100644 --- a/x-pack/plugins/siem/public/plugin.tsx +++ b/x-pack/plugins/siem/public/plugin.tsx @@ -64,13 +64,8 @@ export class Plugin implements IPlugin { test('Build KQL query with two data provider', () => { const dataProviders = mockDataProviders.slice(0, 2); const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('(name : "Provider 1") or (name : "Provider 2" )'); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('(name : "Provider 1" ) or (name : "Provider 2" )'); }); test('Build KQL query with one data provider and one and', () => { @@ -113,6 +113,23 @@ describe('Build KQL Query', () => { '(name : "Provider 1" and name : "Provider 3" and name : "Provider 4") or (name : "Provider 2" and name : "Provider 5")' ); }); + + test('Build KQL query with all data provider', () => { + const kqlQuery = buildGlobalQuery(mockDataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual( + '(name : "Provider 1" ) or (name : "Provider 2" ) or (name : "Provider 3" ) or (name : "Provider 4" ) or (name : "Provider 5" ) or (name : "Provider 6" ) or (name : "Provider 7" ) or (name : "Provider 8" ) or (name : "Provider 9" ) or (name : "Provider 10" )' + ); + }); + + test('Build complex KQL query with and and or', () => { + const dataProviders = cloneDeep(mockDataProviders); + dataProviders[0].and = mockDataProviders.slice(2, 4); + dataProviders[1].and = mockDataProviders.slice(4, 5); + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual( + '(name : "Provider 1" and name : "Provider 3" and name : "Provider 4") or (name : "Provider 2" and name : "Provider 5") or (name : "Provider 3" ) or (name : "Provider 4" ) or (name : "Provider 5" ) or (name : "Provider 6" ) or (name : "Provider 7" ) or (name : "Provider 8" ) or (name : "Provider 9" ) or (name : "Provider 10" )' + ); + }); }); describe('Combined Queries', () => { diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/helpers.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/helpers.tsx index 776ff114734d9..da74d22575f85 100644 --- a/x-pack/plugins/siem/public/timelines/components/timeline/helpers.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/helpers.tsx @@ -79,9 +79,9 @@ const buildQueryForAndProvider = ( export const buildGlobalQuery = (dataProviders: DataProvider[], browserFields: BrowserFields) => dataProviders .reduce((query, dataProvider: DataProvider, i) => { - const prepend = (q: string) => `${q !== '' ? `(${q}) or ` : ''}`; - const openParen = i > 0 ? '(' : ''; - const closeParen = i > 0 ? ')' : ''; + const prepend = (q: string) => `${q !== '' ? `${q} or ` : ''}`; + const openParen = i >= 0 && dataProviders.length > 1 ? '(' : ''; + const closeParen = i >= 0 && dataProviders.length > 1 ? ')' : ''; return dataProvider.enabled ? `${prepend(query)}${openParen}${buildQueryMatch(dataProvider, browserFields)} ${ 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..4c90869a9fe84 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/README.md +++ b/x-pack/plugins/siem/server/lib/detection_engine/README.md @@ -152,7 +152,7 @@ logging.events: ``` See these two README.md's pages for more references on the alerting and actions API: -https://github.com/elastic/kibana/blob/master/x-pack/plugins/alerting/README.md +https://github.com/elastic/kibana/blob/master/x-pack/plugins/alerts/README.md https://github.com/elastic/kibana/tree/master/x-pack/plugins/actions ### Signals API @@ -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/notifications/create_notifications.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/create_notifications.test.ts index e0414f842ceb3..440efc8d0d5a3 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/create_notifications.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/create_notifications.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { createNotifications } from './create_notifications'; describe('createNotifications', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/create_notifications.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/create_notifications.ts index 35a737177ad49..a472d8a4df4a4 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/create_notifications.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/create_notifications.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Alert } from '../../../../../alerting/common'; +import { Alert } from '../../../../../alerts/common'; import { APP_ID, NOTIFICATIONS_ID } from '../../../../common/constants'; import { CreateNotificationParams } from './types'; import { addTags } from './add_tags'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/delete_notifications.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/delete_notifications.test.ts index 089822f486aeb..2f754c126771a 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/delete_notifications.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/delete_notifications.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { deleteNotifications } from './delete_notifications'; import { readNotifications } from './read_notifications'; jest.mock('./read_notifications'); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/find_notifications.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/find_notifications.ts index b47ea348bd4d6..5d3a328dd6fbb 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/find_notifications.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/find_notifications.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FindResult } from '../../../../../alerting/server'; +import { FindResult } from '../../../../../alerts/server'; import { NOTIFICATIONS_ID } from '../../../../common/constants'; import { FindNotificationParams } from './types'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/get_signals_count.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/get_signals_count.ts index 69f37da1e225b..038a8916c87d5 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/get_signals_count.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/get_signals_count.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertServices } from '../../../../../alerting/server'; +import { AlertServices } from '../../../../../alerts/server'; import { buildSignalsSearchQuery } from './build_signals_query'; interface GetSignalsCount { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/read_notifications.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/read_notifications.test.ts index 961aac15c484d..a46f65da58043 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/read_notifications.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/read_notifications.test.ts @@ -5,7 +5,7 @@ */ import { readNotifications } from './read_notifications'; -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { getNotificationResult, getFindNotificationsResultWithSingleHit, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/read_notifications.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/read_notifications.ts index c585c474556a1..fe9101335b4f5 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/read_notifications.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/read_notifications.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SanitizedAlert } from '../../../../../alerting/common'; +import { SanitizedAlert } from '../../../../../alerts/common'; import { ReadNotificationParams, isAlertType } from './types'; import { findNotifications } from './find_notifications'; import { INTERNAL_RULE_ALERT_ID_KEY } from '../../../../common/constants'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts index e8d778bddadc2..47356679c8075 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts @@ -8,7 +8,7 @@ import { loggingServiceMock } from 'src/core/server/mocks'; import { getResult } from '../routes/__mocks__/request_responses'; import { rulesNotificationAlertType } from './rules_notification_alert_type'; import { buildSignalsSearchQuery } from './build_signals_query'; -import { alertsMock, AlertServicesMock } from '../../../../../../plugins/alerting/server/mocks'; +import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; import { NotificationExecutorOptions } from './types'; jest.mock('./build_signals_query'); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts index a0bd5e092c6ea..a26ddfc90434a 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts @@ -5,7 +5,7 @@ */ import { mapKeys, snakeCase } from 'lodash/fp'; -import { AlertInstance } from '../../../../../alerting/server'; +import { AlertInstance } from '../../../../../alerts/server'; import { RuleTypeParams } from '../types'; export type NotificationRuleTypeParams = RuleTypeParams & { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.ts index d0bb1bfdfb1c4..1345bf2ac6339 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.ts @@ -10,8 +10,8 @@ import { AlertType, State, AlertExecutorOptions, -} from '../../../../../alerting/server'; -import { Alert } from '../../../../../alerting/common'; +} from '../../../../../alerts/server'; +import { Alert } from '../../../../../alerts/common'; import { NOTIFICATIONS_ID } from '../../../../common/constants'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/update_notifications.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/update_notifications.test.ts index b9dc42b96696d..c7763c7ed7e77 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/update_notifications.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/update_notifications.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { updateNotifications } from './update_notifications'; import { readNotifications } from './read_notifications'; import { createNotifications } from './create_notifications'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/update_notifications.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/update_notifications.ts index 5889b0e4dcfb8..17024c7c0d75f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/update_notifications.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/update_notifications.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PartialAlert } from '../../../../../alerting/server'; +import { PartialAlert } from '../../../../../alerts/server'; import { readNotifications } from './read_notifications'; import { UpdateNotificationParams } from './types'; import { addTags } from './add_tags'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts index a24375c368d63..65f38507605a5 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts @@ -10,7 +10,7 @@ import { elasticsearchServiceMock, savedObjectsClientMock, } from '../../../../../../../../src/core/server/mocks'; -import { alertsClientMock } from '../../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../../alerts/server/mocks'; import { licensingMock } from '../../../../../../licensing/server/mocks'; import { siemMock } from '../../../../mocks'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index 83dd87002e8f9..6268451042da1 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -4,6 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { + PrePackagedRulesSchema, + prePackagedRulesSchema, +} from '../../../../../common/detection_engine/schemas/response/prepackaged_rules_schema'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../../common/constants'; import { getIndexExists } from '../../index/get_index_exists'; @@ -14,10 +18,6 @@ import { updatePrepackagedRules } from '../../rules/update_prepacked_rules'; import { getRulesToInstall } from '../../rules/get_rules_to_install'; import { getRulesToUpdate } from '../../rules/get_rules_to_update'; import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules'; -import { - PrePackagedRulesSchema, - prePackagedRulesSchema, -} from '../schemas/response/prepackaged_rules_schema'; import { validate } from './validate'; export const addPrepackedRulesRoute = (router: IRouter) => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index ff6d212deb584..d88cc7fcde504 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -6,6 +6,7 @@ import uuid from 'uuid'; +import { rulesBulkSchema } from '../../../../../common/detection_engine/schemas/response/rules_bulk_schema'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { SetupPlugins } from '../../../../plugin'; @@ -24,7 +25,6 @@ import { buildSiemResponse, } from '../utils'; import { createRulesBulkSchema } from '../schemas/create_rules_bulk_schema'; -import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts index 036e29aa0ebe7..01ad3c7d4e726 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { rulesBulkSchema } from '../../../../../common/detection_engine/schemas/response/rules_bulk_schema'; import { IRouter, RouteConfig, RequestHandler } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { queryRulesBulkSchema } from '../schemas/query_rules_bulk_schema'; -import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; import { getIdBulkError } from './utils'; import { transformValidateBulkError, validate } from './validate'; import { transformBulkError, buildRouteValidation, buildSiemResponse } from '../utils'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts index 67a54f3ba492a..90380b0483c82 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts @@ -4,6 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { + PrePackagedRulesStatusSchema, + prePackagedRulesStatusSchema, +} from '../../../../../common/detection_engine/schemas/response/prepackaged_rules_status_schema'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../../common/constants'; import { transformError, buildSiemResponse } from '../utils'; @@ -12,10 +16,6 @@ import { getRulesToInstall } from '../../rules/get_rules_to_install'; import { getRulesToUpdate } from '../../rules/get_rules_to_update'; import { findRules } from '../../rules/find_rules'; import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules'; -import { - PrePackagedRulesStatusSchema, - prePackagedRulesStatusSchema, -} from '../schemas/response/prepackaged_rules_status_schema'; import { validate } from './validate'; export const getPrepackagedRulesStatusRoute = (router: IRouter) => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index 901e7403bb953..311149087cc49 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -7,6 +7,10 @@ import { chunk } from 'lodash/fp'; import { extname } from 'path'; +import { + ImportRulesSchema, + importRulesSchema, +} from '../../../../../common/detection_engine/schemas/response/import_rules_schema'; import { IRouter } from '../../../../../../../../src/core/server'; import { createPromiseFromStreams } from '../../../../../../../../src/legacy/utils/streams'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; @@ -31,7 +35,6 @@ import { import { ImportRuleAlertRest } from '../../types'; import { patchRules } from '../../rules/patch_rules'; import { importRulesQuerySchema, importRulesPayloadSchema } from '../schemas/import_rules_schema'; -import { ImportRulesSchema, importRulesSchema } from '../schemas/response/import_rules_schema'; import { getTupleDuplicateErrorsAndUniqueRules } from './utils'; import { validate } from './validate'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index cc4d5e03500a4..0d0cd28738c92 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { rulesBulkSchema } from '../../../../../common/detection_engine/schemas/response/rules_bulk_schema'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { SetupPlugins } from '../../../../plugin'; @@ -14,7 +15,6 @@ import { transformBulkError, buildRouteValidation, buildSiemResponse } from '../ import { getIdBulkError } from './utils'; import { transformValidateBulkError, validate } from './validate'; import { patchRulesBulkSchema } from '../schemas/patch_rules_bulk_schema'; -import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; import { patchRules } from '../../rules/patch_rules'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index c0dfecc71ce05..335684dc38b32 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { rulesBulkSchema } from '../../../../../common/detection_engine/schemas/response/rules_bulk_schema'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { SetupPlugins } from '../../../../plugin'; @@ -15,7 +16,6 @@ import { transformValidateBulkError, validate } from './validate'; import { buildRouteValidation, transformBulkError, buildSiemResponse } from '../utils'; import { updateRulesBulkSchema } from '../schemas/update_rules_bulk_schema'; import { updateRules } from '../../rules/update_rules'; -import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts index ec9e84d4fa6bb..df158d23c0e24 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts @@ -23,8 +23,8 @@ import { ImportRuleAlertRest, RuleAlertParamsRest, RuleTypeParams } from '../../ import { BulkError, ImportSuccessError } from '../utils'; import { getSimpleRule, getOutputRuleAlertForRest } from '../__mocks__/utils'; import { createPromiseFromStreams } from '../../../../../../../../src/legacy/utils/streams'; -import { PartialAlert } from '../../../../../../alerting/server'; -import { SanitizedAlert } from '../../../../../../alerting/server/types'; +import { PartialAlert } from '../../../../../../alerts/server'; +import { SanitizedAlert } from '../../../../../../alerts/server/types'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; import { RuleAlertType } from '../../rules/types'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts index 861e6463533fb..5329ff04435ca 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts @@ -8,7 +8,7 @@ import { pickBy, countBy } from 'lodash/fp'; import { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; import uuid from 'uuid'; -import { PartialAlert, FindResult } from '../../../../../../alerting/server'; +import { PartialAlert, FindResult } from '../../../../../../alerts/server'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; import { RuleAlertType, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts index 9069202d4d3aa..07b891e2bf021 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts @@ -13,10 +13,10 @@ import { transformValidateBulkError, } from './validate'; import { getResult } from '../__mocks__/request_responses'; -import { FindResult } from '../../../../../../alerting/server'; -import { RulesSchema } from '../schemas/response/rules_schema'; +import { FindResult } from '../../../../../../alerts/server'; import { BulkError } from '../utils'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; +import { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; export const ruleOutput: RulesSchema = { actions: [], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts index cda3a4b81ed9b..5fc239ed48263 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts @@ -9,9 +9,14 @@ import { fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import * as t from 'io-ts'; +import { findRulesSchema } from '../../../../../common/detection_engine/schemas/response/find_rules_schema'; +import { + RulesSchema, + rulesSchema, +} from '../../../../../common/detection_engine/schemas/response/rules_schema'; import { formatErrors } from '../../../../../common/format_errors'; import { exactCheck } from '../../../../../common/exact_check'; -import { PartialAlert, FindResult } from '../../../../../../alerting/server'; +import { PartialAlert, FindResult } from '../../../../../../alerts/server'; import { isAlertType, IRuleSavedAttributesSavedObjectAttributes, @@ -19,9 +24,7 @@ import { } from '../../rules/types'; import { OutputRuleAlertRest } from '../../types'; import { createBulkErrorObject, BulkError } from '../utils'; -import { rulesSchema, RulesSchema } from '../schemas/response/rules_schema'; import { transformFindAlerts, transform, transformAlertToRule } from './utils'; -import { findRulesSchema } from '../schemas/response/find_rules_schema'; import { RuleActions } from '../../rule_actions/types'; export const transformValidateFindAlerts = ( diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts index 226dea7c20344..66356a1d65352 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertAction } from '../../../../../../alerting/common'; +import { AlertAction } from '../../../../../../alerts/common'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; import { ThreatParams, PrepackagedRules } from '../../types'; import { addPrepackagedRulesSchema } from './add_prepackaged_rules_schema'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts index 1e2941015b735..013db2020a146 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertAction } from '../../../../../../alerting/common'; +import { AlertAction } from '../../../../../../alerts/common'; import { createRulesSchema } from './create_rules_schema'; import { PatchRuleAlertParamsRest } from '../../rules/types'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts index d28530ffb789e..cb03c4781cb6c 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertAction } from '../../../../../../alerting/common'; +import { AlertAction } from '../../../../../../alerts/common'; import { importRulesSchema, importRulesQuerySchema, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts index 755c0b2ccaa3f..218cae68db036 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertAction } from '../../../../../../alerting/common'; +import { AlertAction } from '../../../../../../alerts/common'; import { patchRulesSchema } from './patch_rules_schema'; import { PatchRuleAlertParamsRest } from '../../rules/types'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts deleted file mode 100644 index 1c1bee58f0c97..0000000000000 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts +++ /dev/null @@ -1,97 +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 * as t from 'io-ts'; -import { Either, left, fold } from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; - -import { isMlRule } from '../../../../../../common/machine_learning/helpers'; -import { - dependentRulesSchema, - RequiredRulesSchema, - partialRulesSchema, - requiredRulesSchema, -} from './rules_schema'; -import { typeAndTimelineOnlySchema, TypeAndTimelineOnly } from './type_timeline_only_schema'; - -export const addSavedId = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { - if (typeAndTimelineOnly.type === 'saved_query') { - return [t.exact(t.type({ saved_id: dependentRulesSchema.props.saved_id }))]; - } else { - return []; - } -}; - -export const addTimelineTitle = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { - if (typeAndTimelineOnly.timeline_id != null) { - return [ - t.exact(t.type({ timeline_title: dependentRulesSchema.props.timeline_title })), - t.exact(t.type({ timeline_id: dependentRulesSchema.props.timeline_id })), - ]; - } else { - return []; - } -}; - -export const addQueryFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { - if (typeAndTimelineOnly.type === 'query' || typeAndTimelineOnly.type === 'saved_query') { - return [ - t.exact(t.type({ query: dependentRulesSchema.props.query })), - t.exact(t.type({ language: dependentRulesSchema.props.language })), - ]; - } else { - return []; - } -}; - -export const addMlFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { - if (isMlRule(typeAndTimelineOnly.type)) { - return [ - t.exact(t.type({ anomaly_threshold: dependentRulesSchema.props.anomaly_threshold })), - t.exact( - t.type({ machine_learning_job_id: dependentRulesSchema.props.machine_learning_job_id }) - ), - ]; - } else { - return []; - } -}; - -export const getDependents = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed => { - const dependents: t.Mixed[] = [ - t.exact(requiredRulesSchema), - t.exact(partialRulesSchema), - ...addSavedId(typeAndTimelineOnly), - ...addTimelineTitle(typeAndTimelineOnly), - ...addQueryFields(typeAndTimelineOnly), - ...addMlFields(typeAndTimelineOnly), - ]; - - if (dependents.length > 1) { - // This unsafe cast is because t.intersection does not use an array but rather a set of - // tuples and really does not look like they expected us to ever dynamically build up - // intersections, but here we are doing that. Looking at their code, although they limit - // the array elements to 5, it looks like you have N number of intersections - const unsafeCast: [t.Mixed, t.Mixed] = dependents as [t.Mixed, t.Mixed]; - return t.intersection(unsafeCast); - } else { - // We are not allowed to call t.intersection with a single value so we return without - // it here normally. - return dependents[0]; - } -}; - -export const checkTypeDependents = (input: unknown): Either => { - const typeOnlyDecoded = typeAndTimelineOnlySchema.decode(input); - const onLeft = (errors: t.Errors): Either => left(errors); - const onRight = ( - typeAndTimelineOnly: TypeAndTimelineOnly - ): Either => { - const intersections = getDependents(typeAndTimelineOnly); - return intersections.decode(input); - }; - return pipe(typeOnlyDecoded, fold(onLeft, onRight)); -}; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts deleted file mode 100644 index ade4d12517aca..0000000000000 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts +++ /dev/null @@ -1,289 +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 { left } from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; - -import { rulesSchema, RulesSchema, removeList } from './rules_schema'; -import { getBaseResponsePayload } from './__mocks__/utils'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; -import { foldLeftRight, getPaths } from '../../../../../../common/test_utils'; -import { exactCheck } from '../../../../../../common/exact_check'; - -export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z'; - -describe('rules_schema', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - - test('it should validate a type of "query" without anything extra', () => { - const payload = getBaseResponsePayload(); - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - const expected = getBaseResponsePayload(); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(expected); - }); - - test('it should NOT validate a type of "query" when it has extra data', () => { - const payload: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); - payload.invalid_extra_data = 'invalid_extra_data'; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate invalid_data for the type', () => { - const payload: Omit & { type: string } = getBaseResponsePayload(); - payload.type = 'invalid_data'; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "invalid_data" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a type of "query" with a saved_id together', () => { - const payload = getBaseResponsePayload(); - payload.type = 'query'; - payload.saved_id = 'save id 123'; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "saved_id"']); - expect(message.schema).toEqual({}); - }); - - test('it should validate a type of "saved_query" with a "saved_id" dependent', () => { - const payload = getBaseResponsePayload(); - payload.type = 'saved_query'; - payload.saved_id = 'save id 123'; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - const expected = getBaseResponsePayload(); - - expected.type = 'saved_query'; - expected.saved_id = 'save id 123'; - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(expected); - }); - - test('it should NOT validate a type of "saved_query" without a "saved_id" dependent', () => { - const payload = getBaseResponsePayload(); - payload.type = 'saved_query'; - delete payload.saved_id; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "saved_id"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a type of "saved_query" when it has extra data', () => { - const payload: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); - payload.type = 'saved_query'; - payload.saved_id = 'save id 123'; - payload.invalid_extra_data = 'invalid_extra_data'; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); - expect(message.schema).toEqual({}); - }); - - test('it should validate a type of "timeline_id" if there is a "timeline_title" dependent', () => { - const payload = getBaseResponsePayload(); - payload.timeline_id = 'some timeline id'; - payload.timeline_title = 'some timeline title'; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - const expected = getBaseResponsePayload(); - expected.timeline_id = 'some timeline id'; - expected.timeline_title = 'some timeline title'; - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(expected); - }); - - test('it should NOT validate a type of "timeline_id" if there is "timeline_title" dependent when it has extra invalid data', () => { - const payload: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); - payload.timeline_id = 'some timeline id'; - payload.timeline_title = 'some timeline title'; - payload.invalid_extra_data = 'invalid_extra_data'; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a type of "timeline_id" if there is NOT a "timeline_title" dependent', () => { - const payload = getBaseResponsePayload(); - payload.timeline_id = 'some timeline id'; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "timeline_title"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a type of "timeline_title" if there is NOT a "timeline_id" dependent', () => { - const payload = getBaseResponsePayload(); - payload.timeline_title = 'some timeline title'; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "timeline_title"']); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a type of "saved_query" with a "saved_id" dependent and a "timeline_title" but there is NOT a "timeline_id"', () => { - const payload = getBaseResponsePayload(); - payload.saved_id = 'some saved id'; - payload.type = 'saved_query'; - payload.timeline_title = 'some timeline title'; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "timeline_title"']); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a type of "saved_query" with a "saved_id" dependent and a "timeline_id" but there is NOT a "timeline_title"', () => { - const payload = getBaseResponsePayload(); - payload.saved_id = 'some saved id'; - payload.type = 'saved_query'; - payload.timeline_id = 'some timeline id'; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "timeline_title"', - ]); - expect(message.schema).toEqual({}); - }); - - // TODO: (LIST-FEATURE) Remove this test once the feature flag is deployed - test('it should remove exceptions_list when we need it to be removed because the feature is off but there exists a list in the data', () => { - const payload = getBaseResponsePayload(); - const decoded = rulesSchema.decode(payload); - const listRemoved = removeList(decoded); - const message = pipe(listRemoved, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ - id: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9', - created_at: '2020-02-20T03:57:54.037Z', - updated_at: '2020-02-20T03:57:54.037Z', - created_by: 'elastic', - description: 'some description', - enabled: true, - false_positives: ['false positive 1', 'false positive 2'], - from: 'now-6m', - immutable: false, - name: 'Query with a rule id', - query: 'user.name: root or user.name: admin', - references: ['test 1', 'test 2'], - severity: 'high', - updated_by: 'elastic_kibana', - tags: [], - to: 'now', - type: 'query', - threat: [], - version: 1, - output_index: '.siem-signals-hassanabad-frank-default', - max_signals: 100, - risk_score: 55, - language: 'kuery', - rule_id: 'query-rule-id', - interval: '5m', - status: 'succeeded', - status_date: '2020-02-22T16:47:50.047Z', - last_success_at: '2020-02-22T16:47:50.047Z', - last_success_message: 'succeeded', - }); - }); - - test('it should work with exceptions_list that are not there and not cause invalidation or errors', () => { - const payload = getBaseResponsePayload(); - const { exceptions_list, ...payloadWithoutLists } = payload; - const decoded = rulesSchema.decode(payloadWithoutLists); - const listRemoved = removeList(decoded); - const message = pipe(listRemoved, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ - id: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9', - created_at: '2020-02-20T03:57:54.037Z', - updated_at: '2020-02-20T03:57:54.037Z', - created_by: 'elastic', - description: 'some description', - enabled: true, - false_positives: ['false positive 1', 'false positive 2'], - from: 'now-6m', - immutable: false, - name: 'Query with a rule id', - query: 'user.name: root or user.name: admin', - references: ['test 1', 'test 2'], - severity: 'high', - updated_by: 'elastic_kibana', - tags: [], - to: 'now', - type: 'query', - threat: [], - version: 1, - output_index: '.siem-signals-hassanabad-frank-default', - max_signals: 100, - risk_score: 55, - language: 'kuery', - rule_id: 'query-rule-id', - interval: '5m', - status: 'succeeded', - status_date: '2020-02-22T16:47:50.047Z', - last_success_at: '2020-02-22T16:47:50.047Z', - last_success_message: 'succeeded', - }); - }); -}); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts index b89df0fc0f3ab..8bda16de97775 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertAction } from '../../../../../../alerting/common'; +import { AlertAction } from '../../../../../../alerts/common'; import { updateRulesSchema } from './update_rules_schema'; import { PatchRuleAlertParamsRest } from '../../rules/types'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts index 26c3b29ff2c51..2ff6d6ac646ae 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts @@ -5,7 +5,7 @@ */ import { RuleAlertAction } from '../../../../common/detection_engine/types'; -import { AlertServices } from '../../../../../alerting/server'; +import { AlertServices } from '../../../../../alerts/server'; import { ruleActionsSavedObjectType } from './saved_object_mappings'; import { IRuleActionsAttributesSavedObjectAttributes } from './types'; import { getThrottleOptions, getRuleActionsFromSavedObject } from './utils'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts index 251f9155f9331..3d5734b13ea48 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertServices } from '../../../../../alerting/server'; +import { AlertServices } from '../../../../../alerts/server'; import { ruleActionsSavedObjectType } from './saved_object_mappings'; import { getRuleActionsSavedObject } from './get_rule_actions_saved_object'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts index 83cd59f0a1cde..c36f6ca831c57 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts @@ -5,7 +5,7 @@ */ import { RuleAlertAction } from '../../../../common/detection_engine/types'; -import { AlertServices } from '../../../../../alerting/server'; +import { AlertServices } from '../../../../../alerts/server'; import { ruleActionsSavedObjectType } from './saved_object_mappings'; import { IRuleActionsAttributesSavedObjectAttributes } from './types'; import { getRuleActionsFromSavedObject } from './utils'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts index 3364827d397d2..c650de2a5e2b9 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertServices } from '../../../../../alerting/server'; +import { AlertServices } from '../../../../../alerts/server'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { getRuleActionsSavedObject } from './get_rule_actions_saved_object'; import { createRuleActionsSavedObject } from './create_rule_actions_saved_object'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts index c8a3b1bbc38ad..fd3d107103f19 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertServices } from '../../../../../alerting/server'; +import { AlertServices } from '../../../../../alerts/server'; import { ruleActionsSavedObjectType } from './saved_object_mappings'; import { RulesActionsSavedObject } from './get_rule_actions_saved_object'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts index f4f0a8042d0a5..f086166d0685e 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { getMlResult } from '../routes/__mocks__/request_responses'; import { createRules } from './create_rules'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules.ts index a007fe35b407e..67e066c6670fb 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/create_rules.ts @@ -5,7 +5,7 @@ */ import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; -import { Alert } from '../../../../../alerting/common'; +import { Alert } from '../../../../../alerts/common'; import { APP_ID, SIGNALS_ID } from '../../../../common/constants'; import { CreateRuleParams } from './types'; import { addTags } from './add_tags'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/delete_rules.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/delete_rules.test.ts index 6bc5fc2a88b6d..f96a9e38d6a6c 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/delete_rules.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/delete_rules.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { deleteRules } from './delete_rules'; import { readRules } from './read_rules'; jest.mock('./read_rules'); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/find_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/find_rules.ts index ac600b0b5b218..c634f07387825 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/find_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/find_rules.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FindResult } from '../../../../../alerting/server'; +import { FindResult } from '../../../../../alerts/server'; import { SIGNALS_ID } from '../../../../common/constants'; import { FindRuleParams } from './types'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts index d79b428a2f76d..203a23402f097 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { getResult, getFindResultWithSingleHit, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts index 512164fc3d2e1..a3119131a0037 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts @@ -5,7 +5,7 @@ */ import { INTERNAL_IMMUTABLE_KEY } from '../../../../common/constants'; -import { AlertsClient } from '../../../../../alerting/server'; +import { AlertsClient } from '../../../../../alerts/server'; import { RuleAlertType, isAlertTypes } from './types'; import { findRules } from './find_rules'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts index 6df250f1cf513..ee21c33540024 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts @@ -9,7 +9,7 @@ import { getFindResultWithSingleHit, FindHit, } from '../routes/__mocks__/request_responses'; -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { getExportAll } from './get_export_all'; import { unSetFeatureFlagsForTestsOnly, setFeatureFlagsForTestsOnly } from '../feature_flags'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_all.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_all.ts index 06e70f0bad184..433da2be6b347 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_all.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_all.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertsClient } from '../../../../../alerting/server'; +import { AlertsClient } from '../../../../../alerts/server'; import { getNonPackagedRules } from './get_existing_prepackaged_rules'; import { getExportDetailsNdjson } from './get_export_details_ndjson'; import { transformAlertsToRules } from '../routes/rules/utils'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index 092a9a8faf395..b00b7353a370f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -11,7 +11,7 @@ import { FindHit, } from '../routes/__mocks__/request_responses'; import * as readRules from './read_rules'; -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../feature_flags'; describe('get_export_by_object_ids', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts index beaaaa8701c87..38cf8008f65c8 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertsClient } from '../../../../../alerting/server'; +import { AlertsClient } from '../../../../../alerts/server'; import { getExportDetailsNdjson } from './get_export_details_ndjson'; import { isAlertType } from '../rules/types'; import { readRules } from './read_rules'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts index 0266d702b3dcc..7b2cef9060f8c 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Alert } from '../../../../../alerting/common'; -import { AlertsClient } from '../../../../../alerting/server'; +import { Alert } from '../../../../../alerts/common'; +import { AlertsClient } from '../../../../../alerts/server'; import { createRules } from './create_rules'; import { PrepackagedRules } from '../types'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.test.ts index a42500223012e..3c1267c939345 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.test.ts @@ -5,7 +5,7 @@ */ import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { getResult, getMlResult } from '../routes/__mocks__/request_responses'; import { patchRules } from './patch_rules'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts index 6dfb72532afbb..1e728ae7b8d0b 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts @@ -5,7 +5,7 @@ */ import { defaults } from 'lodash/fp'; -import { PartialAlert } from '../../../../../alerting/server'; +import { PartialAlert } from '../../../../../alerts/server'; import { PatchRuleParams } from './types'; import { addTags } from './add_tags'; import { calculateVersion, calculateName, calculateInterval } from './utils'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts index 600848948be0c..ef8e70c78422c 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts @@ -5,7 +5,7 @@ */ import { readRules } from './read_rules'; -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { getResult, getFindResultWithSingleHit } from '../routes/__mocks__/request_responses'; export class TestError extends Error { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/read_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/read_rules.ts index 9e0d5b3d05b3f..a8b76aeb8c453 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/read_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/read_rules.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SanitizedAlert } from '../../../../../alerting/common'; +import { SanitizedAlert } from '../../../../../alerts/common'; import { INTERNAL_RULE_ID_KEY } from '../../../../common/constants'; import { findRules } from './find_rules'; import { ReadRuleParams, isAlertType } from './types'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/types.ts index d65261549232b..70d53090f81cc 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/types.ts @@ -13,8 +13,8 @@ import { SavedObjectsFindResponse, SavedObjectsClientContract, } from 'kibana/server'; -import { AlertsClient, PartialAlert } from '../../../../../alerting/server'; -import { Alert, SanitizedAlert } from '../../../../../alerting/common'; +import { AlertsClient, PartialAlert } from '../../../../../alerts/server'; +import { Alert, SanitizedAlert } from '../../../../../alerts/common'; import { SIGNALS_ID } from '../../../../common/constants'; import { RuleAlertParams, RuleTypeParams, RuleAlertParamsRest } from '../types'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.test.ts index 2d77e9a707f74..ede5c51d1e5e7 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.test.ts @@ -5,7 +5,7 @@ */ import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { mockPrepackagedRule, getFindResultWithSingleHit, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts index 5063ddd5e52e2..c793d7eb9b6dc 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts @@ -5,7 +5,7 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; -import { AlertsClient } from '../../../../../alerting/server'; +import { AlertsClient } from '../../../../../alerts/server'; import { patchRules } from './patch_rules'; import { PrepackagedRules } from '../types'; import { readRules } from './read_rules'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts index 13c601b40e4f1..222411deb37ab 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts @@ -5,7 +5,7 @@ */ import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { getResult, getMlResult } from '../routes/__mocks__/request_responses'; import { updateRules } from './update_rules'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.ts index 711f019458096..54031b6e35bf1 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.ts @@ -5,7 +5,7 @@ */ import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; -import { PartialAlert } from '../../../../../alerting/server'; +import { PartialAlert } from '../../../../../alerts/server'; import { readRules } from './read_rules'; import { UpdateRuleParams } from './types'; import { addTags } from './add_tags'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts index c5cf85d7ba014..8fceb8ef720b5 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts @@ -5,7 +5,7 @@ */ import { RuleAlertAction } from '../../../../common/detection_engine/types'; -import { AlertsClient, AlertServices } from '../../../../../alerting/server'; +import { AlertsClient, AlertServices } from '../../../../../alerts/server'; import { updateOrCreateRuleActionsSavedObject } from '../rule_actions/update_or_create_rule_actions_saved_object'; import { updateNotifications } from '../notifications/update_notifications'; import { RuleActions } from '../rule_actions/types'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_alert_instances.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_alert_instances.sh index b5f272d0a8a09..a052123f0cc34 100755 --- a/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_alert_instances.sh +++ b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_alert_instances.sh @@ -10,8 +10,8 @@ set -e ./check_env_variables.sh # Example: ./get_alert_instances.sh -# https://github.com/elastic/kibana/blob/master/x-pack/plugins/alerting/README.md#get-apialert_find-find-alerts +# https://github.com/elastic/kibana/blob/master/x-pack/plugins/alerts/README.md#get-apialert_find-find-alerts curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}${SPACE_URL}/api/alert/_find \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/alerts/_find \ | jq . diff --git a/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_alert_types.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_alert_types.sh index 28c250e9368a6..edade604d74ce 100755 --- a/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_alert_types.sh +++ b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_alert_types.sh @@ -10,8 +10,8 @@ set -e ./check_env_variables.sh # Example: ./get_alert_types.sh -# https://github.com/elastic/kibana/blob/master/x-pack/plugins/alerting/README.md#get-apialerttypes-list-alert-types +# https://github.com/elastic/kibana/blob/master/x-pack/plugins/alerts/README.md#get-apialerttypes-list-alert-types curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}${SPACE_URL}/api/alert/types \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/alerts/list_alert_types \ | jq . 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/build_exceptions_query.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_exceptions_query.test.ts index ec8db77dac725..772ebd932698b 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/build_exceptions_query.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_exceptions_query.test.ts @@ -15,7 +15,7 @@ import { formatQuery, getLanguageBooleanOperator, } from './build_exceptions_query'; -import { List } from '../routes/schemas/types/lists_default_array'; +import { List } from '../../../../common/detection_engine/schemas/types/lists_default_array'; describe('build_exceptions_query', () => { describe('getLanguageBooleanOperator', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/build_exceptions_query.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_exceptions_query.ts index e7be5025a51f0..b33a2376589ef 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/build_exceptions_query.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_exceptions_query.ts @@ -3,8 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { + ListOperator, + ListValues, + List, +} from '../../../../common/detection_engine/schemas/types/lists_default_array'; import { Query } from '../../../../../../../src/plugins/data/server'; -import { List, ListOperator, ListValues } from '../routes/schemas/types/lists_default_array'; import { RuleAlertParams, Language } from '../types'; type Operators = 'and' | 'or' | 'not'; 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..80839545951d5 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 @@ -9,7 +9,7 @@ import set from 'set-value'; import { SearchResponse } from 'elasticsearch'; import { Logger } from '../../../../../../../src/core/server'; -import { AlertServices } from '../../../../../alerting/server'; +import { AlertServices } from '../../../../../alerts/server'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams, RefreshTypes } from '../types'; import { singleBulkCreate, SingleBulkCreateResponse } from './single_bulk_create'; @@ -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..d56e167f59e4c --- /dev/null +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/filter_events_with_list.test.ts @@ -0,0 +1,224 @@ +/* + * 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 { getListItemResponseMock } from '../../../../../lists/common/schemas/response/list_item_schema.mock'; +import { listMock } from '../../../../../lists/server/mocks'; + +const someGuids = Array.from({ length: 13 }).map((x) => uuid.v4()); + +describe('filterEventsAgainstList', () => { + let listClient = listMock.getListClient(); + beforeEach(() => { + jest.clearAllMocks(); + listClient = listMock.getListClient(); + listClient.getListItemByValues = jest.fn().mockResolvedValue([]); + }); + + it('should respond with eventSearchResult if exceptionList is empty', async () => { + const res = await filterEventsAgainstList({ + logger: mockLogger, + 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, + 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, + 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, + 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 () => { + listClient.getListItemByValues = jest.fn(({ value }) => + Promise.resolve( + value.slice(0, 2).map((item) => ({ + ...getListItemResponseMock(), + value: item, + })) + ) + ); + const res = await filterEventsAgainstList({ + logger: mockLogger, + 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((listClient.getListItemByValues as jest.Mock).mock.calls[0][0].type).toEqual('ip'); + expect((listClient.getListItemByValues as jest.Mock).mock.calls[0][0].listId).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, + 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 () => { + listClient.getListItemByValues = jest.fn(({ value }) => + Promise.resolve( + value.slice(0, 2).map((item) => ({ + ...getListItemResponseMock(), + value: item, + })) + ) + ); + const res = await filterEventsAgainstList({ + logger: mockLogger, + 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((listClient.getListItemByValues as jest.Mock).mock.calls[0][0].type).toEqual('ip'); + expect((listClient.getListItemByValues as jest.Mock).mock.calls[0][0].listId).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..07435fda0da2e --- /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 { List } from '../../../../common/detection_engine/schemas/types/lists_default_array'; +import { type } from '../../../../../lists/common/schemas/common'; +import { ListClient } from '../../../../../lists/server'; +import { SignalSearchResponse, SearchTypes } from './types'; +import { RuleAlertParams } from '../types'; + +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/get_filter.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/get_filter.test.ts index 35ec1950cedaa..0930fbdb534f5 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/get_filter.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/get_filter.test.ts @@ -6,7 +6,7 @@ import { getQueryFilter, getFilter } from './get_filter'; import { PartialFilter } from '../types'; -import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; +import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; describe('get_filter', () => { let servicesMock: AlertServicesMock; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/get_filter.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/get_filter.ts index 3c226130faf25..1630192b3c03a 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/get_filter.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/get_filter.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertServices } from '../../../../../alerting/server'; +import { AlertServices } from '../../../../../alerts/server'; import { assertUnreachable } from '../../../utils/build_query'; import { Filter, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.test.ts index 6fc99ada16ece..a4ddec13ac513 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; +import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; import { DEFAULT_INDEX_KEY, DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; import { getInputIndex } from './get_input_output_index'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.ts index 85e3eeac476e4..c001312fbf2f5 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.ts @@ -5,7 +5,7 @@ */ import { DEFAULT_INDEX_KEY, DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; -import { AlertServices } from '../../../../../alerting/server'; +import { AlertServices } from '../../../../../alerts/server'; export const getInputIndex = async ( services: AlertServices, 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..163ed76d0c6c3 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,31 @@ 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 { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; import uuid from 'uuid'; +import { getListItemResponseMock } from '../../../../../lists/common/schemas/response/list_item_schema.mock'; +import { listMock } from '../../../../../lists/server/mocks'; describe('searchAfterAndBulkCreate', () => { let mockService: AlertServicesMock; let inputIndexPattern: string[] = []; + let listClient = listMock.getListClient(); + const someGuids = Array.from({ length: 13 }).map(() => uuid.v4()); beforeEach(() => { jest.clearAllMocks(); + listClient = listMock.getListClient(); + listClient.getListItemByValues = jest.fn().mockResolvedValue([]); 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 +49,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 +64,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 +95,21 @@ describe('searchAfterAndBulkCreate', () => { ], }); const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ - someResult: repeatedSearchResultsWithSortId(3, 1, someGuids.slice(6, 9)), ruleParams: sampleParams, + 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 +129,61 @@ 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: [ - { - fakeItemValue: 'fakeItemKey', - }, + listClient, + exceptionsList: [ { - 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 +203,58 @@ 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, + }, + }, + ], + }); + + listClient.getListItemByValues = jest.fn(({ value }) => + Promise.resolve( + value.slice(0, 2).map((item) => ({ + ...getListItemResponseMock(), + value: item, + })) + ) + ); const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ - someResult: sampleDocSearchResultsNoSortIdNoHits(), ruleParams: sampleParams, + listClient, + exceptionsList: undefined, services: mockService, logger: mockLogger, id: sampleRuleGuid, @@ -249,31 +275,31 @@ 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, + 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 +320,38 @@ 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()); + listClient.getListItemByValues = jest.fn(({ value }) => + Promise.resolve( + value.slice(0, 2).map((item) => ({ + ...getListItemResponseMock(), + value: item, + })) + ) + ); const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ - someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), + 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 +373,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, @@ -366,8 +397,29 @@ describe('searchAfterAndBulkCreate', () => { .mockImplementation(() => { throw Error('Fake Error'); }); + listClient.getListItemByValues = jest.fn(({ value }) => + Promise.resolve( + value.slice(0, 2).map((item) => ({ + ...getListItemResponseMock(), + value: item, + })) + ) + ); const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ - someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), + 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 +441,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..e44b82224d1ce 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 { AlertServices } from '../../../../../alerting/server'; +import { AlertServices } from '../../../../../alerts/server'; +import { ListClient } from '../../../../../lists/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..f94eb7006829e 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 @@ -8,7 +8,7 @@ import moment from 'moment'; import { loggingServiceMock } from 'src/core/server/mocks'; import { getResult, getMlResult } from '../routes/__mocks__/request_responses'; import { signalRulesAlertType } from './signal_rule_alert_type'; -import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; +import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; import { ruleStatusServiceFactory } from './rule_status_service'; import { getGapBetweenRuns } from './utils'; import { RuleExecutorOptions } from './types'; @@ -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 { listMock } from '../../../../../lists/server/mocks'; jest.mock('./rule_status_saved_objects_client'); jest.mock('./rule_status_service'); @@ -110,6 +111,7 @@ describe('rules_notification_alert_type', () => { logger, version, ml: mlMock, + lists: listMock.createSetup(), }); }); @@ -199,6 +201,7 @@ describe('rules_notification_alert_type', () => { logger, version, ml: undefined, + lists: undefined, }); await alert.executor(payload); expect(logger.error).toHaveBeenCalled(); @@ -358,7 +361,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..8b9fb0574efe9 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 @@ -18,7 +18,7 @@ import { } from './__mocks__/es_results'; import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; import { singleBulkCreate, filterDuplicateRules } from './single_bulk_create'; -import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; +import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; describe('singleBulkCreate', () => { const mockService: AlertServicesMock = alertsMock.createAlertServices(); @@ -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..6f4d01ea73a79 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 @@ -6,7 +6,7 @@ import { countBy, isEmpty } from 'lodash'; import { performance } from 'perf_hooks'; -import { AlertServices } from '../../../../../alerting/server'; +import { AlertServices } from '../../../../../alerts/server'; import { SignalSearchResponse, BulkResponse } from './types'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams, RefreshTypes } from '../types'; @@ -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..50b0cb27990f8 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 @@ -10,7 +10,7 @@ import { sampleDocSearchResultsWithSortId, } from './__mocks__/es_results'; import { singleSearchAfter } from './single_search_after'; -import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; +import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; describe('singleSearchAfter', () => { const mockService: AlertServicesMock = alertsMock.createAlertServices(); @@ -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..409f374d7df1e 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 @@ -5,7 +5,7 @@ */ import { performance } from 'perf_hooks'; -import { AlertServices } from '../../../../../alerting/server'; +import { AlertServices } from '../../../../../alerts/server'; import { Logger } from '../../../../../../../src/core/server'; import { SignalSearchResponse } from './types'; import { buildEventsSearchQuery } from './build_events_query'; @@ -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..90497b6e34cb4 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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertType, State, AlertExecutorOptions } from '../../../../../alerting/server'; +import { AlertType, State, AlertExecutorOptions } from '../../../../../alerts/server'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleAlertParams, OutputRuleAlertRest } from '../types'; import { SearchResponse } from '../../types'; @@ -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/detection_engine/signals/utils.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/utils.ts index 989c919244d65..f0ca08b73fac6 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/utils.ts @@ -7,7 +7,7 @@ import { createHash } from 'crypto'; import moment from 'moment'; import dateMath from '@elastic/datemath'; -import { parseDuration } from '../../../../../alerting/server'; +import { parseDuration } from '../../../../../alerts/server'; import { BulkResponse, BulkResponseErrorAggregation } from './types'; export const generateId = ( diff --git a/x-pack/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts index d29d885f9797a..d07fa382e114a 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { getResult, getFindResultWithMultiHits } from '../routes/__mocks__/request_responses'; import { INTERNAL_RULE_ID_KEY, INTERNAL_IDENTIFIER } from '../../../../common/constants'; import { readRawTags, readTags, convertTagsToSet, convertToTags, isTags } from './read_tags'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/tags/read_tags.ts b/x-pack/plugins/siem/server/lib/detection_engine/tags/read_tags.ts index 003c852cb80af..2bb2b5ec47e2f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/tags/read_tags.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/tags/read_tags.ts @@ -6,7 +6,7 @@ import { has } from 'lodash/fp'; import { INTERNAL_IDENTIFIER } from '../../../../common/constants'; -import { AlertsClient } from '../../../../../alerting/server'; +import { AlertsClient } from '../../../../../alerts/server'; import { findRules } from '../rules/find_rules'; export interface TagType { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/types.ts b/x-pack/plugins/siem/server/lib/detection_engine/types.ts index f2026804da51a..53c8a9bf0a7e7 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/types.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/types.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ListsDefaultArraySchema } from '../../../common/detection_engine/schemas/types/lists_default_array'; import { CallAPIOptions } from '../../../../../../src/core/server'; import { Filter } from '../../../../../../src/plugins/data/server'; import { IRuleStatusAttributes } from './rules/types'; -import { ListsDefaultArraySchema } from './routes/schemas/types/lists_default_array'; import { RuleAlertAction, RuleType } from '../../../common/detection_engine/types'; export type PartialFilter = Partial; 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/lib/timeline/routes/import_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts index 48c6081e855a0..c16b73ff51b56 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts @@ -7,6 +7,7 @@ import { extname } from 'path'; import { chunk, omit } from 'lodash/fp'; +import { importRulesSchema } from '../../../../common/detection_engine/schemas/response/import_rules_schema'; import { createPromiseFromStreams } from '../../../../../../../src/legacy/utils'; import { IRouter } from '../../../../../../../src/core/server'; @@ -16,7 +17,6 @@ import { SetupPlugins } from '../../../plugin'; import { ConfigType } from '../../../config'; import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; -import { importRulesSchema } from '../../detection_engine/routes/schemas/response/import_rules_schema'; import { validate } from '../../detection_engine/routes/rules/validate'; import { buildSiemResponse, diff --git a/x-pack/plugins/siem/server/plugin.ts b/x-pack/plugins/siem/server/plugin.ts index 3c336991f3d9d..a8858c91d677c 100644 --- a/x-pack/plugins/siem/server/plugin.ts +++ b/x-pack/plugins/siem/server/plugin.ts @@ -15,10 +15,11 @@ import { PluginInitializerContext, Logger, } from '../../../../src/core/server'; -import { PluginSetupContract as AlertingSetup } from '../../alerting/server'; +import { PluginSetupContract as AlertingSetup } from '../../alerts/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'; @@ -45,13 +46,14 @@ import { EndpointAppContext } from './endpoint/types'; import { IngestIndexPatternRetriever } from './endpoint/alerts/index_pattern'; export interface SetupPlugins { - alerting: AlertingSetup; + alerts: AlertingSetup; encryptedSavedObjects?: EncryptedSavedObjectsSetup; features: FeaturesSetup; licensing: LicensingPluginSetup; security?: SecuritySetup; spaces?: SpacesSetup; ml?: MlSetup; + lists?: ListPluginSetup; } export interface StartPlugins { @@ -189,22 +191,23 @@ 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/plugins/telemetry_collection_xpack/server/plugin.ts b/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts index b0afba8852495..3f01d7423eded 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts @@ -18,7 +18,7 @@ export class TelemetryCollectionXpackPlugin implements Plugin { public setup(core: CoreSetup, { telemetryCollectionManager }: TelemetryCollectionXpackDepsSetup) { telemetryCollectionManager.setCollection({ - esCluster: core.elasticsearch.dataClient, + esCluster: core.elasticsearch.legacy.client, title: 'local_xpack', priority: 1, statsGetter: getStatsWithXpack, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4522a641d6dd9..0557663e99696 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3983,16 +3983,16 @@ "xpack.advancedUiActions.customizePanelTimeRange.modal.updatePanelTimeRangeButtonTitle": "更新", "xpack.advancedUiActions.customizeTimeRange.modal.headerTitle": "パネルの時間範囲のカスタマイズ", "xpack.advancedUiActions.customizeTimeRangeMenuItem.displayName": "時間範囲のカスタマイズ", - "xpack.alerting.alertNavigationRegistry.get.missingNavigationError": "「{consumer}」内のアラートタイプ「{alertType}」のナビゲーションは登録されていません。", - "xpack.alerting.alertNavigationRegistry.register.duplicateDefaultError": "「{consumer}」内のデフォルトナビゲーションは既に登録されています。", - "xpack.alerting.alertNavigationRegistry.register.duplicateNavigationError": "「{consumer}」内のアラートタイプ「{alertType}」のナビゲーションは既に登録されています。", - "xpack.alerting.alertsClient.validateActions.invalidGroups": "無効なアクショングループ: {groups}", - "xpack.alerting.alertTypeRegistry.get.missingAlertTypeError": "アラートタイプ\"{id}\"は登録されていません。", - "xpack.alerting.alertTypeRegistry.register.duplicateAlertTypeError": "アラートタイプ\"{id}\"は既に登録されています。", - "xpack.alerting.api.error.disabledApiKeys": "アラートは API キーに依存しますがキーが無効になっているようです", - "xpack.alerting.appName": "アラート", - "xpack.alerting.loadAlertType.missingAlertTypeError": "アラートタイプ「{id}」は登録されていません。", - "xpack.alerting.serverSideErrors.unavailableLicenseInformationErrorMessage": "アラートを利用できません。現在ライセンス情報が利用できません。", + "xpack.alerts.alertNavigationRegistry.get.missingNavigationError": "「{consumer}」内のアラートタイプ「{alertType}」のナビゲーションは登録されていません。", + "xpack.alerts.alertNavigationRegistry.register.duplicateDefaultError": "「{consumer}」内のデフォルトナビゲーションは既に登録されています。", + "xpack.alerts.alertNavigationRegistry.register.duplicateNavigationError": "「{consumer}」内のアラートタイプ「{alertType}」のナビゲーションは既に登録されています。", + "xpack.alerts.alertsClient.validateActions.invalidGroups": "無効なアクショングループ: {groups}", + "xpack.alerts.alertTypeRegistry.get.missingAlertTypeError": "アラートタイプ\"{id}\"は登録されていません。", + "xpack.alerts.alertTypeRegistry.register.duplicateAlertTypeError": "アラートタイプ\"{id}\"は既に登録されています。", + "xpack.alerts.api.error.disabledApiKeys": "アラートは API キーに依存しますがキーが無効になっているようです", + "xpack.alerts.appName": "アラート", + "xpack.alerts.loadAlertType.missingAlertTypeError": "アラートタイプ「{id}」は登録されていません。", + "xpack.alerts.serverSideErrors.unavailableLicenseInformationErrorMessage": "アラートを利用できません。現在ライセンス情報が利用できません。", "xpack.alertingBuiltins.indexThreshold.actionGroupThresholdMetTitle": "しきい値一致", "xpack.alertingBuiltins.indexThreshold.actionVariableContextDateLabel": "アラートがしきい値を超えた日付。", "xpack.alertingBuiltins.indexThreshold.actionVariableContextGroupLabel": "しきい値を超えたグループ。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index bceded8ea3bbb..4f62da260d43f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3986,16 +3986,16 @@ "xpack.advancedUiActions.customizePanelTimeRange.modal.updatePanelTimeRangeButtonTitle": "更新", "xpack.advancedUiActions.customizeTimeRange.modal.headerTitle": "定制面板时间范围", "xpack.advancedUiActions.customizeTimeRangeMenuItem.displayName": "定制时间范围", - "xpack.alerting.alertNavigationRegistry.get.missingNavigationError": "在“{consumer}”内针对告警类型“{alertType}”的导航未注册。", - "xpack.alerting.alertNavigationRegistry.register.duplicateDefaultError": "“{consumer}”内的默认导航已注册。", - "xpack.alerting.alertNavigationRegistry.register.duplicateNavigationError": "在“{consumer}”内针对告警类型“{alertType}”的导航已注册。", - "xpack.alerting.alertsClient.validateActions.invalidGroups": "无效操作组:{groups}", - "xpack.alerting.alertTypeRegistry.get.missingAlertTypeError": "未注册告警类型“{id}”。", - "xpack.alerting.alertTypeRegistry.register.duplicateAlertTypeError": "已注册告警类型“{id}”。", - "xpack.alerting.api.error.disabledApiKeys": "Alerting 依赖的 API 密钥似乎已禁用", - "xpack.alerting.appName": "Alerting", - "xpack.alerting.loadAlertType.missingAlertTypeError": "未注册告警类型“{id}”。", - "xpack.alerting.serverSideErrors.unavailableLicenseInformationErrorMessage": "Alerting 不可用 - 许可信息当前不可用。", + "xpack.alerts.alertNavigationRegistry.get.missingNavigationError": "在“{consumer}”内针对告警类型“{alertType}”的导航未注册。", + "xpack.alerts.alertNavigationRegistry.register.duplicateDefaultError": "“{consumer}”内的默认导航已注册。", + "xpack.alerts.alertNavigationRegistry.register.duplicateNavigationError": "在“{consumer}”内针对告警类型“{alertType}”的导航已注册。", + "xpack.alerts.alertsClient.validateActions.invalidGroups": "无效操作组:{groups}", + "xpack.alerts.alertTypeRegistry.get.missingAlertTypeError": "未注册告警类型“{id}”。", + "xpack.alerts.alertTypeRegistry.register.duplicateAlertTypeError": "已注册告警类型“{id}”。", + "xpack.alerts.api.error.disabledApiKeys": "Alerting 依赖的 API 密钥似乎已禁用", + "xpack.alerts.appName": "Alerting", + "xpack.alerts.loadAlertType.missingAlertTypeError": "未注册告警类型“{id}”。", + "xpack.alerts.serverSideErrors.unavailableLicenseInformationErrorMessage": "Alerting 不可用 - 许可信息当前不可用。", "xpack.alertingBuiltins.indexThreshold.actionGroupThresholdMetTitle": "阈值已达到", "xpack.alertingBuiltins.indexThreshold.actionVariableContextDateLabel": "告警超过阈值的日期。", "xpack.alertingBuiltins.indexThreshold.actionVariableContextGroupLabel": "超过阈值的组。", diff --git a/x-pack/plugins/triggers_actions_ui/README.md b/x-pack/plugins/triggers_actions_ui/README.md index b07413ae73ee8..5a25f7b94050e 100644 --- a/x-pack/plugins/triggers_actions_ui/README.md +++ b/x-pack/plugins/triggers_actions_ui/README.md @@ -258,7 +258,7 @@ Each alert type should be defined as `AlertTypeModel` object with the these prop |requiresAppContext|Define if alert type is enabled for create and edit in the alerting management UI.| IMPORTANT: The current UI supports a single action group only. -Action groups are mapped from the server API result for [GET /api/alert/types: List alert types](https://github.com/elastic/kibana/tree/master/x-pack/legacy/plugins/alerting#get-apialerttypes-list-alert-types). +Action groups are mapped from the server API result for [GET /api/alerts/list_alert_types: List alert types](https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerts#get-apialerttypes-list-alert-types). Server side alert type model: ``` export interface AlertType { @@ -296,7 +296,7 @@ triggers_actions_ui.alertTypeRegistry.register(getSomeNewAlertType()); ## Create and register new alert type UI example -Before registering a UI for a new Alert Type, you should first register the type on the server-side by following the Alerting guide: https://github.com/elastic/kibana/tree/master/x-pack/legacy/plugins/alerting#example +Before registering a UI for a new Alert Type, you should first register the type on the server-side by following the Alerting guide: https://github.com/elastic/kibana/tree/master/x-pack/plugins/alerts#example Alert type UI is expected to be defined as `AlertTypeModel` object. @@ -1257,7 +1257,7 @@ Then this dependencies will be used to embed Actions form or register your own a const initialAlert = ({ name: 'test', params: {}, - consumer: 'alerting', + consumer: 'alerts', alertTypeId: '.index-threshold', schedule: { interval: '1m', diff --git a/x-pack/plugins/triggers_actions_ui/kibana.json b/x-pack/plugins/triggers_actions_ui/kibana.json index 11cbacee08afc..d8f5055368831 100644 --- a/x-pack/plugins/triggers_actions_ui/kibana.json +++ b/x-pack/plugins/triggers_actions_ui/kibana.json @@ -3,7 +3,7 @@ "version": "kibana", "server": false, "ui": true, - "optionalPlugins": ["alerting", "alertingBuiltins"], + "optionalPlugins": ["alerts", "alertingBuiltins"], "requiredPlugins": ["management", "charts", "data"], "configPath": ["xpack", "trigger_actions_ui"] } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx index ebd9294ce1e6d..d7dc674ef8d0c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx @@ -22,7 +22,7 @@ import { ActionTypeModel, AlertTypeModel } from '../types'; import { TypeRegistry } from './type_registry'; import { ChartsPluginStart } from '../../../../../src/plugins/charts/public'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; -import { PluginStartContract as AlertingStart } from '../../../alerting/public'; +import { PluginStartContract as AlertingStart } from '../../../alerts/public'; import { suspendedComponentWithProps } from './lib/suspended_component_with_props'; const TriggersActionsUIHome = lazy(async () => import('./home')); @@ -34,7 +34,7 @@ export interface AppDeps { dataPlugin: DataPublicPluginStart; charts: ChartsPluginStart; chrome: ChromeStart; - alerting?: AlertingStart; + alerts?: AlertingStart; navigateToApp: CoreStart['application']['navigateToApp']; docLinks: DocLinksStart; toastNotifications: ToastsSetup; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx index 84cbc73ca92ca..244d431930f2e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx @@ -33,7 +33,7 @@ import { getThresholdAlertVisualizationData } from '../../../../common/lib/index import { AggregationType, Comparator } from '../../../../common/types'; import { AlertsContextValue } from '../../../context/alerts_context'; import { IndexThresholdAlertParams } from './types'; -import { parseDuration } from '../../../../../../alerting/common/parse_duration'; +import { parseDuration } from '../../../../../../alerts/common/parse_duration'; const customTheme = () => { return { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts index 265cfddab4c06..47b55f44bfb92 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export { BASE_ALERT_API_PATH } from '../../../../alerting/common'; +export { BASE_ALERT_API_PATH } from '../../../../alerts/common'; export { BASE_ACTION_API_PATH } from '../../../../actions/common'; export const BASE_PATH = '/management/insightsAndAlerting/triggersActions'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts index c35dd06385448..714dc5210e390 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts @@ -23,7 +23,7 @@ function prefixKeys(actionVariables: ActionVariable[], prefix: string): ActionVa } // this list should be the same as in: -// x-pack/plugins/alerting/server/task_runner/transform_action_params.ts +// x-pack/plugins/alerts/server/task_runner/transform_action_params.ts function getAlwaysProvidedActionVariables(): ActionVariable[] { const result: ActionVariable[] = []; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts index f384a78e2e080..94d9166b40909 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts @@ -53,7 +53,7 @@ describe('loadAlertTypes', () => { expect(result).toEqual(resolvedValue); expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "/api/alert/types", + "/api/alerts/list_alert_types", ] `); }); @@ -80,7 +80,7 @@ describe('loadAlert', () => { http.get.mockResolvedValueOnce(resolvedValue); expect(await loadAlert({ http, alertId })).toEqual(resolvedValue); - expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}`); + expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}`); }); }); @@ -99,7 +99,7 @@ describe('loadAlertState', () => { http.get.mockResolvedValueOnce(resolvedValue); expect(await loadAlertState({ http, alertId })).toEqual(resolvedValue); - expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}/state`); + expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}/state`); }); test('should parse AlertInstances', async () => { @@ -136,7 +136,7 @@ describe('loadAlertState', () => { }, }, }); - expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}/state`); + expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}/state`); }); test('should handle empty response from api', async () => { @@ -144,7 +144,7 @@ describe('loadAlertState', () => { http.get.mockResolvedValueOnce(''); expect(await loadAlertState({ http, alertId })).toEqual({}); - expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}/state`); + expect(http.get).toHaveBeenCalledWith(`/api/alerts/alert/${alertId}/state`); }); }); @@ -162,7 +162,7 @@ describe('loadAlerts', () => { expect(result).toEqual(resolvedValue); expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "/api/alert/_find", + "/api/alerts/_find", Object { "query": Object { "default_search_operator": "AND", @@ -192,7 +192,7 @@ describe('loadAlerts', () => { expect(result).toEqual(resolvedValue); expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "/api/alert/_find", + "/api/alerts/_find", Object { "query": Object { "default_search_operator": "AND", @@ -226,7 +226,7 @@ describe('loadAlerts', () => { expect(result).toEqual(resolvedValue); expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "/api/alert/_find", + "/api/alerts/_find", Object { "query": Object { "default_search_operator": "AND", @@ -260,7 +260,7 @@ describe('loadAlerts', () => { expect(result).toEqual(resolvedValue); expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "/api/alert/_find", + "/api/alerts/_find", Object { "query": Object { "default_search_operator": "AND", @@ -295,7 +295,7 @@ describe('loadAlerts', () => { expect(result).toEqual(resolvedValue); expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "/api/alert/_find", + "/api/alerts/_find", Object { "query": Object { "default_search_operator": "AND", @@ -330,7 +330,7 @@ describe('loadAlerts', () => { expect(result).toEqual(resolvedValue); expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "/api/alert/_find", + "/api/alerts/_find", Object { "query": Object { "default_search_operator": "AND", @@ -356,13 +356,13 @@ describe('deleteAlerts', () => { expect(http.delete.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/api/alert/1", + "/api/alerts/alert/1", ], Array [ - "/api/alert/2", + "/api/alerts/alert/2", ], Array [ - "/api/alert/3", + "/api/alerts/alert/3", ], ] `); @@ -373,7 +373,7 @@ describe('createAlert', () => { test('should call create alert API', async () => { const alertToCreate = { name: 'test', - consumer: 'alerting', + consumer: 'alerts', tags: ['foo'], enabled: true, alertTypeId: 'test', @@ -402,9 +402,9 @@ describe('createAlert', () => { expect(result).toEqual(resolvedValue); expect(http.post.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "/api/alert", + "/api/alerts/alert", Object { - "body": "{\\"name\\":\\"test\\",\\"consumer\\":\\"alerting\\",\\"tags\\":[\\"foo\\"],\\"enabled\\":true,\\"alertTypeId\\":\\"test\\",\\"schedule\\":{\\"interval\\":\\"1m\\"},\\"actions\\":[],\\"params\\":{},\\"throttle\\":null,\\"createdAt\\":\\"1970-01-01T00:00:00.000Z\\",\\"updatedAt\\":\\"1970-01-01T00:00:00.000Z\\",\\"apiKey\\":null,\\"apiKeyOwner\\":null}", + "body": "{\\"name\\":\\"test\\",\\"consumer\\":\\"alerts\\",\\"tags\\":[\\"foo\\"],\\"enabled\\":true,\\"alertTypeId\\":\\"test\\",\\"schedule\\":{\\"interval\\":\\"1m\\"},\\"actions\\":[],\\"params\\":{},\\"throttle\\":null,\\"createdAt\\":\\"1970-01-01T00:00:00.000Z\\",\\"updatedAt\\":\\"1970-01-01T00:00:00.000Z\\",\\"apiKey\\":null,\\"apiKeyOwner\\":null}", }, ] `); @@ -415,7 +415,7 @@ describe('updateAlert', () => { test('should call alert update API', async () => { const alertToUpdate = { throttle: '1m', - consumer: 'alerting', + consumer: 'alerts', name: 'test', tags: ['foo'], schedule: { @@ -444,7 +444,7 @@ describe('updateAlert', () => { expect(result).toEqual(resolvedValue); expect(http.put.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "/api/alert/123", + "/api/alerts/alert/123", Object { "body": "{\\"throttle\\":\\"1m\\",\\"name\\":\\"test\\",\\"tags\\":[\\"foo\\"],\\"schedule\\":{\\"interval\\":\\"1m\\"},\\"params\\":{},\\"actions\\":[]}", }, @@ -460,7 +460,7 @@ describe('enableAlert', () => { expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/api/alert/1/_enable", + "/api/alerts/alert/1/_enable", ], ] `); @@ -474,7 +474,7 @@ describe('disableAlert', () => { expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/api/alert/1/_disable", + "/api/alerts/alert/1/_disable", ], ] `); @@ -488,7 +488,7 @@ describe('muteAlertInstance', () => { expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/api/alert/1/alert_instance/123/_mute", + "/api/alerts/alert/1/alert_instance/123/_mute", ], ] `); @@ -502,7 +502,7 @@ describe('unmuteAlertInstance', () => { expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/api/alert/1/alert_instance/123/_unmute", + "/api/alerts/alert/1/alert_instance/123/_unmute", ], ] `); @@ -516,7 +516,7 @@ describe('muteAlert', () => { expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/api/alert/1/_mute_all", + "/api/alerts/alert/1/_mute_all", ], ] `); @@ -530,7 +530,7 @@ describe('unmuteAlert', () => { expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/api/alert/1/_unmute_all", + "/api/alerts/alert/1/_unmute_all", ], ] `); @@ -545,13 +545,13 @@ describe('enableAlerts', () => { expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/api/alert/1/_enable", + "/api/alerts/alert/1/_enable", ], Array [ - "/api/alert/2/_enable", + "/api/alerts/alert/2/_enable", ], Array [ - "/api/alert/3/_enable", + "/api/alerts/alert/3/_enable", ], ] `); @@ -566,13 +566,13 @@ describe('disableAlerts', () => { expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/api/alert/1/_disable", + "/api/alerts/alert/1/_disable", ], Array [ - "/api/alert/2/_disable", + "/api/alerts/alert/2/_disable", ], Array [ - "/api/alert/3/_disable", + "/api/alerts/alert/3/_disable", ], ] `); @@ -587,13 +587,13 @@ describe('muteAlerts', () => { expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/api/alert/1/_mute_all", + "/api/alerts/alert/1/_mute_all", ], Array [ - "/api/alert/2/_mute_all", + "/api/alerts/alert/2/_mute_all", ], Array [ - "/api/alert/3/_mute_all", + "/api/alerts/alert/3/_mute_all", ], ] `); @@ -608,13 +608,13 @@ describe('unmuteAlerts', () => { expect(http.post.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/api/alert/1/_unmute_all", + "/api/alerts/alert/1/_unmute_all", ], Array [ - "/api/alert/2/_unmute_all", + "/api/alerts/alert/2/_unmute_all", ], Array [ - "/api/alert/3/_unmute_all", + "/api/alerts/alert/3/_unmute_all", ], ] `); @@ -628,7 +628,7 @@ describe('health', () => { expect(http.get.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "/api/alert/_health", + "/api/alerts/_health", ], ] `); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts index 2176f978822ca..35fdc3974a296 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.ts @@ -9,12 +9,12 @@ import * as t from 'io-ts'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { pick } from 'lodash'; -import { alertStateSchema, AlertingFrameworkHealth } from '../../../../alerting/common'; +import { alertStateSchema, AlertingFrameworkHealth } from '../../../../alerts/common'; import { BASE_ALERT_API_PATH } from '../constants'; import { Alert, AlertType, AlertWithoutId, AlertTaskState } from '../../types'; export async function loadAlertTypes({ http }: { http: HttpSetup }): Promise { - return await http.get(`${BASE_ALERT_API_PATH}/types`); + return await http.get(`${BASE_ALERT_API_PATH}/list_alert_types`); } export async function loadAlert({ @@ -24,7 +24,7 @@ export async function loadAlert({ http: HttpSetup; alertId: string; }): Promise { - return await http.get(`${BASE_ALERT_API_PATH}/${alertId}`); + return await http.get(`${BASE_ALERT_API_PATH}/alert/${alertId}`); } type EmptyHttpResponse = ''; @@ -36,7 +36,7 @@ export async function loadAlertState({ alertId: string; }): Promise { return await http - .get(`${BASE_ALERT_API_PATH}/${alertId}/state`) + .get(`${BASE_ALERT_API_PATH}/alert/${alertId}/state`) .then((state: AlertTaskState | EmptyHttpResponse) => (state ? state : {})) .then((state: AlertTaskState) => { return pipe( @@ -104,7 +104,7 @@ export async function deleteAlerts({ }): Promise<{ successes: string[]; errors: string[] }> { const successes: string[] = []; const errors: string[] = []; - await Promise.all(ids.map((id) => http.delete(`${BASE_ALERT_API_PATH}/${id}`))).then( + await Promise.all(ids.map((id) => http.delete(`${BASE_ALERT_API_PATH}/alert/${id}`))).then( function (fulfilled) { successes.push(...fulfilled); }, @@ -122,7 +122,7 @@ export async function createAlert({ http: HttpSetup; alert: Omit; }): Promise { - return await http.post(`${BASE_ALERT_API_PATH}`, { + return await http.post(`${BASE_ALERT_API_PATH}/alert`, { body: JSON.stringify(alert), }); } @@ -136,7 +136,7 @@ export async function updateAlert({ alert: Pick; id: string; }): Promise { - return await http.put(`${BASE_ALERT_API_PATH}/${id}`, { + return await http.put(`${BASE_ALERT_API_PATH}/alert/${id}`, { body: JSON.stringify( pick(alert, ['throttle', 'name', 'tags', 'schedule', 'params', 'actions']) ), @@ -144,7 +144,7 @@ export async function updateAlert({ } export async function enableAlert({ id, http }: { id: string; http: HttpSetup }): Promise { - await http.post(`${BASE_ALERT_API_PATH}/${id}/_enable`); + await http.post(`${BASE_ALERT_API_PATH}/alert/${id}/_enable`); } export async function enableAlerts({ @@ -158,7 +158,7 @@ export async function enableAlerts({ } export async function disableAlert({ id, http }: { id: string; http: HttpSetup }): Promise { - await http.post(`${BASE_ALERT_API_PATH}/${id}/_disable`); + await http.post(`${BASE_ALERT_API_PATH}/alert/${id}/_disable`); } export async function disableAlerts({ @@ -180,7 +180,7 @@ export async function muteAlertInstance({ instanceId: string; http: HttpSetup; }): Promise { - await http.post(`${BASE_ALERT_API_PATH}/${id}/alert_instance/${instanceId}/_mute`); + await http.post(`${BASE_ALERT_API_PATH}/alert/${id}/alert_instance/${instanceId}/_mute`); } export async function unmuteAlertInstance({ @@ -192,11 +192,11 @@ export async function unmuteAlertInstance({ instanceId: string; http: HttpSetup; }): Promise { - await http.post(`${BASE_ALERT_API_PATH}/${id}/alert_instance/${instanceId}/_unmute`); + await http.post(`${BASE_ALERT_API_PATH}/alert/${id}/alert_instance/${instanceId}/_unmute`); } export async function muteAlert({ id, http }: { id: string; http: HttpSetup }): Promise { - await http.post(`${BASE_ALERT_API_PATH}/${id}/_mute_all`); + await http.post(`${BASE_ALERT_API_PATH}/alert/${id}/_mute_all`); } export async function muteAlerts({ ids, http }: { ids: string[]; http: HttpSetup }): Promise { @@ -204,7 +204,7 @@ export async function muteAlerts({ ids, http }: { ids: string[]; http: HttpSetup } export async function unmuteAlert({ id, http }: { id: string; http: HttpSetup }): Promise { - await http.post(`${BASE_ALERT_API_PATH}/${id}/_unmute_all`); + await http.post(`${BASE_ALERT_API_PATH}/alert/${id}/_unmute_all`); } export async function unmuteAlerts({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index df7d1e64c8e91..7ce952e9b3e0a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -160,7 +160,7 @@ describe('action_form', () => { const initialAlert = ({ name: 'test', params: {}, - consumer: 'alerting', + consumer: 'alerts', alertTypeId: alertType.id, schedule: { interval: '1m', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx index 4a4fce5094f0d..3d16bdfa61a00 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx @@ -13,7 +13,7 @@ import { actionTypeRegistryMock } from '../../../action_type_registry.mock'; import { AppContextProvider } from '../../../app_context'; import { chartPluginMock } from '../../../../../../../../src/plugins/charts/public/mocks'; import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks'; -import { alertingPluginMock } from '../../../../../../alerting/public/mocks'; +import { alertingPluginMock } from '../../../../../../alerts/public/mocks'; jest.mock('../../../lib/action_connector_api', () => ({ loadAllActions: jest.fn(), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.test.tsx index e2d9c5cb7fffe..54d335aaba5aa 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.test.tsx @@ -13,7 +13,7 @@ import { ViewInApp } from './view_in_app'; import { useAppDependencies } from '../../../app_context'; jest.mock('../../../app_context', () => { - const alerting = { + const alerts = { getNavigation: jest.fn(async (id) => id === 'alert-with-nav' ? { path: '/alert' } : undefined ), @@ -23,7 +23,7 @@ jest.mock('../../../app_context', () => { useAppDependencies: jest.fn(() => ({ http: jest.fn(), navigateToApp, - alerting, + alerts, legacy: { capabilities: { get: jest.fn(() => ({})), @@ -41,7 +41,7 @@ describe('view in app', () => { describe('link to the app that created the alert', () => { it('is disabled when there is no navigation', async () => { const alert = mockAlert(); - const { alerting } = useAppDependencies(); + const { alerts } = useAppDependencies(); let component: ReactWrapper; await act(async () => { @@ -53,7 +53,7 @@ describe('view in app', () => { expect(component!.find('button').prop('disabled')).toBe(true); expect(component!.text()).toBe('View in app'); - expect(alerting!.getNavigation).toBeCalledWith(alert.id); + expect(alerts!.getNavigation).toBeCalledWith(alert.id); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.tsx index f1f5d8323c22a..5b5de070a94e6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/view_in_app.tsx @@ -16,7 +16,7 @@ import { AlertNavigation, AlertStateNavigation, AlertUrlNavigation, -} from '../../../../../../alerting/common'; +} from '../../../../../../alerts/common'; import { Alert } from '../../../../types'; export interface ViewInAppProps { @@ -28,7 +28,7 @@ const NO_NAVIGATION = false; type AlertNavigationLoadingState = AlertNavigation | false | null; export const ViewInApp: React.FunctionComponent = ({ alert }) => { - const { navigateToApp, alerting: maybeAlerting } = useAppDependencies(); + const { navigateToApp, alerts: maybeAlerting } = useAppDependencies(); const [alertNavigation, setAlertNavigation] = useState(null); useEffect(() => { @@ -40,13 +40,14 @@ export const ViewInApp: React.FunctionComponent = ({ alert }) => * navigation isn't supported */ () => setAlertNavigation(NO_NAVIGATION), - (alerting) => - alerting + (alerts) => { + return alerts .getNavigation(alert.id) .then((nav) => (nav ? setAlertNavigation(nav) : setAlertNavigation(NO_NAVIGATION))) .catch(() => { setAlertNavigation(NO_NAVIGATION); - }) + }); + } ) ); }, [alert.id, maybeAlerting]); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx index 56874f3d38b64..f6e8dc49ec275 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx @@ -120,11 +120,7 @@ describe('alert_add', () => { }, }} > - {}} - /> + {}} /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx index bb7e593170f8b..e408c7fcb8144 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx @@ -84,7 +84,7 @@ describe('alert_edit', () => { window: '1s', comparator: 'between', }, - consumer: 'alerting', + consumer: 'alerts', alertTypeId: 'my-alert-type', enabled: false, schedule: { interval: '1m' }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx index ed36bc6c8d580..c9ce2848c5670 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx @@ -85,7 +85,7 @@ describe('alert_form', () => { const initialAlert = ({ name: 'test', params: {}, - consumer: 'alerting', + consumer: 'alerts', schedule: { interval: '1m', }, @@ -302,7 +302,7 @@ describe('alert_form', () => { name: 'test', alertTypeId: alertType.id, params: {}, - consumer: 'alerting', + consumer: 'alerts', schedule: { interval: '1m', }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index 87e018ebe3376..874091b2bb7a8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -30,7 +30,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { getDurationNumberInItsUnit, getDurationUnitValue, -} from '../../../../../alerting/common/parse_duration'; +} from '../../../../../alerts/common/parse_duration'; import { loadAlertTypes } from '../../lib/alert_api'; import { actionVariablesFromAlertType } from '../../lib/action_variables'; import { AlertReducerAction } from './alert_reducer'; @@ -168,7 +168,7 @@ export const AlertForm = ({ : null; const alertTypeRegistryList = - alert.consumer === 'alerting' + alert.consumer === 'alerts' ? alertTypeRegistry .list() .filter( @@ -179,6 +179,7 @@ export const AlertForm = ({ .filter( (alertTypeRegistryItem: AlertTypeModel) => alertTypesIndex && + alertTypesIndex[alertTypeRegistryItem.id] && alertTypesIndex[alertTypeRegistryItem.id].producer === alert.consumer ); const alertTypeNodes = alertTypeRegistryList.map(function (item, index) { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_reducer.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_reducer.test.ts index bd320de144024..4e4d8e237aa2f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_reducer.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_reducer.test.ts @@ -11,7 +11,7 @@ describe('alert reducer', () => { beforeAll(() => { initialAlert = ({ params: {}, - consumer: 'alerting', + consumer: 'alerts', alertTypeId: null, schedule: { interval: '1m', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index cf1524094b41d..a59a4a37bec1f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -15,7 +15,7 @@ import { ValidationResult } from '../../../../types'; import { AppContextProvider } from '../../../app_context'; import { chartPluginMock } from '../../../../../../../../src/plugins/charts/public/mocks'; import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks'; -import { alertingPluginMock } from '../../../../../../alerting/public/mocks'; +import { alertingPluginMock } from '../../../../../../alerts/public/mocks'; jest.mock('../../../lib/action_connector_api', () => ({ loadActionTypes: jest.fn(), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index bd4676cd83071..2929ce6defeaf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -439,7 +439,7 @@ export const AlertsList: React.FunctionComponent = () => { }} > diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index dcf120d37ef8b..3453165a15f69 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -15,7 +15,7 @@ import { TypeRegistry } from './application/type_registry'; import { ManagementStart, ManagementSectionId } from '../../../../src/plugins/management/public'; import { boot } from './application/boot'; import { ChartsPluginStart } from '../../../../src/plugins/charts/public'; -import { PluginStartContract as AlertingStart } from '../../alerting/public'; +import { PluginStartContract as AlertingStart } from '../../alerts/public'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; export interface TriggersAndActionsUIPublicPluginSetup { @@ -32,7 +32,7 @@ interface PluginsStart { data: DataPublicPluginStart; charts: ChartsPluginStart; management: ManagementStart; - alerting?: AlertingStart; + alerts?: AlertingStart; navigateToApp: CoreStart['application']['navigateToApp']; } @@ -83,7 +83,7 @@ export class Plugin boot({ dataPlugin: plugins.data, charts: plugins.charts, - alerting: plugins.alerting, + alerts: plugins.alerts, element: params.element, toastNotifications: core.notifications.toasts, http: core.http, diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 11152c56c49ec..52179dd35767c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -5,7 +5,7 @@ */ import { HttpSetup, DocLinksStart } from 'kibana/public'; import { ComponentType } from 'react'; -import { ActionGroup } from '../../alerting/common'; +import { ActionGroup } from '../../alerts/common'; import { ActionType } from '../../actions/common'; import { TypeRegistry } from './application/type_registry'; import { @@ -14,7 +14,7 @@ import { AlertTaskState, RawAlertInstance, AlertingFrameworkHealth, -} from '../../../plugins/alerting/common'; +} from '../../alerts/common'; export { Alert, AlertAction, AlertTaskState, RawAlertInstance, AlertingFrameworkHealth }; export { ActionType }; diff --git a/x-pack/plugins/uptime/kibana.json b/x-pack/plugins/uptime/kibana.json index ce8b64ce07254..5fbd6129fd18f 100644 --- a/x-pack/plugins/uptime/kibana.json +++ b/x-pack/plugins/uptime/kibana.json @@ -4,7 +4,7 @@ "kibanaVersion": "kibana", "optionalPlugins": ["capabilities", "data", "home"], "requiredPlugins": [ - "alerting", + "alerts", "embeddable", "features", "licensing", diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts index f4d1c72770494..5ffc71945caef 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts @@ -36,7 +36,7 @@ export interface UptimeCoreSetup { export interface UptimeCorePlugins { features: PluginSetupContract; - alerting: any; + alerts: any; elasticsearch: any; usageCollection: UsageCollectionSetup; } diff --git a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts index 73d104c1d21ae..8c487c85c5720 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts @@ -11,12 +11,12 @@ import { fullListByIdAndLocation, } from '../status_check'; import { GetMonitorStatusResult } from '../../requests'; -import { AlertType } from '../../../../../alerting/server'; +import { AlertType } from '../../../../../alerts/server'; import { IRouter } from 'kibana/server'; import { UMServerLibs } from '../../lib'; import { UptimeCoreSetup } from '../../adapters'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; -import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; +import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; /** * The alert takes some dependencies as parameters; these are things like @@ -39,7 +39,7 @@ const bootstrapDependencies = (customRequests?: any) => { * This function aims to provide an easy way to give mock props that will * reduce boilerplate for tests. * @param params the params received at alert creation time - * @param services the core services provided by kibana/alerting platforms + * @param services the core services provided by kibana/alerts platforms * @param state the state the alert maintains */ const mockOptions = ( diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index 17479bb451b18..3dd1558f5da91 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { isRight } from 'fp-ts/lib/Either'; import { ThrowReporter } from 'io-ts/lib/ThrowReporter'; import { i18n } from '@kbn/i18n'; -import { AlertExecutorOptions } from '../../../../alerting/server'; +import { AlertExecutorOptions } from '../../../../alerts/server'; import { UptimeAlertTypeFactory } from './types'; import { GetMonitorStatusResult } from '../requests'; import { StatusCheckExecutorParamsType } from '../../../common/runtime_types'; diff --git a/x-pack/plugins/uptime/server/lib/alerts/types.ts b/x-pack/plugins/uptime/server/lib/alerts/types.ts index bc1e82224f7b0..a321cc124ac22 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/types.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertType } from '../../../../alerting/server'; +import { AlertType } from '../../../../alerts/server'; import { UptimeCoreSetup } from '../adapters'; import { UMServerLibs } from '../lib'; diff --git a/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts b/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts index 9f153e186420d..53b1fe881cd92 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts @@ -15,6 +15,7 @@ import { SortOrder, } from '../../../../common/runtime_types'; import { MonitorEnricher } from './fetch_page'; +import { getHistogramInterval } from '../../helper/get_histogram_interval'; export const enrichMonitorGroups: MonitorEnricher = async ( queryContext: QueryContext, @@ -317,11 +318,13 @@ const getHistogramForMonitors = async ( }, aggs: { histogram: { - auto_date_histogram: { + date_histogram: { field: '@timestamp', // 12 seems to be a good size for performance given // long monitor lists of up to 100 on the overview page - buckets: 12, + fixed_interval: + getHistogramInterval(queryContext.dateRangeStart, queryContext.dateRangeEnd, 12) + + 'ms', missing: 0, }, aggs: { diff --git a/x-pack/plugins/uptime/server/uptime_server.ts b/x-pack/plugins/uptime/server/uptime_server.ts index 180067c0abde2..fb90dfe2be6c5 100644 --- a/x-pack/plugins/uptime/server/uptime_server.ts +++ b/x-pack/plugins/uptime/server/uptime_server.ts @@ -19,6 +19,6 @@ export const initUptimeServer = ( ); uptimeAlertTypeFactories.forEach((alertTypeFactory) => - plugins.alerting.registerType(alertTypeFactory(server, libs)) + plugins.alerts.registerType(alertTypeFactory(server, libs)) ); }; diff --git a/x-pack/test/accessibility/apps/home.ts b/x-pack/test/accessibility/apps/home.ts index 1f05ff676e3a0..fe698acec322a 100644 --- a/x-pack/test/accessibility/apps/home.ts +++ b/x-pack/test/accessibility/apps/home.ts @@ -12,7 +12,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const globalNav = getService('globalNav'); - describe('Kibana Home', () => { + // FLAKY: https://github.com/elastic/kibana/issues/66976 + describe.skip('Kibana Home', () => { before(async () => { await PageObjects.common.navigateToApp('home'); }); diff --git a/x-pack/test/accessibility/apps/search_profiler.ts b/x-pack/test/accessibility/apps/search_profiler.ts index 8a13940695f9e..138231d3cf025 100644 --- a/x-pack/test/accessibility/apps/search_profiler.ts +++ b/x-pack/test/accessibility/apps/search_profiler.ts @@ -14,7 +14,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); const flyout = getService('flyout'); - describe('Accessibility Search Profiler Editor', () => { + // FLAKY: https://github.com/elastic/kibana/issues/67821 + describe.skip('Accessibility Search Profiler Editor', () => { before(async () => { await PageObjects.common.navigateToApp('searchProfiler'); await a11y.testAppSnapshot(); diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/kibana.json b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/kibana.json index 98c57db16c914..fc42e3199095d 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/kibana.json +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/kibana.json @@ -3,7 +3,7 @@ "version": "1.0.0", "kibanaVersion": "kibana", "configPath": ["xpack"], - "requiredPlugins": ["taskManager", "features", "actions", "alerting"], + "requiredPlugins": ["taskManager", "features", "actions", "alerts"], "optionalPlugins": ["spaces"], "server": true, "ui": false diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts index a921dac7d43a1..3b6befb3fe807 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/action_types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup } from 'kibana/server'; +import { CoreSetup } from 'src/core/server'; import { schema } from '@kbn/config-schema'; import { FixtureStartDeps, FixtureSetupDeps } from './plugin'; import { ActionType, ActionTypeExecutorOptions } from '../../../../../../../plugins/actions/server'; @@ -13,7 +13,7 @@ export function defineActionTypes( core: CoreSetup, { actions }: Pick ) { - const clusterClient = core.elasticsearch.adminClient; + const clusterClient = core.elasticsearch.legacy.client; // Action types const noopActionType: ActionType = { diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts index ff3a3e48d5b1a..8e3d6b6909a14 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts @@ -4,17 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup } from 'kibana/server'; +import { CoreSetup } from 'src/core/server'; import { schema } from '@kbn/config-schema'; import { times } from 'lodash'; import { FixtureStartDeps, FixtureSetupDeps } from './plugin'; -import { AlertType, AlertExecutorOptions } from '../../../../../../../plugins/alerting/server'; +import { AlertType, AlertExecutorOptions } from '../../../../../../../plugins/alerts/server'; export function defineAlertTypes( core: CoreSetup, - { alerting }: Pick + { alerts }: Pick ) { - const clusterClient = core.elasticsearch.adminClient; + const clusterClient = core.elasticsearch.legacy.client; const alwaysFiringAlertType: AlertType = { id: 'test.always-firing', name: 'Test: Always Firing', @@ -286,13 +286,13 @@ export function defineAlertTypes( }, async executor(opts: AlertExecutorOptions) {}, }; - alerting.registerType(alwaysFiringAlertType); - alerting.registerType(cumulativeFiringAlertType); - alerting.registerType(neverFiringAlertType); - alerting.registerType(failingAlertType); - alerting.registerType(validationAlertType); - alerting.registerType(authorizationAlertType); - alerting.registerType(noopAlertType); - alerting.registerType(onlyContextVariablesAlertType); - alerting.registerType(onlyStateVariablesAlertType); + alerts.registerType(alwaysFiringAlertType); + alerts.registerType(cumulativeFiringAlertType); + alerts.registerType(neverFiringAlertType); + alerts.registerType(failingAlertType); + alerts.registerType(validationAlertType); + alerts.registerType(authorizationAlertType); + alerts.registerType(noopAlertType); + alerts.registerType(onlyContextVariablesAlertType); + alerts.registerType(onlyStateVariablesAlertType); } diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts index af8dd0282c578..47563f8a5f078 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts @@ -6,7 +6,7 @@ import { Plugin, CoreSetup } from 'kibana/server'; import { PluginSetupContract as ActionsPluginSetup } from '../../../../../../../plugins/actions/server/plugin'; -import { PluginSetupContract as AlertingPluginSetup } from '../../../../../../../plugins/alerting/server/plugin'; +import { PluginSetupContract as AlertingPluginSetup } from '../../../../../../../plugins/alerts/server/plugin'; import { EncryptedSavedObjectsPluginStart } from '../../../../../../../plugins/encrypted_saved_objects/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../../plugins/features/server'; import { defineAlertTypes } from './alert_types'; @@ -16,7 +16,7 @@ import { defineRoutes } from './routes'; export interface FixtureSetupDeps { features: FeaturesPluginSetup; actions: ActionsPluginSetup; - alerting: AlertingPluginSetup; + alerts: AlertingPluginSetup; } export interface FixtureStartDeps { @@ -24,17 +24,14 @@ export interface FixtureStartDeps { } export class FixturePlugin implements Plugin { - public setup( - core: CoreSetup, - { features, actions, alerting }: FixtureSetupDeps - ) { + public setup(core: CoreSetup, { features, actions, alerts }: FixtureSetupDeps) { features.registerFeature({ - id: 'alerting', - name: 'Alerting', - app: ['alerting', 'kibana'], + id: 'alerts', + name: 'Alerts', + app: ['alerts', 'kibana'], privileges: { all: { - app: ['alerting', 'kibana'], + app: ['alerts', 'kibana'], savedObject: { all: ['alert'], read: [], @@ -43,7 +40,7 @@ export class FixturePlugin implements Plugin { - const pluginPath = plugin ? `/${plugin}` : ''; return this.supertest - .delete(`${getUrlPrefix(spaceId)}/api${pluginPath}/${type}/${id}`) + .delete(`${getUrlPrefix(spaceId)}/api/${plugin}/${type}/${id}`) .set('kbn-xsrf', 'foo') .expect(204); }) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts b/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts index d58fcd29e29fc..c72ee6a192bf2 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts @@ -47,7 +47,7 @@ const GlobalRead: User = { kibana: [ { feature: { - alerting: ['read'], + alerts: ['read'], actions: ['read'], }, spaces: ['*'], @@ -75,7 +75,7 @@ const Space1All: User = { kibana: [ { feature: { - alerting: ['all'], + alerts: ['all'], actions: ['all'], }, spaces: ['space1'], diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts index 785285f6d455c..45491aa2d28fc 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts @@ -121,7 +121,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { objectRemover.add(space.id, createdAction.id, 'action', 'actions'); const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send( getTestAlertData({ @@ -142,7 +142,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { }) ) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await supertestWithoutAuth .get(`${getUrlPrefix(space.id)}/api/actions`) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts index 02cd661cbaf04..ab58a205f9d47 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts @@ -336,7 +336,7 @@ instanceStateValue: true const reference = alertUtils.generateReference(); const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( @@ -374,7 +374,7 @@ instanceStateValue: true case 'superuser at space1': case 'space_1_all at space1': expect(response.statusCode).to.eql(200); - objectRemover.add(space.id, response.body.id, 'alert', undefined); + objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); // Wait for the task to be attempted once and idle const scheduledActionTask = await retry.try(async () => { @@ -428,7 +428,7 @@ instanceStateValue: true const testStart = new Date(); const reference = alertUtils.generateReference(); const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( @@ -457,7 +457,7 @@ instanceStateValue: true break; case 'space_1_all at space1': expect(response.statusCode).to.eql(200); - objectRemover.add(space.id, response.body.id, 'alert', undefined); + objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); // Wait for test.authorization to index a document before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('alert:test.authorization', reference); @@ -490,7 +490,7 @@ instanceStateValue: true break; case 'superuser at space1': expect(response.statusCode).to.eql(200); - objectRemover.add(space.id, response.body.id, 'alert', undefined); + objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); // Wait for test.authorization to index a document before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('alert:test.authorization', reference); @@ -532,7 +532,7 @@ instanceStateValue: true .expect(200); objectRemover.add(space.id, createdAction.id, 'action', 'actions'); const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( @@ -571,7 +571,7 @@ instanceStateValue: true break; case 'space_1_all at space1': expect(response.statusCode).to.eql(200); - objectRemover.add(space.id, response.body.id, 'alert', undefined); + objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); // Ensure test.authorization indexed 1 document before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('action:test.authorization', reference); @@ -604,7 +604,7 @@ instanceStateValue: true break; case 'superuser at space1': expect(response.statusCode).to.eql(200); - objectRemover.add(space.id, response.body.id, 'alert', undefined); + objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); // Ensure test.authorization indexed 1 document before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('action:test.authorization', reference); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts index ad9fd117c3604..4ca943f3e188a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts @@ -43,7 +43,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .expect(200); const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( @@ -72,7 +72,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { case 'superuser at space1': case 'space_1_all at space1': expect(response.statusCode).to.eql(200); - objectRemover.add(space.id, response.body.id, 'alert', undefined); + objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); expect(response.body).to.eql({ id: response.body.id, name: 'abc', @@ -126,7 +126,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { it('should handle create alert request appropriately when an alert is disabled ', async () => { const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send(getTestAlertData({ enabled: false })); @@ -145,7 +145,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { case 'superuser at space1': case 'space_1_all at space1': expect(response.statusCode).to.eql(200); - objectRemover.add(space.id, response.body.id, 'alert', undefined); + objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); expect(response.body.scheduledTaskId).to.eql(undefined); break; default: @@ -155,7 +155,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { it('should handle create alert request appropriately when alert type is unregistered', async () => { const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( @@ -191,7 +191,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { it('should handle create alert request appropriately when payload is empty and invalid', async () => { const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send({}); @@ -223,7 +223,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { it(`should handle create alert request appropriately when params isn't valid`, async () => { const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( @@ -260,7 +260,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { it('should handle create alert request appropriately when interval schedule is wrong syntax', async () => { const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send(getTestAlertData(getTestAlertData({ schedule: { interval: '10x' } }))); @@ -292,7 +292,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { it('should handle create alert request appropriately when interval schedule is 0', async () => { const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send(getTestAlertData(getTestAlertData({ schedule: { interval: '0s' } }))); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts index 593ae574e6f34..6f8ae010b9cd8 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts @@ -32,13 +32,13 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { describe(scenario.id, () => { it('should handle delete alert request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); const response = await supertestWithoutAuth - .delete(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .delete(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password); @@ -52,7 +52,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { error: 'Not Found', message: 'Not Found', }); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); // Ensure task still exists await getScheduledTask(createdAlert.scheduledTaskId); break; @@ -74,14 +74,14 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { it(`shouldn't delete alert from another space`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await supertestWithoutAuth - .delete(`${getUrlPrefix('other')}/api/alert/${createdAlert.id}`) + .delete(`${getUrlPrefix('other')}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password); @@ -111,7 +111,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { it('should still be able to delete alert when AAD is broken', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); @@ -129,7 +129,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { .expect(200); const response = await supertestWithoutAuth - .delete(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .delete(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password); @@ -143,7 +143,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { error: 'Not Found', message: 'Not Found', }); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); // Ensure task still exists await getScheduledTask(createdAlert.scheduledTaskId); break; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts index dbbccba70a715..589942a7ac11c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts @@ -40,11 +40,11 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte describe(scenario.id, () => { it('should handle disable alert request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: true })) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await alertUtils.getDisableRequest(createdAlert.id); @@ -86,11 +86,11 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte it('should still be able to disable alert when AAD is broken', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: true })) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); await supertest .put( @@ -144,11 +144,11 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte it(`shouldn't disable alert from another space`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix('other')}/api/alert`) + .post(`${getUrlPrefix('other')}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: true })) .expect(200); - objectRemover.add('other', createdAlert.id, 'alert', undefined); + objectRemover.add('other', createdAlert.id, 'alert', 'alerts'); const response = await alertUtils.getDisableRequest(createdAlert.id); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts index 611556aaf1fef..8cb0eeb0092a3 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts @@ -40,11 +40,11 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex describe(scenario.id, () => { it('should handle enable alert request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await alertUtils.getEnableRequest(createdAlert.id); @@ -64,7 +64,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); @@ -91,11 +91,11 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex it('should still be able to enable alert when AAD is broken', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); await supertest .put( @@ -127,7 +127,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); @@ -154,11 +154,11 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex it(`shouldn't enable alert from another space`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix('other')}/api/alert`) + .post(`${getUrlPrefix('other')}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add('other', createdAlert.id, 'alert', undefined); + objectRemover.add('other', createdAlert.id, 'alert', 'alerts'); const response = await alertUtils.getEnableRequest(createdAlert.id); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts index 1c4d684eb78de..5fe9edeb91ec9 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts @@ -24,15 +24,17 @@ export default function createFindTests({ getService }: FtrProviderContext) { describe(scenario.id, () => { it('should handle find alert request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await supertestWithoutAuth .get( - `${getUrlPrefix(space.id)}/api/alert/_find?search=test.noop&search_fields=alertTypeId` + `${getUrlPrefix( + space.id + )}/api/alerts/_find?search=test.noop&search_fields=alertTypeId` ) .auth(user.username, user.password); @@ -95,7 +97,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { .expect(200); const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send( getTestAlertData({ @@ -110,13 +112,13 @@ export default function createFindTests({ getService }: FtrProviderContext) { }) ) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await supertestWithoutAuth .get( `${getUrlPrefix( space.id - )}/api/alert/_find?filter=alert.attributes.actions:{ actionTypeId: test.noop }` + )}/api/alerts/_find?filter=alert.attributes.actions:{ actionTypeId: test.noop }` ) .auth(user.username, user.password); @@ -174,15 +176,15 @@ export default function createFindTests({ getService }: FtrProviderContext) { it(`shouldn't find alert from another space`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await supertestWithoutAuth .get( - `${getUrlPrefix('other')}/api/alert/_find?search=test.noop&search_fields=alertTypeId` + `${getUrlPrefix('other')}/api/alerts/_find?search=test.noop&search_fields=alertTypeId` ) .auth(user.username, user.password); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts index 5800273dce75d..a203b7d7c151b 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts @@ -24,14 +24,14 @@ export default function createGetTests({ getService }: FtrProviderContext) { describe(scenario.id, () => { it('should handle get alert request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .auth(user.username, user.password); switch (scenario.id) { @@ -78,14 +78,14 @@ export default function createGetTests({ getService }: FtrProviderContext) { it(`shouldn't get alert from another space`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await supertestWithoutAuth - .get(`${getUrlPrefix('other')}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix('other')}/api/alerts/alert/${createdAlert.id}`) .auth(user.username, user.password); expect(response.statusCode).to.eql(404); @@ -114,7 +114,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { it(`should handle get alert request appropriately when alert doesn't exist`, async () => { const response = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/1`) + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/1`) .auth(user.username, user.password); switch (scenario.id) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts index 42a6b36df0f97..fd071bd55b377 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts @@ -24,14 +24,14 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont describe(scenario.id, () => { it('should handle getAlertState alert request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}/state`) + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}/state`) .auth(user.username, user.password); switch (scenario.id) { @@ -57,14 +57,14 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont it(`shouldn't getAlertState for an alert from another space`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await supertestWithoutAuth - .get(`${getUrlPrefix('other')}/api/alert/${createdAlert.id}/state`) + .get(`${getUrlPrefix('other')}/api/alerts/alert/${createdAlert.id}/state`) .auth(user.username, user.password); expect(response.statusCode).to.eql(404); @@ -93,7 +93,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont it(`should handle getAlertState request appropriately when alert doesn't exist`, async () => { const response = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/1/state`) + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/1/state`) .auth(user.username, user.password); switch (scenario.id) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts index 91b0ca0a37c92..f14f66f66fe2f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function alertingTests({ loadTestFile }: FtrProviderContext) { - describe('Alerting', () => { + describe('Alerts', () => { loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./disable')); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts index 4f6b26dfb67fa..dd31e2dbbb5b8 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts @@ -19,7 +19,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { describe(scenario.id, () => { it('should return 200 with list of alert types', async () => { const response = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/types`) + .get(`${getUrlPrefix(space.id)}/api/alerts/list_alert_types`) .auth(user.username, user.password); switch (scenario.id) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts index 0196615629e23..2416bc2ea1d12 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts @@ -32,11 +32,11 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) describe(scenario.id, () => { it('should handle mute alert request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await alertUtils.getMuteAllRequest(createdAlert.id); @@ -56,7 +56,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts index 0c05dbdf55842..c59b9f4503a03 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts @@ -32,11 +32,11 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider describe(scenario.id, () => { it('should handle mute alert instance request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await alertUtils.getMuteInstanceRequest(createdAlert.id, '1'); @@ -56,7 +56,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); @@ -76,14 +76,16 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider it('should handle mute alert instance request appropriately and not duplicate mutedInstanceIds when muting an instance already muted', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); await supertest - .post(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}/alert_instance/1/_mute`) + .post( + `${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}/alert_instance/1/_mute` + ) .set('kbn-xsrf', 'foo') .expect(204, ''); @@ -105,7 +107,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts index ebe9f1f645ed7..fd22752ccc11a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts @@ -32,14 +32,14 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex describe(scenario.id, () => { it('should handle unmute alert request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); await supertest - .post(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}/_mute_all`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}/_mute_all`) .set('kbn-xsrf', 'foo') .expect(204, ''); @@ -61,7 +61,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts index 7142fd7d91adf..72b524282354a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts @@ -32,14 +32,16 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider describe(scenario.id, () => { it('should handle unmute alert instance request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); await supertest - .post(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}/alert_instance/1/_mute`) + .post( + `${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}/alert_instance/1/_mute` + ) .set('kbn-xsrf', 'foo') .expect(204, ''); @@ -61,7 +63,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts index 0af1e22584643..2bcc035beb7a9 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts @@ -39,11 +39,11 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { describe(scenario.id, () => { it('should handle update alert request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const updatedData = { name: 'bcd', @@ -56,7 +56,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { throttle: '1m', }; const response = await supertestWithoutAuth - .put(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send(updatedData); @@ -110,11 +110,11 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { it('should still be able to update when AAD is broken', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); await supertest .put( @@ -139,7 +139,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { throttle: '1m', }; const response = await supertestWithoutAuth - .put(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send(updatedData); @@ -193,14 +193,14 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { it(`shouldn't update alert from another space`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await supertestWithoutAuth - .put(`${getUrlPrefix('other')}/api/alert/${createdAlert.id}`) + .put(`${getUrlPrefix('other')}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send({ @@ -240,14 +240,14 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { it('should handle update alert request appropriately when attempting to change alert type', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await supertestWithoutAuth - .put(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send({ @@ -289,7 +289,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { it('should handle update alert request appropriately when payload is empty and invalid', async () => { const response = await supertestWithoutAuth - .put(`${getUrlPrefix(space.id)}/api/alert/1`) + .put(`${getUrlPrefix(space.id)}/api/alerts/alert/1`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send({}); @@ -321,7 +321,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { it(`should handle update alert request appropriately when alertTypeConfig isn't valid`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send( getTestAlertData({ @@ -332,10 +332,10 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { }) ) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await supertestWithoutAuth - .put(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send({ @@ -375,7 +375,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { it('should handle update alert request appropriately when interval schedule is wrong syntax', async () => { const response = await supertestWithoutAuth - .put(`${getUrlPrefix(space.id)}/api/alert/1`) + .put(`${getUrlPrefix(space.id)}/api/alerts/alert/1`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( @@ -413,7 +413,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { it('should handle updates to an alert schedule by rescheduling the underlying task', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send( getTestAlertData({ @@ -421,7 +421,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { }) ) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); await retry.try(async () => { const alertTask = (await getAlertingTaskById(createdAlert.scheduledTaskId)).docs[0]; @@ -441,7 +441,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { throttle: '1m', }; const response = await supertestWithoutAuth - .put(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send(updatedData); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts index 6349919c15cd2..bf72b970dc0f1 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts @@ -32,11 +32,11 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte describe(scenario.id, () => { it('should handle update alert api key request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await alertUtils.getUpdateApiKeyRequest(createdAlert.id); @@ -56,7 +56,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); @@ -76,11 +76,11 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte it('should still be able to update API key when AAD is broken', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert', undefined); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); await supertest .put( @@ -112,7 +112,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); @@ -132,11 +132,11 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte it(`shouldn't update alert api key from another space`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix('other')}/api/alert`) + .post(`${getUrlPrefix('other')}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add('other', createdAlert.id, 'alert', undefined); + objectRemover.add('other', createdAlert.id, 'alert', 'alerts'); const response = await alertUtils.getUpdateApiKeyRequest(createdAlert.id); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts index d3c914942bd90..8ffe65a8ebb48 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts @@ -186,7 +186,7 @@ instanceStateValue: true const reference = alertUtils.generateReference(); const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send( getTestAlertData({ @@ -211,7 +211,7 @@ instanceStateValue: true ); expect(response.statusCode).to.eql(200); - objectRemover.add(space.id, response.body.id, 'alert', undefined); + objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); const scheduledActionTask = await retry.try(async () => { const searchResult = await es.search({ index: '.kibana_task_manager', @@ -255,7 +255,7 @@ instanceStateValue: true it('should have proper callCluster and savedObjectsClient authorization for alert type executor', async () => { const reference = alertUtils.generateReference(); const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send( getTestAlertData({ @@ -271,7 +271,7 @@ instanceStateValue: true ); expect(response.statusCode).to.eql(200); - objectRemover.add(space.id, response.body.id, 'alert', undefined); + objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); const alertTestRecord = ( await esTestIndexTool.waitForDocs('alert:test.authorization', reference) )[0]; @@ -301,7 +301,7 @@ instanceStateValue: true .expect(200); objectRemover.add(space.id, createdAction.id, 'action', 'actions'); const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send( getTestAlertData({ @@ -327,7 +327,7 @@ instanceStateValue: true ); expect(response.statusCode).to.eql(200); - objectRemover.add(space.id, response.body.id, 'alert', undefined); + objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); const actionTestRecord = ( await esTestIndexTool.waitForDocs('action:test.authorization', reference) )[0]; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts index 353f7d02f6b0b..8412c09eefcda 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts @@ -342,7 +342,7 @@ export default function alertTests({ getService }: FtrProviderContext) { }; const { status, body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send({ name: params.name, @@ -372,7 +372,7 @@ export default function alertTests({ getService }: FtrProviderContext) { expect(status).to.be(200); const alertId = createdAlert.id; - objectRemover.add(Spaces.space1.id, alertId, 'alert', undefined); + objectRemover.add(Spaces.space1.id, alertId, 'alert', 'alerts'); return alertId; } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts index b10c356cf40d5..fa256712a012b 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts @@ -39,7 +39,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .expect(200); const response = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send( getTestAlertData({ @@ -54,7 +54,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { ); expect(response.status).to.eql(200); - objectRemover.add(Spaces.space1.id, response.body.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, response.body.id, 'alert', 'alerts'); expect(response.body).to.eql({ id: response.body.id, name: 'abc', @@ -104,12 +104,12 @@ export default function createAlertTests({ getService }: FtrProviderContext) { it('should handle create alert request appropriately when an alert is disabled ', async () => { const response = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })); expect(response.status).to.eql(200); - objectRemover.add(Spaces.space1.id, response.body.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, response.body.id, 'alert', 'alerts'); expect(response.body.scheduledTaskId).to.eql(undefined); }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/delete.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/delete.ts index 3aea982f948ea..e9dfe1607d32d 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/delete.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/delete.ts @@ -28,13 +28,13 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { it('should handle delete alert request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); await supertest - .delete(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}`) + .delete(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .expect(204, ''); @@ -48,13 +48,13 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { it(`shouldn't delete alert from another space`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); await supertest - .delete(`${getUrlPrefix(Spaces.other.id)}/api/alert/${createdAlert.id}`) + .delete(`${getUrlPrefix(Spaces.other.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .expect(404, { statusCode: 404, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts index 7152a76fa167f..afa4f03e23b30 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts @@ -35,11 +35,11 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte it('should handle disable alert request appropriately', async () => { const { body: createdAlert } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: true })) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); await alertUtils.disable(createdAlert.id); @@ -61,11 +61,11 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte it(`shouldn't disable alert from another space`, async () => { const { body: createdAlert } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.other.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.other.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: true })) .expect(200); - objectRemover.add(Spaces.other.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.other.id, createdAlert.id, 'alert', 'alerts'); await alertUtils.getDisableRequest(createdAlert.id).expect(404, { statusCode: 404, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts index 3d556d0936022..05b212bb064f3 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts @@ -35,16 +35,16 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex it('should handle enable alert request appropriately', async () => { const { body: createdAlert } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); await alertUtils.enable(createdAlert.id); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .expect(200); expect(typeof updatedAlert.scheduledTaskId).to.eql('string'); @@ -67,11 +67,11 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex it(`shouldn't enable alert from another space`, async () => { const { body: createdAlert } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.other.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.other.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(Spaces.other.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.other.id, createdAlert.id, 'alert', 'alerts'); await alertUtils.getEnableRequest(createdAlert.id).expect(404, { statusCode: 404, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts index f57b136b9637a..06f27d666c3da 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts @@ -20,16 +20,16 @@ export default function createFindTests({ getService }: FtrProviderContext) { it('should handle find alert request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); const response = await supertest.get( `${getUrlPrefix( Spaces.space1.id - )}/api/alert/_find?search=test.noop&search_fields=alertTypeId` + )}/api/alerts/_find?search=test.noop&search_fields=alertTypeId` ); expect(response.status).to.eql(200); @@ -63,17 +63,17 @@ export default function createFindTests({ getService }: FtrProviderContext) { it(`shouldn't find alert from another space`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); await supertest .get( `${getUrlPrefix( Spaces.other.id - )}/api/alert/_find?search=test.noop&search_fields=alertTypeId` + )}/api/alerts/_find?search=test.noop&search_fields=alertTypeId` ) .expect(200, { page: 1, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts index 6b216d2ba265f..ff671e16654b5 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts @@ -20,14 +20,14 @@ export default function createGetTests({ getService }: FtrProviderContext) { it('should handle get alert request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}` + `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}` ); expect(response.status).to.eql(200); @@ -57,14 +57,14 @@ export default function createGetTests({ getService }: FtrProviderContext) { it(`shouldn't find alert from another space`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); await supertest - .get(`${getUrlPrefix(Spaces.other.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(Spaces.other.id)}/api/alerts/alert/${createdAlert.id}`) .expect(404, { statusCode: 404, error: 'Not Found', @@ -73,7 +73,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { }); it(`should handle get alert request appropriately when alert doesn't exist`, async () => { - await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/1`).expect(404, { + await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/1`).expect(404, { statusCode: 404, error: 'Not Found', message: 'Saved object [alert/1] not found', diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts index 06f5f5542780c..d3f08d7c509a0 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts @@ -21,14 +21,14 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont it('should handle getAlertState request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}/state` + `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}/state` ); expect(response.status).to.eql(200); @@ -37,7 +37,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont it('should fetch updated state', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send({ enabled: true, @@ -51,12 +51,12 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont params: {}, }) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); // wait for alert to actually execute await retry.try(async () => { const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}/state` + `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}/state` ); expect(response.status).to.eql(200); @@ -65,7 +65,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont }); const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}/state` + `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}/state` ); expect(response.body.alertTypeState.runCount).to.greaterThan(0); @@ -79,11 +79,13 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont }); it(`should handle getAlertState request appropriately when alert doesn't exist`, async () => { - await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/1/state`).expect(404, { - statusCode: 404, - error: 'Not Found', - message: 'Saved object [alert/1] not found', - }); + await supertest + .get(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/1/state`) + .expect(404, { + statusCode: 404, + error: 'Not Found', + message: 'Saved object [alert/1] not found', + }); }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts index 845a6f7955739..aef87eefba2ad 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts @@ -15,7 +15,9 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { describe('list_alert_types', () => { it('should return 200 with list of alert types', async () => { - const response = await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/types`); + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/api/alerts/list_alert_types` + ); expect(response.status).to.eql(200); const fixtureAlertType = response.body.find((alertType: any) => alertType.id === 'test.noop'); expect(fixtureAlertType).to.eql({ @@ -32,7 +34,9 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { }); it('should return actionVariables with both context and state', async () => { - const response = await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/types`); + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/api/alerts/list_alert_types` + ); expect(response.status).to.eql(200); const fixtureAlertType = response.body.find( @@ -46,7 +50,9 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { }); it('should return actionVariables with just context', async () => { - const response = await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/types`); + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/api/alerts/list_alert_types` + ); expect(response.status).to.eql(200); const fixtureAlertType = response.body.find( @@ -60,7 +66,9 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { }); it('should return actionVariables with just state', async () => { - const response = await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/types`); + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/api/alerts/list_alert_types` + ); expect(response.status).to.eql(200); const fixtureAlertType = response.body.find( diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts index b2ba38ac98470..f881d0c677bb5 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts @@ -27,16 +27,16 @@ export default function createMuteTests({ getService }: FtrProviderContext) { it('should handle mute alert request appropriately', async () => { const { body: createdAlert } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); await alertUtils.muteAll(createdAlert.id); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .expect(200); expect(updatedAlert.muteAll).to.eql(true); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_instance.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_instance.ts index d9f52d3321e32..ca0d72aedf7a1 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_instance.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_instance.ts @@ -27,16 +27,16 @@ export default function createMuteInstanceTests({ getService }: FtrProviderConte it('should handle mute alert instance request appropriately', async () => { const { body: createdAlert } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); await alertUtils.muteInstance(createdAlert.id, '1'); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .expect(200); expect(updatedAlert.mutedInstanceIds).to.eql(['1']); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts index 7c5f1e0a62130..1df99540903d0 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts @@ -27,17 +27,17 @@ export default function createUnmuteTests({ getService }: FtrProviderContext) { it('should handle unmute alert request appropriately', async () => { const { body: createdAlert } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); await alertUtils.muteAll(createdAlert.id); await alertUtils.unmuteAll(createdAlert.id); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .expect(200); expect(updatedAlert.muteAll).to.eql(false); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_instance.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_instance.ts index 86464c3d6bb64..332842ce8015f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_instance.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_instance.ts @@ -27,17 +27,17 @@ export default function createUnmuteInstanceTests({ getService }: FtrProviderCon it('should handle unmute alert instance request appropriately', async () => { const { body: createdAlert } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); await alertUtils.muteInstance(createdAlert.id, '1'); await alertUtils.unmuteInstance(createdAlert.id, '1'); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .expect(200); expect(updatedAlert.mutedInstanceIds).to.eql([]); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts index fc0aeb71d9066..b01a1b140f2d6 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts @@ -20,11 +20,11 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { it('should handle update alert request appropriately', async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); const updatedData = { name: 'bcd', @@ -37,7 +37,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { throttle: '1m', }; const response = await supertest - .put(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}`) + .put(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .send(updatedData) .expect(200); @@ -75,14 +75,14 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { it(`shouldn't update alert from another space`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); await supertest - .put(`${getUrlPrefix(Spaces.other.id)}/api/alert/${createdAlert.id}`) + .put(`${getUrlPrefix(Spaces.other.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .send({ name: 'bcd', diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update_api_key.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update_api_key.ts index 9c7b4dcc8b1a3..93f91bdc73150 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update_api_key.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update_api_key.ts @@ -31,16 +31,16 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte it('should handle update alert api key appropriately', async () => { const { body: createdAlert } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts'); await alertUtils.updateApiKey(createdAlert.id); const { body: updatedAlert } = await supertestWithoutAuth - .get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}`) + .get(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .expect(200); expect(updatedAlert.apiKeyOwner).to.eql(null); @@ -56,11 +56,11 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte it(`shouldn't update alert api key from another space`, async () => { const { body: createdAlert } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.other.id)}/api/alert`) + .post(`${getUrlPrefix(Spaces.other.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(Spaces.other.id, createdAlert.id, 'alert', undefined); + objectRemover.add(Spaces.other.id, createdAlert.id, 'alert', 'alerts'); await alertUtils.getUpdateApiKeyRequest(createdAlert.id).expect(404, { statusCode: 404, diff --git a/x-pack/test/api_integration/apis/endpoint/alerts/index.ts b/x-pack/test/api_integration/apis/endpoint/alerts/index.ts index 155513aefc609..ecdee09ce7edf 100644 --- a/x-pack/test/api_integration/apis/endpoint/alerts/index.ts +++ b/x-pack/test/api_integration/apis/endpoint/alerts/index.ts @@ -70,7 +70,7 @@ export default function ({ getService }: FtrProviderContext) { let nullableEventId = ''; - describe('Endpoint alert API', () => { + describe.skip('Endpoint alert API', () => { describe('when data is in elasticsearch', () => { before(async () => { await esArchiver.load('endpoint/alerts/api_feature'); diff --git a/x-pack/test/api_integration/apis/endpoint/alerts/index_pattern.ts b/x-pack/test/api_integration/apis/endpoint/alerts/index_pattern.ts index e87b063453054..df1cbcfe28e7b 100644 --- a/x-pack/test/api_integration/apis/endpoint/alerts/index_pattern.ts +++ b/x-pack/test/api_integration/apis/endpoint/alerts/index_pattern.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - describe('Endpoint index pattern API', () => { + describe.skip('Endpoint index pattern API', () => { it('should retrieve the index pattern for events', async () => { const { body } = await supertest.get('/api/endpoint/index_pattern/events').expect(200); expect(body.indexPattern).to.eql('events-endpoint-*'); diff --git a/x-pack/test/api_integration/apis/endpoint/metadata.ts b/x-pack/test/api_integration/apis/endpoint/metadata.ts index 5c4bb52b8d9e2..c01919f60a922 100644 --- a/x-pack/test/api_integration/apis/endpoint/metadata.ts +++ b/x-pack/test/api_integration/apis/endpoint/metadata.ts @@ -14,7 +14,7 @@ const numberOfHostsInFixture = 3; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); - describe('test metadata api', () => { + describe.skip('test metadata api', () => { describe('POST /api/endpoint/metadata when index is empty', () => { it('metadata api should return empty result when index is empty', async () => { await esArchiver.unload('endpoint/metadata/api_feature'); diff --git a/x-pack/test/api_integration/apis/management/index_management/component_templates.ts b/x-pack/test/api_integration/apis/management/index_management/component_templates.ts new file mode 100644 index 0000000000000..a33e82ad9f79d --- /dev/null +++ b/x-pack/test/api_integration/apis/management/index_management/component_templates.ts @@ -0,0 +1,296 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +// @ts-ignore +import { initElasticsearchHelpers } from './lib'; +// @ts-ignore +import { API_BASE_PATH } from './constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + const { createComponentTemplate, deleteComponentTemplate } = initElasticsearchHelpers(es); + + describe('Component templates', function () { + describe('Get', () => { + const COMPONENT_NAME = 'test_component_template'; + const COMPONENT = { + template: { + settings: { + index: { + number_of_shards: 1, + }, + }, + mappings: { + _source: { + enabled: false, + }, + properties: { + host_name: { + type: 'keyword', + }, + created_at: { + type: 'date', + format: 'EEE MMM dd HH:mm:ss Z yyyy', + }, + }, + }, + }, + }; + + before(() => createComponentTemplate({ body: COMPONENT, name: COMPONENT_NAME })); + after(() => deleteComponentTemplate(COMPONENT_NAME)); + + describe('all component templates', () => { + it('should return an array of component templates', async () => { + const { body: componentTemplates } = await supertest + .get(`${API_BASE_PATH}/component_templates`) + .set('kbn-xsrf', 'xxx') + .expect(200); + + const testComponentTemplate = componentTemplates.find( + ({ name }: { name: string }) => name === COMPONENT_NAME + ); + + expect(testComponentTemplate).to.eql({ + name: COMPONENT_NAME, + component_template: COMPONENT, + }); + }); + }); + + describe('one component template', () => { + it('should return a single component template', async () => { + const uri = `${API_BASE_PATH}/component_templates/${COMPONENT_NAME}`; + + const { body } = await supertest.get(uri).set('kbn-xsrf', 'xxx').expect(200); + + expect(body).to.eql({ + name: COMPONENT_NAME, + component_template: { + ...COMPONENT, + }, + }); + }); + }); + }); + + describe('Create', () => { + const COMPONENT_NAME = 'test_create_component_template'; + const REQUIRED_FIELDS_COMPONENT_NAME = 'test_create_required_fields_component_template'; + + after(() => { + deleteComponentTemplate(COMPONENT_NAME); + deleteComponentTemplate(REQUIRED_FIELDS_COMPONENT_NAME); + }); + + it('should create a component template', async () => { + const { body } = await supertest + .post(`${API_BASE_PATH}/component_templates`) + .set('kbn-xsrf', 'xxx') + .send({ + name: COMPONENT_NAME, + version: 1, + template: { + settings: { + number_of_shards: 1, + }, + aliases: { + alias1: {}, + }, + mappings: { + properties: { + host_name: { + type: 'keyword', + }, + }, + }, + }, + _meta: { + description: 'set number of shards to one', + serialization: { + class: 'MyComponentTemplate', + id: 10, + }, + }, + }) + .expect(200); + + expect(body).to.eql({ + acknowledged: true, + }); + }); + + it('should create a component template with only required fields', async () => { + const { body } = await supertest + .post(`${API_BASE_PATH}/component_templates`) + .set('kbn-xsrf', 'xxx') + // Excludes version and _meta fields + .send({ + name: REQUIRED_FIELDS_COMPONENT_NAME, + template: {}, + }) + .expect(200); + + expect(body).to.eql({ + acknowledged: true, + }); + }); + + it('should not allow creation of a component template with the same name of an existing one', async () => { + const { body } = await supertest + .post(`${API_BASE_PATH}/component_templates`) + .set('kbn-xsrf', 'xxx') + .send({ + name: COMPONENT_NAME, + template: {}, + }) + .expect(409); + + expect(body).to.eql({ + statusCode: 409, + error: 'Conflict', + message: `There is already a component template with name '${COMPONENT_NAME}'.`, + }); + }); + }); + + describe('Update', () => { + const COMPONENT_NAME = 'test_component_template'; + const COMPONENT = { + template: { + settings: { + index: { + number_of_shards: 1, + }, + }, + mappings: { + _source: { + enabled: false, + }, + properties: { + host_name: { + type: 'keyword', + }, + created_at: { + type: 'date', + format: 'EEE MMM dd HH:mm:ss Z yyyy', + }, + }, + }, + }, + }; + + before(() => createComponentTemplate({ body: COMPONENT, name: COMPONENT_NAME })); + after(() => deleteComponentTemplate(COMPONENT_NAME)); + + it('should allow an existing component template to be updated', async () => { + const uri = `${API_BASE_PATH}/component_templates/${COMPONENT_NAME}`; + + const { body } = await supertest + .put(uri) + .set('kbn-xsrf', 'xxx') + .send({ + ...COMPONENT, + version: 1, + }) + .expect(200); + + expect(body).to.eql({ + acknowledged: true, + }); + }); + + it('should not allow a non-existing component template to be updated', async () => { + const uri = `${API_BASE_PATH}/component_templates/component_does_not_exist`; + + const { body } = await supertest + .put(uri) + .set('kbn-xsrf', 'xxx') + .send({ + ...COMPONENT, + version: 1, + }) + .expect(404); + + expect(body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: + '[resource_not_found_exception] component template matching [component_does_not_exist] not found', + }); + }); + }); + + describe('Delete', () => { + const COMPONENT = { + template: { + settings: { + index: { + number_of_shards: 1, + }, + }, + }, + }; + + it('should delete a component template', async () => { + // Create component template to be deleted + const COMPONENT_NAME = 'test_delete_component_template'; + createComponentTemplate({ body: COMPONENT, name: COMPONENT_NAME }); + + const uri = `${API_BASE_PATH}/component_templates/${COMPONENT_NAME}`; + + const { body } = await supertest.delete(uri).set('kbn-xsrf', 'xxx').expect(200); + + expect(body).to.eql({ + itemsDeleted: [COMPONENT_NAME], + errors: [], + }); + }); + + it('should delete multiple component templates', async () => { + // Create component templates to be deleted + const COMPONENT_ONE_NAME = 'test_delete_component_1'; + const COMPONENT_TWO_NAME = 'test_delete_component_2'; + createComponentTemplate({ body: COMPONENT, name: COMPONENT_ONE_NAME }); + createComponentTemplate({ body: COMPONENT, name: COMPONENT_TWO_NAME }); + + const uri = `${API_BASE_PATH}/component_templates/${COMPONENT_ONE_NAME},${COMPONENT_TWO_NAME}`; + + const { + body: { itemsDeleted, errors }, + } = await supertest.delete(uri).set('kbn-xsrf', 'xxx').expect(200); + + expect(errors).to.eql([]); + + // The itemsDeleted array order isn't guaranteed, so we assert against each name instead + [COMPONENT_ONE_NAME, COMPONENT_TWO_NAME].forEach((componentName) => { + expect(itemsDeleted.includes(componentName)).to.be(true); + }); + }); + + it('should return an error for any component templates not sucessfully deleted', async () => { + const COMPONENT_DOES_NOT_EXIST = 'component_does_not_exist'; + + // Create component template to be deleted + const COMPONENT_ONE_NAME = 'test_delete_component_1'; + createComponentTemplate({ body: COMPONENT, name: COMPONENT_ONE_NAME }); + + const uri = `${API_BASE_PATH}/component_templates/${COMPONENT_ONE_NAME},${COMPONENT_DOES_NOT_EXIST}`; + + const { body } = await supertest.delete(uri).set('kbn-xsrf', 'xxx').expect(200); + + expect(body.itemsDeleted).to.eql([COMPONENT_ONE_NAME]); + expect(body.errors[0].name).to.eql(COMPONENT_DOES_NOT_EXIST); + expect(body.errors[0].error.msg).to.contain('index_template_missing_exception'); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/management/index_management/index.js b/x-pack/test/api_integration/apis/management/index_management/index.js index cd3e27f9f7a61..fdee325938ff4 100644 --- a/x-pack/test/api_integration/apis/management/index_management/index.js +++ b/x-pack/test/api_integration/apis/management/index_management/index.js @@ -11,5 +11,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./settings')); loadTestFile(require.resolve('./stats')); loadTestFile(require.resolve('./templates')); + loadTestFile(require.resolve('./component_templates')); }); } diff --git a/x-pack/test/api_integration/apis/management/index_management/lib/elasticsearch.js b/x-pack/test/api_integration/apis/management/index_management/lib/elasticsearch.js index 78aed8142eeba..b950a56a913db 100644 --- a/x-pack/test/api_integration/apis/management/index_management/lib/elasticsearch.js +++ b/x-pack/test/api_integration/apis/management/index_management/lib/elasticsearch.js @@ -34,6 +34,14 @@ export const initElasticsearchHelpers = (es) => { const catTemplate = (name) => es.cat.templates({ name, format: 'json' }); + const createComponentTemplate = (componentTemplate) => { + return es.dataManagement.saveComponentTemplate(componentTemplate); + }; + + const deleteComponentTemplate = (componentTemplateName) => { + return es.dataManagement.deleteComponentTemplate({ name: componentTemplateName }); + }; + return { createIndex, deleteIndex, @@ -42,5 +50,7 @@ export const initElasticsearchHelpers = (es) => { indexStats, cleanUp, catTemplate, + createComponentTemplate, + deleteComponentTemplate, }; }; diff --git a/x-pack/test/api_integration/apis/ml/data_frame_analytics/delete.ts b/x-pack/test/api_integration/apis/ml/data_frame_analytics/delete.ts new file mode 100644 index 0000000000000..23bff0d0c2855 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/data_frame_analytics/delete.ts @@ -0,0 +1,218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { DataFrameAnalyticsConfig } from '../../../../../plugins/ml/public/application/data_frame_analytics/common'; +import { DeepPartial } from '../../../../../plugins/ml/common/types/common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const jobId = `bm_${Date.now()}`; + const generateDestinationIndex = (analyticsId: string) => `user-${analyticsId}`; + const commonJobConfig = { + source: { + index: ['ft_bank_marketing'], + query: { + match_all: {}, + }, + }, + analysis: { + classification: { + dependent_variable: 'y', + training_percent: 20, + }, + }, + analyzed_fields: { + includes: [], + excludes: [], + }, + model_memory_limit: '350mb', + }; + + const testJobConfigs: Array> = [ + 'Test delete job only', + 'Test delete job and target index', + 'Test delete job and index pattern', + 'Test delete job, target index, and index pattern', + ].map((description, idx) => { + const analyticsId = `${jobId}_${idx + 1}`; + return { + id: analyticsId, + description, + dest: { + index: generateDestinationIndex(analyticsId), + results_field: 'ml', + }, + ...commonJobConfig, + }; + }); + + async function createJobs(mockJobConfigs: Array>) { + for (const jobConfig of mockJobConfigs) { + await ml.api.createDataFrameAnalyticsJob(jobConfig as DataFrameAnalyticsConfig); + } + } + + describe('DELETE data_frame/analytics', () => { + before(async () => { + await esArchiver.loadIfNeeded('ml/bm_classification'); + await ml.testResources.setKibanaTimeZoneToUTC(); + await createJobs(testJobConfigs); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + describe('DeleteDataFrameAnalytics', () => { + it('should delete analytics jobs by id', async () => { + const analyticsId = `${jobId}_1`; + const { body } = await supertest + .delete(`/api/ml/data_frame/analytics/${analyticsId}`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.analyticsJobDeleted.success).to.eql(true); + await ml.api.waitForDataFrameAnalyticsJobNotToExist(analyticsId); + }); + + it('should not allow to retrieve analytics jobs for unauthorized user', async () => { + const analyticsId = `${jobId}_2`; + const { body } = await supertest + .delete(`/api/ml/data_frame/analytics/${analyticsId}`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + await ml.api.waitForDataFrameAnalyticsJobToExist(analyticsId); + }); + + it('should not allow to retrieve analytics jobs for the user with only view permission', async () => { + const analyticsId = `${jobId}_2`; + const { body } = await supertest + .delete(`/api/ml/data_frame/analytics/${analyticsId}`) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + await ml.api.waitForDataFrameAnalyticsJobToExist(analyticsId); + }); + + it('should show 404 error if job does not exist or has already been deleted', async () => { + const { body } = await supertest + .delete(`/api/ml/data_frame/analytics/${jobId}_invalid`) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + }); + + describe('with deleteDestIndex setting', function () { + const analyticsId = `${jobId}_2`; + const destinationIndex = generateDestinationIndex(analyticsId); + + before(async () => { + await ml.api.createIndices(destinationIndex); + await ml.api.assertIndicesExist(destinationIndex); + }); + + after(async () => { + await ml.api.deleteIndices(destinationIndex); + }); + + it('should delete job and destination index by id', async () => { + const { body } = await supertest + .delete(`/api/ml/data_frame/analytics/${analyticsId}`) + .query({ deleteDestIndex: true }) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.analyticsJobDeleted.success).to.eql(true); + expect(body.destIndexDeleted.success).to.eql(true); + expect(body.destIndexPatternDeleted.success).to.eql(false); + await ml.api.waitForDataFrameAnalyticsJobNotToExist(analyticsId); + await ml.api.assertIndicesNotToExist(destinationIndex); + }); + }); + + describe('with deleteDestIndexPattern setting', function () { + const analyticsId = `${jobId}_3`; + const destinationIndex = generateDestinationIndex(analyticsId); + + before(async () => { + // Mimic real job by creating index pattern after job is created + await ml.testResources.createIndexPatternIfNeeded(destinationIndex); + }); + + after(async () => { + await ml.testResources.deleteIndexPattern(destinationIndex); + }); + + it('should delete job and index pattern by id', async () => { + const { body } = await supertest + .delete(`/api/ml/data_frame/analytics/${analyticsId}`) + .query({ deleteDestIndexPattern: true }) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.analyticsJobDeleted.success).to.eql(true); + expect(body.destIndexDeleted.success).to.eql(false); + expect(body.destIndexPatternDeleted.success).to.eql(true); + await ml.api.waitForDataFrameAnalyticsJobNotToExist(analyticsId); + await ml.testResources.assertIndexPatternNotExist(destinationIndex); + }); + }); + + describe('with deleteDestIndex & deleteDestIndexPattern setting', function () { + const analyticsId = `${jobId}_4`; + const destinationIndex = generateDestinationIndex(analyticsId); + + before(async () => { + // Mimic real job by creating target index & index pattern after DFA job is created + await ml.api.createIndices(destinationIndex); + await ml.api.assertIndicesExist(destinationIndex); + await ml.testResources.createIndexPatternIfNeeded(destinationIndex); + }); + + after(async () => { + await ml.api.deleteIndices(destinationIndex); + await ml.testResources.deleteIndexPattern(destinationIndex); + }); + + it('deletes job, target index, and index pattern by id', async () => { + const { body } = await supertest + .delete(`/api/ml/data_frame/analytics/${analyticsId}`) + .query({ deleteDestIndex: true, deleteDestIndexPattern: true }) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + expect(body.analyticsJobDeleted.success).to.eql(true); + expect(body.destIndexDeleted.success).to.eql(true); + expect(body.destIndexPatternDeleted.success).to.eql(true); + await ml.api.waitForDataFrameAnalyticsJobNotToExist(analyticsId); + await ml.api.assertIndicesNotToExist(destinationIndex); + await ml.testResources.assertIndexPatternNotExist(destinationIndex); + }); + }); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts b/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts index 9e0f952ad501b..6693561076fdd 100644 --- a/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts +++ b/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts @@ -9,5 +9,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('data frame analytics', function () { loadTestFile(require.resolve('./get')); + loadTestFile(require.resolve('./delete')); }); } diff --git a/x-pack/test/api_integration/services/legacy_es.js b/x-pack/test/api_integration/services/legacy_es.js index 12a1576f78982..0ea061365aca2 100644 --- a/x-pack/test/api_integration/services/legacy_es.js +++ b/x-pack/test/api_integration/services/legacy_es.js @@ -8,7 +8,8 @@ import { format as formatUrl } from 'url'; import * as legacyElasticsearch from 'elasticsearch'; -import { elasticsearchClientPlugin } from '../../../plugins/security/server/elasticsearch_client_plugin'; +import { elasticsearchClientPlugin as securityEsClientPlugin } from '../../../plugins/security/server/elasticsearch_client_plugin'; +import { elasticsearchJsPlugin as indexManagementEsClientPlugin } from '../../../plugins/index_management/server/client/elasticsearch'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { DEFAULT_API_VERSION } from '../../../../src/core/server/elasticsearch/elasticsearch_config'; @@ -19,6 +20,6 @@ export function LegacyEsProvider({ getService }) { apiVersion: DEFAULT_API_VERSION, host: formatUrl(config.get('servers.elasticsearch')), requestTimeout: config.get('timeouts.esRequestTimeout'), - plugins: [elasticsearchClientPlugin], + plugins: [securityEsClientPlugin, indexManagementEsClientPlugin], }); } diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts index 9eb62c2fe07b0..45b34b7d26940 100644 --- a/x-pack/test/case_api_integration/common/config.ts +++ b/x-pack/test/case_api_integration/common/config.ts @@ -78,7 +78,6 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) 'some.non.existent.com', ])}`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, - '--xpack.alerting.enabled=true', '--xpack.eventLog.logEntries=true', ...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`), `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`, 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/api.ts b/x-pack/test/functional/services/ml/api.ts index 897f37821001e..fc2ce4bb16b99 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -9,9 +9,9 @@ import { DataFrameAnalyticsConfig } from '../../../../plugins/ml/public/applicat import { FtrProviderContext } from '../../ftr_provider_context'; -import { JOB_STATE, DATAFEED_STATE } from '../../../../plugins/ml/common/constants/states'; +import { DATAFEED_STATE, JOB_STATE } from '../../../../plugins/ml/common/constants/states'; import { DATA_FRAME_TASK_STATE } from '../../../../plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common'; -import { Job, Datafeed } from '../../../../plugins/ml/common/types/anomaly_detection_jobs'; +import { Datafeed, Job } from '../../../../plugins/ml/common/types/anomaly_detection_jobs'; export type MlApi = ProvidedType; @@ -110,6 +110,21 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { ); }, + async createIndices(indices: string) { + log.debug(`Creating indices: '${indices}'...`); + if ((await es.indices.exists({ index: indices, allowNoIndices: false })) === true) { + log.debug(`Indices '${indices}' already exist. Nothing to create.`); + return; + } + + const createResponse = await es.indices.create({ index: indices }); + expect(createResponse) + .to.have.property('acknowledged') + .eql(true, 'Response for create request indices should be acknowledged.'); + + await this.assertIndicesExist(indices); + }, + async deleteIndices(indices: string) { log.debug(`Deleting indices: '${indices}'...`); if ((await es.indices.exists({ index: indices, allowNoIndices: false })) === false) { @@ -122,15 +137,9 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { }); expect(deleteResponse) .to.have.property('acknowledged') - .eql(true, 'Response for delete request should be acknowledged'); + .eql(true, 'Response for delete request should be acknowledged.'); - await retry.waitForWithTimeout(`'${indices}' indices to be deleted`, 30 * 1000, async () => { - if ((await es.indices.exists({ index: indices, allowNoIndices: false })) === false) { - return true; - } else { - throw new Error(`expected indices '${indices}' to be deleted`); - } - }); + await this.assertIndicesNotToExist(indices); }, async cleanMlIndices() { @@ -251,6 +260,16 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { }); }, + async assertIndicesNotToExist(indices: string) { + await retry.tryForTime(30 * 1000, async () => { + if ((await es.indices.exists({ index: indices, allowNoIndices: false })) === false) { + return true; + } else { + throw new Error(`indices '${indices}' should not exist`); + } + }); + }, + async assertIndicesNotEmpty(indices: string) { await retry.tryForTime(30 * 1000, async () => { const response = await es.search({ @@ -394,9 +413,9 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { await this.waitForJobState(jobConfig.job_id, JOB_STATE.CLOSED); }, - async getDataFrameAnalyticsJob(analyticsId: string) { + async getDataFrameAnalyticsJob(analyticsId: string, statusCode = 200) { log.debug(`Fetching data frame analytics job '${analyticsId}'...`); - return await esSupertest.get(`/_ml/data_frame/analytics/${analyticsId}`).expect(200); + return await esSupertest.get(`/_ml/data_frame/analytics/${analyticsId}`).expect(statusCode); }, async waitForDataFrameAnalyticsJobToExist(analyticsId: string) { @@ -409,6 +428,16 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { }); }, + async waitForDataFrameAnalyticsJobNotToExist(analyticsId: string) { + await retry.waitForWithTimeout(`'${analyticsId}' not to exist`, 5 * 1000, async () => { + if (await this.getDataFrameAnalyticsJob(analyticsId, 404)) { + return true; + } else { + throw new Error(`expected data frame analytics job '${analyticsId}' not to exist`); + } + }); + }, + async createDataFrameAnalyticsJob(jobConfig: DataFrameAnalyticsConfig) { const { id: analyticsId, ...analyticsConfig } = jobConfig; log.debug(`Creating data frame analytic job with id '${analyticsId}'...`); diff --git a/x-pack/test/functional/services/ml/test_resources.ts b/x-pack/test/functional/services/ml/test_resources.ts index d349416ec90f7..739fd844f1193 100644 --- a/x-pack/test/functional/services/ml/test_resources.ts +++ b/x-pack/test/functional/services/ml/test_resources.ts @@ -5,7 +5,6 @@ */ import { ProvidedType } from '@kbn/test/types/ftr'; - import { savedSearches } from './test_resources_data'; import { COMMON_REQUEST_HEADERS } from './common'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -24,6 +23,7 @@ export function MachineLearningTestResourcesProvider({ getService }: FtrProvider const kibanaServer = getService('kibanaServer'); const log = getService('log'); const supertest = getService('supertest'); + const retry = getService('retry'); return { async setKibanaTimeZoneToUTC() { @@ -98,6 +98,21 @@ export function MachineLearningTestResourcesProvider({ getService }: FtrProvider } }, + async assertIndexPatternNotExist(title: string) { + await retry.waitForWithTimeout( + `index pattern '${title}' to not exist`, + 5 * 1000, + async () => { + const indexPatternId = await this.getIndexPatternId(title); + if (!indexPatternId) { + return true; + } else { + throw new Error(`Index pattern '${title}' should not exist.`); + } + } + ); + }, + async createSavedSearch(title: string, body: object): Promise { log.debug(`Creating saved search with title '${title}'`); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts index 89ce3742adf64..13bf47676cc09 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts @@ -21,7 +21,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { async function createAlert(overwrites: Record = {}) { const { body: createdAlert } = await supertest - .post(`/api/alert`) + .post(`/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send({ enabled: true, diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts index d78053cf926dc..6cb74aff95be2 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts @@ -89,7 +89,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // put the fetch code in a retry block with a timeout. let alert: any; await retry.tryForTime(15000, async () => { - const apiResponse = await supertest.get('/api/alert/_find?search=uptime-test'); + const apiResponse = await supertest.get('/api/alerts/_find?search=uptime-test'); const alertsFromThisTest = apiResponse.body.data.filter( ({ name }: { name: string }) => name === 'uptime-test' ); @@ -129,7 +129,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { '"minimum_should_match":1}},{"bool":{"should":[{"match":{"monitor.type":"http"}}],"minimum_should_match":1}}]}}]}}]}}' ); } finally { - await supertest.delete(`/api/alert/${id}`).set('kbn-xsrf', 'true').expect(204); + await supertest.delete(`/api/alerts/alert/${id}`).set('kbn-xsrf', 'true').expect(204); } }); }); @@ -176,7 +176,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('has created a valid alert with expected parameters', async () => { let alert: any; await retry.tryForTime(15000, async () => { - const apiResponse = await supertest.get(`/api/alert/_find?search=${alertId}`); + const apiResponse = await supertest.get(`/api/alerts/_find?search=${alertId}`); const alertsFromThisTest = apiResponse.body.data.filter( ({ name }: { name: string }) => name === alertId ); @@ -204,7 +204,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(params).to.eql({}); expect(interval).to.eql('11m'); } finally { - await supertest.delete(`/api/alert/${id}`).set('kbn-xsrf', 'true').expect(204); + await supertest.delete(`/api/alerts/alert/${id}`).set('kbn-xsrf', 'true').expect(204); } }); }); diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/kibana.json b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/kibana.json index 1715f30b82260..74f740f52a8b2 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/kibana.json +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/kibana.json @@ -3,7 +3,7 @@ "version": "1.0.0", "kibanaVersion": "kibana", "configPath": ["xpack"], - "requiredPlugins": ["alerting", "triggers_actions_ui"], + "requiredPlugins": ["alerts", "triggers_actions_ui"], "server": true, "ui": true } diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/plugin.ts index 4c68a3aa15b30..2bc299ede930b 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/plugin.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/plugin.ts @@ -6,21 +6,21 @@ import React from 'react'; import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; -import { PluginSetupContract as AlertingSetup } from '../../../../../../plugins/alerting/public'; -import { AlertType, SanitizedAlert } from '../../../../../../plugins/alerting/common'; +import { PluginSetupContract as AlertingSetup } from '../../../../../../plugins/alerts/public'; +import { AlertType, SanitizedAlert } from '../../../../../../plugins/alerts/common'; import { TriggersAndActionsUIPublicPluginSetup } from '../../../../../../plugins/triggers_actions_ui/public'; export type Setup = void; export type Start = void; export interface AlertingExamplePublicSetupDeps { - alerting: AlertingSetup; + alerts: AlertingSetup; triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; } export class AlertingFixturePlugin implements Plugin { - public setup(core: CoreSetup, { alerting, triggers_actions_ui }: AlertingExamplePublicSetupDeps) { - alerting.registerNavigation( + public setup(core: CoreSetup, { alerts, triggers_actions_ui }: AlertingExamplePublicSetupDeps) { + alerts.registerNavigation( 'consumer-noop', 'test.noop', (alert: SanitizedAlert, alertType: AlertType) => `/alert/${alert.id}` diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts index 123c0c550e71e..fb431351a382d 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts @@ -8,24 +8,24 @@ import { Plugin, CoreSetup } from 'kibana/server'; import { PluginSetupContract as AlertingSetup, AlertType, -} from '../../../../../../plugins/alerting/server'; +} from '../../../../../../plugins/alerts/server'; // this plugin's dependendencies export interface AlertingExampleDeps { - alerting: AlertingSetup; + alerts: AlertingSetup; } export class AlertingFixturePlugin implements Plugin { - public setup(core: CoreSetup, { alerting }: AlertingExampleDeps) { - createNoopAlertType(alerting); - createAlwaysFiringAlertType(alerting); + public setup(core: CoreSetup, { alerts }: AlertingExampleDeps) { + createNoopAlertType(alerts); + createAlwaysFiringAlertType(alerts); } public start() {} public stop() {} } -function createNoopAlertType(alerting: AlertingSetup) { +function createNoopAlertType(alerts: AlertingSetup) { const noopAlertType: AlertType = { id: 'test.noop', name: 'Test: Noop', @@ -34,10 +34,10 @@ function createNoopAlertType(alerting: AlertingSetup) { async executor() {}, producer: 'alerting', }; - alerting.registerType(noopAlertType); + alerts.registerType(noopAlertType); } -function createAlwaysFiringAlertType(alerting: AlertingSetup) { +function createAlwaysFiringAlertType(alerts: AlertingSetup) { // Alert types const alwaysFiringAlertType: any = { id: 'test.always-firing', @@ -63,5 +63,5 @@ function createAlwaysFiringAlertType(alerting: AlertingSetup) { }; }, }; - alerting.registerType(alwaysFiringAlertType); + alerts.registerType(alwaysFiringAlertType); } diff --git a/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts index 2a0d28f246765..25f4c6a932d5e 100644 --- a/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts @@ -38,7 +38,7 @@ export class Alerts { ) { this.log.debug(`creating alert ${name}`); - const { data: alert, status, statusText } = await this.axios.post(`/api/alert`, { + const { data: alert, status, statusText } = await this.axios.post(`/api/alerts/alert`, { enabled: true, name, tags, @@ -63,7 +63,7 @@ export class Alerts { public async createNoOp(name: string) { this.log.debug(`creating alert ${name}`); - const { data: alert, status, statusText } = await this.axios.post(`/api/alert`, { + const { data: alert, status, statusText } = await this.axios.post(`/api/alerts/alert`, { enabled: true, name, tags: ['foo'], @@ -96,7 +96,7 @@ export class Alerts { ) { this.log.debug(`creating alert ${name}`); - const { data: alert, status, statusText } = await this.axios.post(`/api/alert`, { + const { data: alert, status, statusText } = await this.axios.post(`/api/alerts/alert`, { enabled: true, name, tags: ['foo'], @@ -132,7 +132,7 @@ export class Alerts { public async deleteAlert(id: string) { this.log.debug(`deleting alert ${id}`); - const { data: alert, status, statusText } = await this.axios.delete(`/api/alert/${id}`); + const { data: alert, status, statusText } = await this.axios.delete(`/api/alerts/alert/${id}`); if (status !== 204) { throw new Error( `Expected status code of 204, received ${status} ${statusText}: ${util.inspect(alert)}` @@ -144,7 +144,7 @@ export class Alerts { public async getAlertState(id: string) { this.log.debug(`getting alert ${id} state`); - const { data } = await this.axios.get(`/api/alert/${id}/state`); + const { data } = await this.axios.get(`/api/alerts/alert/${id}/state`); return data; } @@ -152,7 +152,7 @@ export class Alerts { this.log.debug(`muting instance ${instanceId} under alert ${id}`); const { data: alert, status, statusText } = await this.axios.post( - `/api/alert/${id}/alert_instance/${instanceId}/_mute` + `/api/alerts/alert/${id}/alert_instance/${instanceId}/_mute` ); if (status !== 204) { throw new Error( diff --git a/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/plugin.ts b/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/plugin.ts index b36d6dca077f7..af410d457fc05 100644 --- a/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/plugin.ts +++ b/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/plugin.ts @@ -38,9 +38,9 @@ export class FeatureUsageTestPlugin }: CoreSetup, { licensing }: FeatureUsageTestSetupDependencies ) { - licensing.featureUsage.register('test_feature_a'); - licensing.featureUsage.register('test_feature_b'); - licensing.featureUsage.register('test_feature_c'); + licensing.featureUsage.register('Test feature A', 'basic'); + licensing.featureUsage.register('Test feature B', 'gold'); + licensing.featureUsage.register('Test feature C', 'platinum'); registerRoutes(http.createRouter(), getStartServices); diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts index e4c4d13ee4a41..f35d6baac8f5a 100644 --- a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts @@ -11,7 +11,7 @@ import { IKibanaResponse, IRouter, CoreSetup, -} from 'kibana/server'; +} from 'src/core/server'; import { EventEmitter } from 'events'; import { TaskManagerStartContract } from '../../../../../plugins/task_manager/server'; @@ -39,7 +39,7 @@ export function initRoutes( taskTestingEvents: EventEmitter ) { async function ensureIndexIsRefreshed() { - return await core.elasticsearch.adminClient.callAsInternalUser('indices.refresh', { + return await core.elasticsearch.legacy.client.callAsInternalUser('indices.refresh', { index: '.kibana_task_manager', }); } diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts index ae756bb56b921..3ea669ae9d404 100644 --- a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Plugin, CoreSetup, CoreStart } from 'kibana/server'; +import { Plugin, CoreSetup, CoreStart } from 'src/core/server'; import { EventEmitter } from 'events'; import { Subject } from 'rxjs'; import { first } from 'rxjs/operators'; @@ -64,7 +64,7 @@ export class SampleTaskManagerFixturePlugin } } - await core.elasticsearch.adminClient.callAsInternalUser('index', { + await core.elasticsearch.legacy.client.callAsInternalUser('index', { index: '.kibana_task_manager_test_result', body: { type: 'task', diff --git a/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/feature_usage.ts b/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/feature_usage.ts index dfbc41d883e02..5c8fac9586e3a 100644 --- a/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/feature_usage.ts +++ b/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/feature_usage.ts @@ -20,15 +20,32 @@ export default function ({ getService }: FtrProviderContext) { describe('/api/licensing/feature_usage', () => { it('returns a map of last feature usages', async () => { const timeA = Date.now(); - await notifyUsage('test_feature_a', timeA); + await notifyUsage('Test feature C', timeA); const timeB = Date.now() - 4567; - await notifyUsage('test_feature_b', timeB); + await notifyUsage('Test feature B', timeB); const response = await supertest.get('/api/licensing/feature_usage').expect(200); - expect(response.body.test_feature_a).to.eql(toISO(timeA)); - expect(response.body.test_feature_b).to.eql(toISO(timeB)); + expect(response.body).to.eql({ + features: [ + { + last_used: null, + license_level: 'basic', + name: 'Test feature A', + }, + { + last_used: toISO(timeB), + license_level: 'gold', + name: 'Test feature B', + }, + { + last_used: toISO(timeA), + license_level: 'platinum', + name: 'Test feature C', + }, + ], + }); }); }); } diff --git a/x-pack/test/reporting_api_integration/reporting/csv_job_params.ts b/x-pack/test/reporting_api_integration/reporting/csv_job_params.ts index 7d11403add136..90f97d44da224 100644 --- a/x-pack/test/reporting_api_integration/reporting/csv_job_params.ts +++ b/x-pack/test/reporting_api_integration/reporting/csv_job_params.ts @@ -46,7 +46,7 @@ export default function ({ getService }: FtrProviderContext) { jobParams: 0, })) as supertest.Response; - expect(resText).to.match(/\\\"jobParams\\\" must be a string/); + expect(resText).to.match(/expected value of type \[string\] but got \[number\]/); expect(resStatus).to.eql(400); }); diff --git a/x-pack/test/siem_cypress/es_archives/custom_rule_with_timeline/data.json.gz b/x-pack/test/siem_cypress/es_archives/custom_rule_with_timeline/data.json.gz new file mode 100644 index 0000000000000..3d50451cee39f Binary files /dev/null and b/x-pack/test/siem_cypress/es_archives/custom_rule_with_timeline/data.json.gz differ diff --git a/x-pack/test/siem_cypress/es_archives/custom_rule_with_timeline/mappings.json b/x-pack/test/siem_cypress/es_archives/custom_rule_with_timeline/mappings.json new file mode 100644 index 0000000000000..d01e6344bcfaf --- /dev/null +++ b/x-pack/test/siem_cypress/es_archives/custom_rule_with_timeline/mappings.json @@ -0,0 +1,7983 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "7b44fba6773e37c806ce290ea9b7024e", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3525d7c22c42bc80f5e6e9cb3f2b26a2", + "application_usage_totals": "c897e4310c5f24b07caaff3db53ae2c1", + "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "cases": "32aa96a6d3855ddda53010ae2048ac22", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "ae24d22d5986d04124cc6568f771066f", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "epm-packages": "92b4b1899b887b090d01c033f3118a85", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "fleet-agent-actions": "e520c855577170c24481be05c3ae14ec", + "fleet-agent-events": "3231653fafe4ef3196fe3b32ab774bf2", + "fleet-agents": "864760267df6c970f629bd4458506c53", + "fleet-enrollment-api-keys": "28b91e20b105b6f928e2012600085d8f", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", + "ingest-agent-configs": "d9a5cbdce8e937f674a7b376c47a34a1", + "ingest-datasources": "c0fe6347b0eebcbf421841669e3acd31", + "ingest-outputs": "0e57221778a7153c8292edf154099036", + "ingest_manager_settings": "c5b0749b4ab03c582efd4c14cb8f132c", + "inventory-view": "88fc7e12fd1b45b6f0787323ce4f18d2", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "21c3ea0763beb1ecb0162529706b88c5", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "23d7aa4a720d4938ccde3983f87bd58d", + "maps-telemetry": "bfd39d88aadadb4be597ea984d433dbe", + "metrics-explorer-view": "428e319af3e822c80a84cf87123ca35c", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "181661168bbadd1eff5902361e2a0d5c", + "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "17ec409954864e592ceec0c5eae29ad9", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "296a89039fc4260292be36b1b005d8f2", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "fcdb453a30092f022f2642db29523d80", + "url": "b675c3be8d76ecf029294d51dc7ec65d", + "visualization": "52d7a13ad68a150c4525b292d23e12cc" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "agent_actions": { + "properties": { + "agent_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "data": { + "type": "flattened" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "agent_configs": { + "properties": { + "datasources": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "text" + }, + "namespace": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "updated_on": { + "type": "keyword" + } + } + }, + "agent_events": { + "properties": { + "action_id": { + "type": "keyword" + }, + "agent_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "data": { + "type": "text" + }, + "message": { + "type": "text" + }, + "payload": { + "type": "text" + }, + "stream_id": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "active": { + "type": "boolean" + }, + "config_id": { + "type": "keyword" + }, + "config_newest_revision": { + "type": "integer" + }, + "config_revision": { + "type": "integer" + }, + "current_error_events": { + "type": "text" + }, + "default_api_key": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "text" + }, + "shared_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "text" + }, + "version": { + "type": "keyword" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "apm-indices": { + "properties": { + "apm_oss": { + "properties": { + "errorIndices": { + "type": "keyword" + }, + "metricsIndices": { + "type": "keyword" + }, + "onboardingIndices": { + "type": "keyword" + }, + "sourcemapIndices": { + "type": "keyword" + }, + "spanIndices": { + "type": "keyword" + }, + "transactionIndices": { + "type": "keyword" + } + } + } + } + }, + "apm-telemetry": { + "properties": { + "agents": { + "properties": { + "dotnet": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "go": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "java": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "js-base": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "nodejs": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "python": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "ruby": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "rum-js": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + } + } + }, + "cardinality": { + "properties": { + "transaction": { + "properties": { + "name": { + "properties": { + "all_agents": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "rum": { + "properties": { + "1d": { + "type": "long" + } + } + } + } + } + } + }, + "user_agent": { + "properties": { + "original": { + "properties": { + "all_agents": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "rum": { + "properties": { + "1d": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "counts": { + "properties": { + "agent_configuration": { + "properties": { + "all": { + "type": "long" + } + } + }, + "error": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "max_error_groups_per_service": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "max_transaction_groups_per_service": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "onboarding": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "services": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "sourcemap": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "span": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "traces": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "transaction": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + } + } + }, + "has_any_services": { + "type": "boolean" + }, + "indices": { + "properties": { + "all": { + "properties": { + "total": { + "properties": { + "docs": { + "properties": { + "count": { + "type": "long" + } + } + }, + "store": { + "properties": { + "size_in_bytes": { + "type": "long" + } + } + } + } + } + } + }, + "shards": { + "properties": { + "total": { + "type": "long" + } + } + } + } + }, + "integrations": { + "properties": { + "ml": { + "properties": { + "all_jobs_count": { + "type": "long" + } + } + } + } + }, + "retainment": { + "properties": { + "error": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "onboarding": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "span": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "transaction": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "services_per_agent": { + "properties": { + "dotnet": { + "null_value": 0, + "type": "long" + }, + "go": { + "null_value": 0, + "type": "long" + }, + "java": { + "null_value": 0, + "type": "long" + }, + "js-base": { + "null_value": 0, + "type": "long" + }, + "nodejs": { + "null_value": 0, + "type": "long" + }, + "python": { + "null_value": 0, + "type": "long" + }, + "ruby": { + "null_value": 0, + "type": "long" + }, + "rum-js": { + "null_value": 0, + "type": "long" + } + } + }, + "tasks": { + "properties": { + "agent_configuration": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "agents": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "cardinality": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "groupings": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "indices_stats": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "integrations": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "processor_events": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "services": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "versions": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + } + } + }, + "version": { + "properties": { + "apm_server": { + "properties": { + "major": { + "type": "long" + }, + "minor": { + "type": "long" + }, + "patch": { + "type": "long" + } + } + } + } + } + } + }, + "application_usage_totals": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + } + } + }, + "application_usage_transactional": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + }, + "timestamp": { + "type": "date" + } + } + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "connector_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "status": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-user-actions": { + "properties": { + "action": { + "type": "keyword" + }, + "action_at": { + "type": "date" + }, + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { + "type": "text" + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "datasources": { + "properties": { + "config_id": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "properties": { + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "processors": { + "type": "keyword" + }, + "streams": { + "properties": { + "config": { + "type": "flattened" + }, + "dataset": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "processors": { + "type": "keyword" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "revision": { + "type": "integer" + } + } + }, + "enrollment_api_keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "epm-package": { + "properties": { + "installed": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "internal": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "epm-packages": { + "properties": { + "es_index_patterns": { + "dynamic": "false", + "type": "object" + }, + "installed": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "internal": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "removable": { + "type": "boolean" + }, + "version": { + "type": "keyword" + } + } + }, + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + }, + "fleet-agent-actions": { + "properties": { + "agent_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "data": { + "type": "binary" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agent-events": { + "properties": { + "action_id": { + "type": "keyword" + }, + "agent_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "data": { + "type": "text" + }, + "message": { + "type": "text" + }, + "payload": { + "type": "text" + }, + "stream_id": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "active": { + "type": "boolean" + }, + "config_id": { + "type": "keyword" + }, + "config_newest_revision": { + "type": "integer" + }, + "config_revision": { + "type": "integer" + }, + "current_error_events": { + "type": "text" + }, + "default_api_key": { + "type": "keyword" + }, + "default_api_key_id": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "flattened" + }, + "shared_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "flattened" + }, + "version": { + "type": "keyword" + } + } + }, + "fleet-enrollment-api-keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "infrastructure-ui-source": { + "properties": { + "description": { + "type": "text" + }, + "fields": { + "properties": { + "container": { + "type": "keyword" + }, + "host": { + "type": "keyword" + }, + "pod": { + "type": "keyword" + }, + "tiebreaker": { + "type": "keyword" + }, + "timestamp": { + "type": "keyword" + } + } + }, + "logAlias": { + "type": "keyword" + }, + "logColumns": { + "properties": { + "fieldColumn": { + "properties": { + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + } + } + }, + "messageColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + }, + "timestampColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + } + }, + "type": "nested" + }, + "metricAlias": { + "type": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "ingest-agent-configs": { + "properties": { + "datasources": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "monitoring_enabled": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "namespace": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest-datasources": { + "properties": { + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "properties": { + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "processors": { + "type": "keyword" + }, + "streams": { + "properties": { + "agent_stream": { + "type": "flattened" + }, + "config": { + "type": "flattened" + }, + "dataset": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "processors": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "revision": { + "type": "integer" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest-outputs": { + "properties": { + "ca_sha256": { + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "ingest_manager_settings": { + "properties": { + "agent_auto_upgrade": { + "type": "keyword" + }, + "kibana_ca_sha256": { + "type": "keyword" + }, + "kibana_url": { + "type": "keyword" + }, + "package_auto_upgrade": { + "type": "keyword" + } + } + }, + "inventory-view": { + "properties": { + "accountId": { + "type": "keyword" + }, + "autoBounds": { + "type": "boolean" + }, + "autoReload": { + "type": "boolean" + }, + "boundsOverride": { + "properties": { + "max": { + "type": "integer" + }, + "min": { + "type": "integer" + } + } + }, + "customMetrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "customOptions": { + "properties": { + "field": { + "type": "keyword" + }, + "text": { + "type": "keyword" + } + }, + "type": "nested" + }, + "filterQuery": { + "properties": { + "expression": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + } + } + }, + "groupBy": { + "properties": { + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "legend": { + "properties": { + "palette": { + "type": "keyword" + }, + "reverseColors": { + "type": "boolean" + }, + "steps": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "nodeType": { + "type": "keyword" + }, + "region": { + "type": "keyword" + }, + "sort": { + "properties": { + "by": { + "type": "keyword" + }, + "direction": { + "type": "keyword" + } + } + }, + "time": { + "type": "long" + }, + "view": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "lens": { + "properties": { + "expression": { + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "bounds": { + "type": "geo_shape" + }, + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "properties": { + "attributesPerMap": { + "properties": { + "dataSourcesCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + }, + "emsVectorLayersCount": { + "dynamic": "true", + "type": "object" + }, + "layerTypesCount": { + "dynamic": "true", + "type": "object" + }, + "layersCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + } + } + }, + "indexPatternsWithGeoFieldCount": { + "type": "long" + }, + "indexPatternsWithGeoPointFieldCount": { + "type": "long" + }, + "indexPatternsWithGeoShapeFieldCount": { + "type": "long" + }, + "mapsTotalCount": { + "type": "long" + }, + "settings": { + "properties": { + "showMapVisualizationTypes": { + "type": "boolean" + } + } + }, + "timeCaptured": { + "type": "date" + } + } + }, + "metrics-explorer-view": { + "properties": { + "chartOptions": { + "properties": { + "stack": { + "type": "boolean" + }, + "type": { + "type": "keyword" + }, + "yAxisMode": { + "type": "keyword" + } + } + }, + "currentTimerange": { + "properties": { + "from": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "to": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "options": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "filterQuery": { + "type": "keyword" + }, + "forceInterval": { + "type": "boolean" + }, + "groupBy": { + "type": "keyword" + }, + "limit": { + "type": "integer" + }, + "metrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "color": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + } + } + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "dashboard": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "ingest-agent-configs": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "ingest-datasources": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "map": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "search": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "visualization": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "outputs": { + "properties": { + "api_key": { + "type": "keyword" + }, + "ca_sha256": { + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-actions": { + "properties": { + "actions": { + "properties": { + "action_type_id": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alertThrottle": { + "type": "keyword" + }, + "ruleAlertId": { + "type": "keyword" + }, + "ruleThrottle": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-status": { + "properties": { + "alertId": { + "type": "keyword" + }, + "bulkCreateTimeDurations": { + "type": "float" + }, + "gap": { + "type": "text" + }, + "lastFailureAt": { + "type": "date" + }, + "lastFailureMessage": { + "type": "text" + }, + "lastLookBackDate": { + "type": "date" + }, + "lastSuccessAt": { + "type": "date" + }, + "lastSuccessMessage": { + "type": "text" + }, + "searchAfterTimeDurations": { + "type": "float" + }, + "status": { + "type": "keyword" + }, + "statusDate": { + "type": "date" + } + } + }, + "siem-ui-timeline": { + "properties": { + "columns": { + "properties": { + "aggregatable": { + "type": "boolean" + }, + "category": { + "type": "keyword" + }, + "columnHeaderType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "example": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "indexes": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "placeholder": { + "type": "text" + }, + "searchable": { + "type": "boolean" + }, + "type": { + "type": "keyword" + } + } + }, + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "dataProviders": { + "properties": { + "and": { + "properties": { + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + } + } + }, + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + } + } + }, + "dateRange": { + "properties": { + "end": { + "type": "date" + }, + "start": { + "type": "date" + } + } + }, + "description": { + "type": "text" + }, + "eventType": { + "type": "keyword" + }, + "favorite": { + "properties": { + "favoriteDate": { + "type": "date" + }, + "fullName": { + "type": "text" + }, + "keySearch": { + "type": "text" + }, + "userName": { + "type": "text" + } + } + }, + "filters": { + "properties": { + "exists": { + "type": "text" + }, + "match_all": { + "type": "text" + }, + "meta": { + "properties": { + "alias": { + "type": "text" + }, + "controlledBy": { + "type": "text" + }, + "disabled": { + "type": "boolean" + }, + "field": { + "type": "text" + }, + "formattedValue": { + "type": "text" + }, + "index": { + "type": "keyword" + }, + "key": { + "type": "keyword" + }, + "negate": { + "type": "boolean" + }, + "params": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "text" + } + } + }, + "missing": { + "type": "text" + }, + "query": { + "type": "text" + }, + "range": { + "type": "text" + }, + "script": { + "type": "text" + } + } + }, + "kqlMode": { + "type": "keyword" + }, + "kqlQuery": { + "properties": { + "filterQuery": { + "properties": { + "kuery": { + "properties": { + "expression": { + "type": "text" + }, + "kind": { + "type": "keyword" + } + } + }, + "serializedQuery": { + "type": "text" + } + } + } + } + }, + "savedQueryId": { + "type": "keyword" + }, + "sort": { + "properties": { + "columnId": { + "type": "keyword" + }, + "sortDirection": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "templateTimelineId": { + "type": "text" + }, + "templateTimelineVersion": { + "type": "integer" + }, + "timelineType": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-note": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-pinned-event": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "properties": { + "errorMessage": { + "type": "keyword" + }, + "indexName": { + "type": "keyword" + }, + "lastCompletedStep": { + "type": "integer" + }, + "locked": { + "type": "date" + }, + "newIndexName": { + "type": "keyword" + }, + "reindexOptions": { + "properties": { + "openAndClose": { + "type": "boolean" + }, + "queueSettings": { + "properties": { + "queuedAt": { + "type": "long" + }, + "startedAt": { + "type": "long" + } + } + } + } + }, + "reindexTaskId": { + "type": "keyword" + }, + "reindexTaskPercComplete": { + "type": "float" + }, + "runningReindexCount": { + "type": "integer" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "uptime-dynamic-settings": { + "properties": { + "certAgeThreshold": { + "type": "long" + }, + "certExpirationThreshold": { + "type": "long" + }, + "heartbeatIndices": { + "type": "keyword" + } + } + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchRefName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + ".siem-signals-default": { + "is_write_index": true + } + }, + "index": ".siem-signals-default-000001", + "mappings": { + "dynamic": "false", + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "build": { + "properties": { + "original": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "client": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "cloud": { + "properties": { + "account": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "availability_zone": { + "ignore_above": 1024, + "type": "keyword" + }, + "instance": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "machine": { + "properties": { + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "project": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "region": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "container": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "tag": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "runtime": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dll": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "compile_time": { + "type": "date" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "malware_classification": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "mapped_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "mapped_size": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dns": { + "properties": { + "answers": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "ttl": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "header_flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "op_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "question": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "subdomain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "resolved_ip": { + "type": "ip" + }, + "response_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "endpoint": { + "properties": { + "artifact": { + "properties": { + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "process": { + "properties": { + "ancestry": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "policy": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "error": { + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "message": { + "norms": false, + "type": "text" + }, + "stack_trace": { + "doc_values": false, + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "doc_values": false, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "file": { + "properties": { + "accessed": { + "type": "date" + }, + "attributes": { + "ignore_above": 1024, + "type": "keyword" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "created": { + "type": "date" + }, + "ctime": { + "type": "date" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "directory": { + "ignore_above": 1024, + "type": "keyword" + }, + "drive_letter": { + "ignore_above": 1, + "type": "keyword" + }, + "entry_modified": { + "type": "double" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "macro": { + "properties": { + "code_page": { + "type": "long" + }, + "collection": { + "properties": { + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "errors": { + "properties": { + "count": { + "type": "long" + }, + "error_type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "file_extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "project_file": { + "properties": { + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "stream": { + "properties": { + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "raw_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "raw_code_size": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + } + } + }, + "malware_classification": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "mime_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mtime": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "quarantine_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "quarantine_result": { + "type": "boolean" + }, + "size": { + "type": "long" + }, + "target_path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "temp_file_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "doc_values": false, + "index": false, + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "variant": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "http": { + "properties": { + "request": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "method": { + "ignore_above": 1024, + "type": "keyword" + }, + "referrer": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "response": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "status_code": { + "type": "long" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "log": { + "properties": { + "file": { + "properties": { + "path": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "logger": { + "ignore_above": 1024, + "type": "keyword" + }, + "origin": { + "properties": { + "file": { + "properties": { + "line": { + "type": "integer" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "function": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "original": { + "doc_values": false, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "syslog": { + "properties": { + "facility": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "priority": { + "type": "long" + }, + "severity": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "network": { + "properties": { + "application": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "community_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "direction": { + "ignore_above": 1024, + "type": "keyword" + }, + "forwarded_ip": { + "type": "ip" + }, + "iana_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "inner": { + "properties": { + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "transport": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "observer": { + "properties": { + "egress": { + "properties": { + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingress": { + "properties": { + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "variant": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "organization": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "package": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "build_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "checksum": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "install_scope": { + "ignore_above": 1024, + "type": "keyword" + }, + "installed": { + "type": "date" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "process": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "malware_classification": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "parent": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "services": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "call_stack": { + "properties": { + "instruction_pointer": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory_section": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "protection": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "module_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "rva": { + "ignore_above": 1024, + "type": "keyword" + }, + "symbol_info": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "start_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "start_address_module": { + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "elevation": { + "type": "boolean" + }, + "elevation_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "impersonation_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "integrity_level": { + "type": "long" + }, + "integrity_level_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "is_appcontainer": { + "type": "boolean" + }, + "privileges": { + "properties": { + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uptime": { + "type": "long" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "elevation": { + "type": "boolean" + }, + "elevation_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "impersonation_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "integrity_level": { + "type": "long" + }, + "integrity_level_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "is_appcontainer": { + "type": "boolean" + }, + "privileges": { + "properties": { + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uptime": { + "type": "long" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "registry": { + "properties": { + "data": { + "properties": { + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "strings": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hive": { + "ignore_above": 1024, + "type": "keyword" + }, + "key": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "related": { + "properties": { + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "author": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "ruleset": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "server": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "service": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "signal": { + "properties": { + "ancestors": { + "properties": { + "depth": { + "type": "long" + }, + "id": { + "type": "keyword" + }, + "rule": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "original_event": { + "properties": { + "action": { + "type": "keyword" + }, + "category": { + "type": "keyword" + }, + "code": { + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + }, + "module": { + "type": "keyword" + }, + "original": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "outcome": { + "type": "keyword" + }, + "provider": { + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "original_time": { + "type": "date" + }, + "parent": { + "properties": { + "depth": { + "type": "long" + }, + "id": { + "type": "keyword" + }, + "index": { + "type": "keyword" + }, + "rule": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "enabled": { + "type": "keyword" + }, + "false_positives": { + "type": "keyword" + }, + "filters": { + "type": "object" + }, + "from": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "immutable": { + "type": "keyword" + }, + "index": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "language": { + "type": "keyword" + }, + "max_signals": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "output_index": { + "type": "keyword" + }, + "query": { + "type": "keyword" + }, + "references": { + "type": "keyword" + }, + "risk_score": { + "type": "keyword" + }, + "rule_id": { + "type": "keyword" + }, + "saved_id": { + "type": "keyword" + }, + "severity": { + "type": "keyword" + }, + "size": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "threat": { + "properties": { + "framework": { + "type": "keyword" + }, + "tactic": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "reference": { + "type": "keyword" + } + } + }, + "technique": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "reference": { + "type": "keyword" + } + } + } + } + }, + "timeline_id": { + "type": "keyword" + }, + "timeline_title": { + "type": "keyword" + }, + "to": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + } + } + }, + "source": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + }, + "target": { + "properties": { + "dll": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "compile_time": { + "type": "date" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "malware_classification": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "mapped_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "mapped_size": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "process": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "malware_classification": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "parent": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "services": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "call_stack": { + "properties": { + "instruction_pointer": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory_section": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "protection": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "module_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "rva": { + "ignore_above": 1024, + "type": "keyword" + }, + "symbol_info": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "start_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "start_address_module": { + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "elevation": { + "type": "boolean" + }, + "elevation_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "impersonation_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "integrity_level": { + "type": "long" + }, + "integrity_level_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "is_appcontainer": { + "type": "boolean" + }, + "privileges": { + "properties": { + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uptime": { + "type": "long" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "elevation": { + "type": "boolean" + }, + "elevation_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "impersonation_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "integrity_level": { + "type": "long" + }, + "integrity_level_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "is_appcontainer": { + "type": "boolean" + }, + "privileges": { + "properties": { + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uptime": { + "type": "long" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "threat": { + "properties": { + "framework": { + "ignore_above": 1024, + "type": "keyword" + }, + "tactic": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "technique": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "tls": { + "properties": { + "cipher": { + "ignore_above": 1024, + "type": "keyword" + }, + "client": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "server_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + }, + "supported_ciphers": { + "ignore_above": 1024, + "type": "keyword" + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "doc_values": false, + "index": false, + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "established": { + "type": "boolean" + }, + "next_protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "resumed": { + "type": "boolean" + }, + "server": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3s": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "doc_values": false, + "index": false, + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + }, + "version_protocol": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "trace": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "transaction": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "url": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user_agent": { + "properties": { + "device": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "variant": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vulnerability": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "classification": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "enumeration": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "report_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "scanner": { + "properties": { + "vendor": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "score": { + "properties": { + "base": { + "type": "float" + }, + "environmental": { + "type": "float" + }, + "temporal": { + "type": "float" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "severity": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "lifecycle": { + "name": ".siem-signals-default", + "rollover_alias": ".siem-signals-default" + }, + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/x-pack/typings/hapi.d.ts b/x-pack/typings/hapi.d.ts index ed86a961cd1db..6af723101fc22 100644 --- a/x-pack/typings/hapi.d.ts +++ b/x-pack/typings/hapi.d.ts @@ -9,7 +9,7 @@ import 'hapi'; import { XPackMainPlugin } from '../legacy/plugins/xpack_main/server/xpack_main'; import { SecurityPlugin } from '../legacy/plugins/security'; import { ActionsPlugin, ActionsClient } from '../plugins/actions/server'; -import { AlertingPlugin, AlertsClient } from '../plugins/alerting/server'; +import { AlertingPlugin, AlertsClient } from '../plugins/alerts/server'; import { TaskManager } from '../plugins/task_manager/server'; declare module 'hapi' { @@ -21,7 +21,7 @@ declare module 'hapi' { xpack_main: XPackMainPlugin; security?: SecurityPlugin; actions?: ActionsPlugin; - alerting?: AlertingPlugin; + alerts?: AlertingPlugin; task_manager?: TaskManager; } } diff --git a/yarn.lock b/yarn.lock index e5463de13ea00..5d47056857bbf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8945,10 +8945,10 @@ chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -chromedriver@^81.0.0: - version "81.0.0" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-81.0.0.tgz#690ba333aedf2b4c4933b6590c3242d3e5f28f3c" - integrity sha512-BA++IQ7O1FzHmNpzMlOfLiSBvPZ946uuhtJjZHEIr/Gb+Ha9jiuGbHiT45l6O3XGbQ8BAwvbmdisjl4rTxro4A== +chromedriver@^83.0.0: + version "83.0.0" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-83.0.0.tgz#75d7d838e58014658c3990089464166fef951926" + integrity sha512-AePp9ykma+z4aKPRqlbzvVlc22VsQ6+rgF+0aL3B5onHOncK18dWSkLrSSJMczP/mXILN9ohGsvpuTwoRSj6OQ== dependencies: "@testim/chrome-version" "^1.0.7" axios "^0.19.2" @@ -13092,10 +13092,10 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.0.tgz#7f37d6ec17f09e6b8fc53288611695b6d12b9daf" - integrity sha512-JbDUxwV3BoT5ZVXQrSVbAiaXhXUkIwvbhPIwZ0N13kX+5yCzOhUNdocxB/UQRuYOHRYYwAxKYwJYc0T4D12pDA== +execa@^4.0.0, execa@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.2.tgz#ad87fb7b2d9d564f70d2b62d511bee41d5cbb240" + integrity sha512-QI2zLa6CjGWdiQsmSkZoGtDx2N+cQIGb3yNolGTdjSQzydzLgYYf8LRuagp7S7fPimjcrzUDSUFd/MgzELMi4Q== dependencies: cross-spawn "^7.0.0" get-stream "^5.0.0"